Merge commit 'v2.5.2' into stable-2.6
authorIustin Pop <iustin@google.com>
Tue, 24 Jul 2012 16:44:59 +0000 (18:44 +0200)
committerIustin Pop <iustin@google.com>
Wed, 25 Jul 2012 09:42:02 +0000 (11:42 +0200)
* commit 'v2.5.2':
  Fix RST formatting in NEWS file
  Update NEWS and bump version for release 2.5.2
  Fix boot=on flag for CDROMs
  KVM: only pass boot flag once

Conflicts:
        NEWS         (trivial, merged the entries)
        configure.ac (trivial, kept ours)

Signed-off-by: Iustin Pop <iustin@google.com>
Reviewed-by: Bernardo Dal Seno <bdalseno@google.com>

307 files changed:
.gitignore
INSTALL
Makefile.am
NEWS
README
autotools/build-bash-completion
autotools/build-rpc [new file with mode: 0755]
autotools/check-header [new file with mode: 0755]
autotools/check-imports [new file with mode: 0755]
autotools/check-man-dashes [new file with mode: 0755]
autotools/check-man-warnings [moved from autotools/check-man with 95% similarity]
autotools/check-news
autotools/check-python-code
autotools/convert-constants
autotools/docpp
autotools/gen-coverage
autotools/run-in-tempdir
autotools/testrunner
autotools/wrong-hardcoded-paths [new file with mode: 0644]
configure.ac
daemons/daemon-util.in
daemons/import-export
doc/admin.rst
doc/conf.py
doc/design-2.1.rst
doc/design-draft.rst
doc/design-network.rst
doc/design-node-state-cache.rst [new file with mode: 0644]
doc/design-oob.rst
doc/design-ovf-support.rst
doc/design-query-splitting.rst [new file with mode: 0644]
doc/design-resource-model.rst [new file with mode: 0644]
doc/design-shared-storage.rst
doc/design-virtual-clusters.rst [new file with mode: 0644]
doc/devnotes.rst
doc/examples/ganeti.initd.in
doc/examples/rapi_testutils.py [new file with mode: 0755]
doc/glossary.rst
doc/hooks.rst
doc/iallocator.rst
doc/index.rst
doc/install.rst
doc/ovfconverter.rst [new file with mode: 0644]
doc/rapi.rst
doc/walkthrough.rst
htools/Ganeti/BasicTypes.hs [new file with mode: 0644]
htools/Ganeti/Confd.hs [new file with mode: 0644]
htools/Ganeti/Confd/Server.hs [new file with mode: 0644]
htools/Ganeti/Config.hs [new file with mode: 0644]
htools/Ganeti/Daemon.hs [new file with mode: 0644]
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/JSON.hs [new file with mode: 0644]
htools/Ganeti/HTools/Loader.hs
htools/Ganeti/HTools/Luxi.hs
htools/Ganeti/HTools/Node.hs
htools/Ganeti/HTools/PeerMap.hs
htools/Ganeti/HTools/Program.hs [new file with mode: 0644]
htools/Ganeti/HTools/Program/Hail.hs
htools/Ganeti/HTools/Program/Hbal.hs
htools/Ganeti/HTools/Program/Hcheck.hs [new file with mode: 0644]
htools/Ganeti/HTools/Program/Hinfo.hs [new file with mode: 0644]
htools/Ganeti/HTools/Program/Hscan.hs
htools/Ganeti/HTools/Program/Hspace.hs
htools/Ganeti/HTools/QC.hs
htools/Ganeti/HTools/QCHelper.hs [new file with mode: 0644]
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/Hash.hs [new file with mode: 0644]
htools/Ganeti/Jobs.hs
htools/Ganeti/Logging.hs [new file with mode: 0644]
htools/Ganeti/Luxi.hs
htools/Ganeti/Objects.hs [new file with mode: 0644]
htools/Ganeti/OpCodes.hs
htools/Ganeti/Runtime.hs [new file with mode: 0644]
htools/Ganeti/Ssconf.hs [new file with mode: 0644]
htools/Ganeti/THH.hs [new file with mode: 0644]
htools/cli-tests-defs.sh [new file with mode: 0644]
htools/hconfd.hs [new file with mode: 0644]
htools/hpc-htools.hs [new symlink]
htools/htools.hs
htools/lint-hints.hs [new file with mode: 0644]
htools/live-test.sh
htools/offline-test.sh [new file with mode: 0755]
htools/test.hs
lib/backend.py
lib/bdev.py
lib/bootstrap.py
lib/build/shell_example_lexer.py [new file with mode: 0644]
lib/build/sphinx_ext.py
lib/cli.py
lib/client/gnt_backup.py
lib/client/gnt_cluster.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/compat.py
lib/confd/__init__.py
lib/confd/client.py
lib/confd/querylib.py
lib/confd/server.py
lib/config.py
lib/constants.py
lib/daemon.py
lib/errors.py
lib/ht.py
lib/http/client.py
lib/http/server.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/locking.py
lib/luxi.py
lib/masterd/instance.py
lib/mcpu.py
lib/netutils.py
lib/objects.py
lib/opcodes.py
lib/ovf.py [new file with mode: 0644]
lib/qlang.py
lib/query.py
lib/rapi/baserlib.py
lib/rapi/client.py
lib/rapi/connector.py
lib/rapi/rlib2.py
lib/rapi/testutils.py [new file with mode: 0644]
lib/rpc.py
lib/rpc_defs.py [new file with mode: 0644]
lib/runtime.py
lib/serializer.py
lib/server/masterd.py
lib/server/noded.py
lib/server/rapi.py
lib/ssconf.py
lib/tools/ensure_dirs.py
lib/utils/__init__.py
lib/utils/algo.py
lib/utils/io.py
lib/utils/mlock.py
lib/utils/nodesetup.py
lib/utils/process.py
lib/utils/text.py
lib/utils/wrapper.py
lib/utils/x509.py
lib/watcher/__init__.py
lib/watcher/nodemaint.py
lib/watcher/state.py
lib/workerpool.py
man/footer.rst
man/ganeti-masterd.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
man/hbal.rst
man/hcheck.rst [new file with mode: 0644]
man/hinfo.rst [new file with mode: 0644]
man/hscan.rst
man/hspace.rst
man/htools.rst
qa/__init__.py [new file with mode: 0644]
qa/ganeti-qa.py
qa/qa-sample.json
qa/qa_cluster.py
qa/qa_config.py
qa/qa_group.py
qa/qa_instance.py
qa/qa_job.py [new file with mode: 0644]
qa/qa_node.py
qa/qa_rapi.py
qa/qa_tags.py
qa/qa_utils.py
test/cfgupgrade_unittest.py
test/cli-test.bash [new file with mode: 0755]
test/daemon-util_unittest.bash
test/data/htools/common-suffix.data [new file with mode: 0644]
test/data/htools/hail-alloc-drbd.json [new file with mode: 0644]
test/data/htools/hail-change-group.json [new file with mode: 0644]
test/data/htools/hail-invalid-reloc.json [new file with mode: 0644]
test/data/htools/hail-node-evac.json [new file with mode: 0644]
test/data/htools/hail-reloc-drbd.json [new file with mode: 0644]
test/data/htools/hbal-split-insts.data [new file with mode: 0644]
test/data/htools/invalid-node.data [new file with mode: 0644]
test/data/htools/missing-resources.data [new file with mode: 0644]
test/data/htools/rapi/groups.json [new file with mode: 0644]
test/data/htools/rapi/info.json [new file with mode: 0644]
test/data/htools/rapi/instances.json [new file with mode: 0644]
test/data/htools/rapi/nodes.json [new file with mode: 0644]
test/data/ovfdata/compr_disk.vmdk.gz [new file with mode: 0644]
test/data/ovfdata/config.ini [new file with mode: 0644]
test/data/ovfdata/corrupted_resources.ovf [new file with mode: 0644]
test/data/ovfdata/empty.ini [new file with mode: 0644]
test/data/ovfdata/empty.ovf [new file with mode: 0644]
test/data/ovfdata/ganeti.mf [new file with mode: 0644]
test/data/ovfdata/ganeti.ovf [new file with mode: 0644]
test/data/ovfdata/gzip_disk.ovf [new file with mode: 0644]
test/data/ovfdata/new_disk.vmdk [new file with mode: 0644]
test/data/ovfdata/no_disk.ini [new file with mode: 0644]
test/data/ovfdata/no_disk_in_ref.ovf [new file with mode: 0644]
test/data/ovfdata/no_os.ini [new file with mode: 0644]
test/data/ovfdata/no_ovf.ova [new file with mode: 0644]
test/data/ovfdata/other/rawdisk.raw [new file with mode: 0644]
test/data/ovfdata/ova.ova [new file with mode: 0644]
test/data/ovfdata/rawdisk.raw [new file with mode: 0644]
test/data/ovfdata/second_disk.vmdk [new file with mode: 0644]
test/data/ovfdata/unsafe_path.ini [new file with mode: 0644]
test/data/ovfdata/virtualbox.ovf [new file with mode: 0644]
test/data/ovfdata/wrong_config.ini [new file with mode: 0644]
test/data/ovfdata/wrong_extension.ovd [new file with mode: 0644]
test/data/ovfdata/wrong_manifest.mf [new file with mode: 0644]
test/data/ovfdata/wrong_manifest.ovf [new file with mode: 0644]
test/data/ovfdata/wrong_ova.ova [new file with mode: 0644]
test/data/ovfdata/wrong_xml.ovf [new file with mode: 0644]
test/docs_unittest.py
test/ganeti-cli.test [new file with mode: 0644]
test/ganeti.asyncnotifier_unittest.py
test/ganeti.bdev_unittest.py
test/ganeti.cli_unittest.py
test/ganeti.client.gnt_instance_unittest.py
test/ganeti.cmdlib_unittest.py
test/ganeti.confd.client_unittest.py
test/ganeti.config_unittest.py
test/ganeti.constants_unittest.py
test/ganeti.hooks_unittest.py
test/ganeti.http_unittest.py
test/ganeti.hypervisor.hv_kvm_unittest.py
test/ganeti.jqueue_unittest.py
test/ganeti.locking_unittest.py
test/ganeti.masterd.instance_unittest.py
test/ganeti.mcpu_unittest.py
test/ganeti.netutils_unittest.py
test/ganeti.objects_unittest.py
test/ganeti.opcodes_unittest.py
test/ganeti.ovf_unittest.py [new file with mode: 0644]
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.resources_unittest.py
test/ganeti.rapi.rlib2_unittest.py
test/ganeti.rapi.testutils_unittest.py [new file with mode: 0755]
test/ganeti.rpc_unittest.py
test/ganeti.runtime_unittest.py
test/ganeti.serializer_unittest.py
test/ganeti.tools.ensure_dirs_unittest.py
test/ganeti.uidpool_unittest.py
test/ganeti.utils.algo_unittest.py
test/ganeti.utils.io_unittest-runasroot.py [new file with mode: 0644]
test/ganeti.utils.io_unittest.py
test/ganeti.utils.nodesetup_unittest.py
test/ganeti.utils.text_unittest.py
test/ganeti.utils.x509_unittest.py
test/ganeti.utils_unittest.py
test/ganeti.workerpool_unittest.py
test/gnt-cli.test [new file with mode: 0644]
test/htools-balancing.test [new file with mode: 0644]
test/htools-basic.test [new file with mode: 0644]
test/htools-dynutil.test [new file with mode: 0644]
test/htools-excl.test [new file with mode: 0644]
test/htools-hail.test [new file with mode: 0644]
test/htools-hspace.test [new file with mode: 0644]
test/htools-invalid.test [new file with mode: 0644]
test/htools-multi-group.test [new file with mode: 0644]
test/htools-no-backend.test [new file with mode: 0644]
test/htools-rapi.test [new file with mode: 0644]
test/htools-single-group.test [new file with mode: 0644]
test/htools-text-backend.test [new file with mode: 0644]
test/lockperf.py [new file with mode: 0755]
test/mocks.py
test/pycurl_reset_unittest.py [new file with mode: 0755]
test/qa.qa_config_unittest.py [new file with mode: 0755]
test/tempfile_fork_unittest.py
test/testutils.py
tools/burnin
tools/cfgupgrade
tools/cfgupgrade12
tools/cluster-merge
tools/confd-client [new file with mode: 0755]
tools/fmtjson [new file with mode: 0755]
tools/lvmstrap
tools/master-ip-setup [new file with mode: 0755]
tools/ovfconverter [new file with mode: 0755]
tools/setup-ssh

index cd88112..8631a91 100644 (file)
@@ -72,6 +72,7 @@
 # lib
 /lib/_autoconf.py
 /lib/_vcsversion.py
+/lib/_generated_rpc.py
 
 # man
 /man/*.[0-9]
 /man/*.gen
 /man/footer.man
 
+# test
+/test/hail
+/test/hbal
+/test/hcheck
+/test/hinfo
+/test/hscan
+/test/hspace
+
 # tools
 /tools/kvm-ifup
 /tools/ensure-dirs
 /htools/coverage
 
 /htools/htools
+/htools/hconfd
+/htools/hpc-htools
 /htools/test
 /htools/*.prof*
 /htools/*.stat
 /htools/*.tix
 /.hpc/
+/*.tix
 
 /htools/Ganeti/HTools/Version.hs
 /htools/Ganeti/Constants.hs
diff --git a/INSTALL b/INSTALL
index cb56e80..bd62a91 100644 (file)
--- a/INSTALL
+++ b/INSTALL
@@ -9,6 +9,8 @@ the :doc:`install`. A glossary of terms can be found in the
 Software Requirements
 ---------------------
 
+.. highlight:: shell-example
+
 Before installing, please verify that you have the following programs:
 
 - `Xen Hypervisor <http://www.xen.org/>`_, version 3.0 or above, if
@@ -16,9 +18,11 @@ Before installing, please verify that you have the following programs:
 - `KVM Hypervisor <http://www.linux-kvm.org>`_, version 72 or above, if
   running on KVM. In order to use advanced features, such as live
   migration, virtio, etc, an even newer version is recommended (qemu-kvm
-  versions 0.11.X or above have shown good behavior).
+  versions 0.11.X and above have shown good behavior).
 - `DRBD <http://www.drbd.org/>`_, kernel module and userspace utils,
-  version 8.0.7 or above
+  version 8.0.7 or above; note that Ganeti doesn't yet support version 8.4
+- `RBD <http://ceph.newdream.net/>`_, kernel modules
+  (``rbd.ko``/``libceph.ko``) and userspace utils (``ceph-common``)
 - `LVM2 <http://sourceware.org/lvm2/>`_
 - `OpenSSH <http://www.openssh.com/portable.html>`_
 - `bridge utilities <http://www.linuxfoundation.org/en/Net:Bridge>`_
@@ -39,24 +43,68 @@ Before installing, please verify that you have the following programs:
   <socat-note>` below
 - `Paramiko <http://www.lag.net/paramiko/>`_, if you want automated SSH
   setup; optional otherwise but manual setup of the nodes required
+- `affinity Python module <http://pypi.python.org/pypi/affinity/0.1.0>`_,
+  optional python package for supporting CPU pinning under KVM
+- `ElementTree Python module <http://effbot.org/zone/element-index.htm>`_,
+  if running python 2.4 (optional, used by the ``ovfconverter`` tool)
+- `qemu-img <http://qemu.org/>`_, if you want to use ``ovfconverter``
+- `fping <http://fping.sourceforge.net/>`_
 
 These programs are supplied as part of most Linux distributions, so
 usually they can be installed via the standard package manager. Also
 many of them will already be installed on a standard machine. On
 Debian/Ubuntu, you can use this command line to install all required
-packages, except for DRBD and Xen::
+packages, except for RBD, DRBD and Xen::
 
   $ apt-get install lvm2 ssh bridge-utils iproute iputils-arping \
                     ndisc6 python python-pyopenssl openssl \
                     python-pyparsing python-simplejson \
-                    python-pyinotify python-pycurl socat
+                    python-pyinotify python-pycurl socat \
+                    python-elementtree qemu
+
+On Fedora to install all required packages except RBD, DRBD and Xen::
+
+  $ yum install openssh openssh-clients bridge-utils iproute ndisc6 \
+                pyOpenSSL pyparsing python-simplejson python-inotify \
+                python-lxml python-paramiko socat qemu-img
+
+
+If you want to build from source, please see doc/devnotes.rst for more
+dependencies.
+
+.. _socat-note:
+.. note::
+  Ganeti's import/export functionality uses ``socat`` with OpenSSL for
+  transferring data between nodes. By default, OpenSSL 0.9.8 and above
+  employ transparent compression of all data using zlib if supported by
+  both sides of a connection. In cases where a lot of data is
+  transferred, this can lead to an increased CPU usage. Additionally,
+  Ganeti already compresses all data using ``gzip`` where it makes sense
+  (for inter-cluster instance moves).
+
+  To remedey this situation, patches implementing a new ``socat`` option
+  for disabling OpenSSL compression have been contributed and will
+  likely be included in the next feature release. Until then, users or
+  distributions need to apply the patches on their own.
+
+  Ganeti will use the option if it's detected by the ``configure``
+  script; auto-detection can be disabled by explicitely passing
+  ``--enable-socat-compress`` (use the option to disable compression) or
+  ``--disable-socat-compress`` (don't use the option).
+
+  The patches and more information can be found on
+  http://www.dest-unreach.org/socat/contrib/socat-opensslcompress.html.
+
+Haskell requirements
+~~~~~~~~~~~~~~~~~~~~
 
-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:
+If you want to enable the `htools` component, which is recommended on
+bigger deployments (this give you automatic instance placement, cluster
+balancing, etc.), then you need to have a Haskell compiler installed on
+your build machine (but this is not required on the machines which are
+just going to run Ganeti). More specifically:
 
-- `GHC <http://www.haskell.org/ghc/>`_ version 6.10 or higher
+- `GHC <http://www.haskell.org/ghc/>`_ version 6.12 or higher
 - or even better, `The Haskell Platform
   <http://hackage.haskell.org/platform/>`_ which gives you a simple way
   to bootstrap Haskell
@@ -74,52 +122,65 @@ 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
 
+Or in Fedora running::
+
+  $ yum install ghc ghc-json-devel ghc-network-devel ghc-parallel-devel
+
+The most recent Fedora doesn't provide ``ghc-curl``. So this needs to be
+installed using ``cabal`` or alternatively htools can be build without
+curl support.
+
 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
 
+If using a distribution which does not provide them, the first install
+the Haskell platform and then install the additional libraries via
+``cabal``::
+
+  $ cabal install json network parallel curl
+
 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.
+In Ganeti version 2.6, one of the daemons (``ganeti-confd``) is shipped
+in two versions: the Python default version (which has no extra
+dependencies), and an experimental Haskell version. This latter version
+can be enabled via the ``./configure`` flag ``--enable-confd=haskell``
+and a few has extra dependencies:
 
-.. _socat-note:
-.. note::
-  Ganeti's import/export functionality uses ``socat`` with OpenSSL for
-  transferring data between nodes. By default, OpenSSL 0.9.8 and above
-  employ transparent compression of all data using zlib if supported by
-  both sides of a connection. In cases where a lot of data is
-  transferred, this can lead to an increased CPU usage. Additionally,
-  Ganeti already compresses all data using ``gzip`` where it makes sense
-  (for inter-cluster instance moves).
+- `hslogger <http://software.complete.org/hslogger>`_, version 1.1 and
+  above (note that Debian Squeeze only has version 1.0.9)
+- `Crypto <http://hackage.haskell.org/package/Crypto>`_, tested with
+  version 4.2.4
+- `text <http://hackage.haskell.org/package/text>`_
+- ``bytestring``, which usually comes with the compiler
+- `hinotify <http://hackage.haskell.org/package/hinotify>`_
 
-  To remedey this situation, patches implementing a new ``socat`` option
-  for disabling OpenSSL compression have been contributed and will
-  likely be included in the next feature release. Until then, users or
-  distributions need to apply the patches on their own.
+These libraries are available in Debian Wheezy (but not in Squeeze), so
+you can use either apt::
 
-  Ganeti will use the option if it's detected by the ``configure``
-  script; auto-detection can be disabled by explicitely passing
-  ``--enable-socat-compress`` (use the option to disable compression) or
-  ``--disable-socat-compress`` (don't use the option).
+  $ apt-get install libghc-hslogger-dev libghc-crypto-dev libghc-text-dev \
+                    libghc-hinotify-dev
 
-  The patches and more information can be found on
-  http://www.dest-unreach.org/socat/contrib/socat-opensslcompress.html.
+or ``cabal``::
+
+  $ cabal install hslogger Crypto text hinotify
 
+to install them.
 
 Installation of the software
 ----------------------------
 
 To install, simply run the following command::
 
-  ./configure --localstatedir=/var --sysconfdir=/etc && \
-  make && \
-  make install
+  $ ./configure --localstatedir=/var --sysconfdir=/etc && \
+    make && \
+    make install
 
 This will install the software under ``/usr/local``. You then need to
 copy ``doc/examples/ganeti.initd`` to ``/etc/init.d/ganeti`` and
index 669e614..29deca7 100644 (file)
@@ -21,12 +21,16 @@ ACLOCAL_AMFLAGS = -I autotools
 BUILD_BASH_COMPLETION = $(top_srcdir)/autotools/build-bash-completion
 RUN_IN_TEMPDIR = $(top_srcdir)/autotools/run-in-tempdir
 CHECK_PYTHON_CODE = $(top_srcdir)/autotools/check-python-code
-CHECK_MAN = $(top_srcdir)/autotools/check-man
+CHECK_HEADER = $(top_srcdir)/autotools/check-header
+CHECK_MAN_DASHES = $(top_srcdir)/autotools/check-man-dashes
+CHECK_MAN_WARNINGS = $(top_srcdir)/autotools/check-man-warnings
 CHECK_VERSION = $(top_srcdir)/autotools/check-version
 CHECK_NEWS = $(top_srcdir)/autotools/check-news
+CHECK_IMPORTS = $(top_srcdir)/autotools/check-imports
 DOCPP = $(top_srcdir)/autotools/docpp
 REPLACE_VARS_SED = autotools/replace_vars.sed
 CONVERT_CONSTANTS = $(top_srcdir)/autotools/convert-constants
+BUILD_RPC = $(top_srcdir)/autotools/build-rpc
 
 # Note: these are automake-specific variables, and must be named after
 # the directory + 'dir' suffix
@@ -52,6 +56,7 @@ myexeclibdir = $(pkglibdir)
 HTOOLS_DIRS = \
        htools \
        htools/Ganeti \
+       htools/Ganeti/Confd \
        htools/Ganeti/HTools \
        htools/Ganeti/HTools/Program
 
@@ -81,6 +86,10 @@ DIRS = \
        qa \
        test \
        test/data \
+       test/data/htools \
+       test/data/htools/rapi \
+       test/data/ovfdata \
+       test/data/ovfdata/other \
        tools
 
 BUILDTIME_DIR_AUTOCREATE = \
@@ -88,7 +97,9 @@ BUILDTIME_DIR_AUTOCREATE = \
        $(APIDOC_DIR) \
        $(APIDOC_PY_DIR) \
        $(APIDOC_HS_DIR) \
-       $(APIDOC_HS_DIR)/Ganeti $(APIDOC_HS_DIR)/Ganeti/HTools \
+       $(APIDOC_HS_DIR)/Ganeti \
+       $(APIDOC_HS_DIR)/Ganeti/Confd \
+       $(APIDOC_HS_DIR)/Ganeti/HTools \
        $(APIDOC_HS_DIR)/Ganeti/HTools/Program \
        $(COVERAGE_DIR) \
        $(COVERAGE_PY_DIR) \
@@ -136,40 +147,58 @@ CLEANFILES = \
        daemons/daemon-util \
        daemons/ganeti-cleaner \
        devel/upload \
+       $(BUILT_EXAMPLES) \
        doc/examples/bash_completion \
-       doc/examples/ganeti.initd \
-       doc/examples/ganeti-kvm-poweroff.initd \
-       doc/examples/ganeti.cron \
-       doc/examples/gnt-config-backup \
-       doc/examples/hooks/ipsec \
+       lib/_generated_rpc.py \
        $(man_MANS) \
        $(manhtml) \
        tools/kvm-ifup \
        stamp-srclinks \
        $(nodist_pkgpython_PYTHON) \
        $(HS_ALL_PROGS) $(HS_BUILT_SRCS) \
+       $(HS_BUILT_TEST_HELPERS) \
        .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.
 BUILT_SOURCES = \
+  $(built_base_sources) \
+       $(BUILT_PYTHON_SOURCES) \
+       $(PYTHON_BOOTSTRAP)
+
+built_base_sources = \
        ganeti \
        stamp-srclinks \
-       $(all_dirfiles) \
-       $(PYTHON_BOOTSTRAP) \
-       $(BUILT_PYTHON_SOURCES)
+       $(all_dirfiles)
 
-BUILT_PYTHON_SOURCES = \
+built_python_base_sources = \
        lib/_autoconf.py \
        lib/_vcsversion.py
 
+BUILT_PYTHON_SOURCES = \
+       $(built_python_base_sources) \
+       lib/_generated_rpc.py
+
+# Generating the RPC wrappers depends on many things, so make sure it's built at
+# the end of the built sources
+lib/_generated_rpc.py: | $(built_base_sources) $(built_python_base_sources)
+
+# these are all built from the underlying %.in sources
+BUILT_EXAMPLES = \
+       doc/examples/ganeti-kvm-poweroff.initd \
+       doc/examples/ganeti.cron \
+       doc/examples/ganeti.initd \
+       doc/examples/gnt-config-backup \
+       doc/examples/hooks/ipsec
+
 nodist_pkgpython_PYTHON = \
        $(BUILT_PYTHON_SOURCES)
 
 noinst_PYTHON = \
        lib/build/__init__.py \
-       lib/build/sphinx_ext.py
+       lib/build/sphinx_ext.py \
+       lib/build/shell_example_lexer.py
 
 pkgpython_PYTHON = \
        lib/__init__.py \
@@ -193,9 +222,11 @@ pkgpython_PYTHON = \
        lib/netutils.py \
        lib/objects.py \
        lib/opcodes.py \
+       lib/ovf.py \
        lib/qlang.py \
        lib/query.py \
        lib/rpc.py \
+       lib/rpc_defs.py \
        lib/runtime.py \
        lib/serializer.py \
        lib/ssconf.py \
@@ -230,7 +261,8 @@ rapi_PYTHON = \
        lib/rapi/client.py \
        lib/rapi/client_utils.py \
        lib/rapi/connector.py \
-       lib/rapi/rlib2.py
+       lib/rapi/rlib2.py \
+       lib/rapi/testutils.py
 
 http_PYTHON = \
        lib/http/__init__.py \
@@ -303,8 +335,11 @@ docrst = \
        doc/design-network.rst \
        doc/design-chained-jobs.rst \
        doc/design-ovf-support.rst \
+       doc/design-resource-model.rst \
        doc/cluster-merge.rst \
        doc/design-shared-storage.rst \
+       doc/design-node-state-cache.rst \
+       doc/design-virtual-clusters.rst \
        doc/devnotes.rst \
        doc/glossary.rst \
        doc/hooks.rst \
@@ -321,17 +356,23 @@ docrst = \
        doc/walkthrough.rst
 
 HS_PROGS = htools/htools
-HS_BIN_ROLES = hbal hscan hspace
+HS_BIN_ROLES = hbal hscan hspace hinfo hcheck
 
-HS_ALL_PROGS = $(HS_PROGS) htools/test
+HS_ALL_PROGS = $(HS_PROGS) htools/test htools/hpc-htools htools/hconfd
 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
+HS_BUILT_TEST_HELPERS = $(HS_BIN_ROLES:%=test/%) test/hail
+
+HFLAGS = -O -Wall -Werror -fwarn-monomorphism-restriction -fwarn-tabs -ihtools
+# extra flags that can be overriden on the command line (e.g. -Wwarn, etc.)
 HEXTRA =
+# internal extra flags (used for htools/test mainly)
+HEXTRA_INT =
 # exclude options for coverage reports
-HPCEXCL = --exclude Main --exclude Ganeti.HTools.QC \
+HPCEXCL = --exclude Main \
        --exclude Ganeti.Constants \
+       --exclude Ganeti.THH \
+       --exclude Ganeti.HTools.QC \
+       --exclude Ganeti.HTools.QCHelper \
        --exclude Ganeti.HTools.Version
 
 HS_LIB_SRCS = \
@@ -343,23 +384,39 @@ HS_LIB_SRCS = \
        htools/Ganeti/HTools/Group.hs \
        htools/Ganeti/HTools/IAlloc.hs \
        htools/Ganeti/HTools/Instance.hs \
+       htools/Ganeti/HTools/JSON.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/QCHelper.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.hs \
        htools/Ganeti/HTools/Program/Hail.hs \
        htools/Ganeti/HTools/Program/Hbal.hs \
+       htools/Ganeti/HTools/Program/Hcheck.hs \
+       htools/Ganeti/HTools/Program/Hinfo.hs \
        htools/Ganeti/HTools/Program/Hscan.hs \
        htools/Ganeti/HTools/Program/Hspace.hs \
+       htools/Ganeti/BasicTypes.hs \
+       htools/Ganeti/Confd.hs \
+       htools/Ganeti/Confd/Server.hs \
+       htools/Ganeti/Config.hs \
+       htools/Ganeti/Daemon.hs \
+       htools/Ganeti/Hash.hs \
        htools/Ganeti/Jobs.hs \
+       htools/Ganeti/Logging.hs \
        htools/Ganeti/Luxi.hs \
-       htools/Ganeti/OpCodes.hs
+       htools/Ganeti/Objects.hs \
+       htools/Ganeti/OpCodes.hs \
+       htools/Ganeti/Runtime.hs \
+       htools/Ganeti/Ssconf.hs \
+       htools/Ganeti/THH.hs
 
 HS_BUILT_SRCS = htools/Ganeti/HTools/Version.hs htools/Ganeti/Constants.hs
 HS_BUILT_SRCS_IN = $(patsubst %,%.in,$(HS_BUILT_SRCS))
@@ -372,7 +429,8 @@ $(RUN_IN_TEMPDIR): | $(all_dirfiles)
 # 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) lib/build/sphinx_ext.py lib/opcodes.py lib/ht.py \
+       $(RUN_IN_TEMPDIR) lib/build/sphinx_ext.py \
+       lib/build/shell_example_lexer.py lib/opcodes.py lib/ht.py \
        | $(BUILT_PYTHON_SOURCES)
        @test -n "$(SPHINX)" || \
            { echo 'sphinx-build' not found during configure; exit 1; }
@@ -411,12 +469,8 @@ docpng = $(patsubst %.dot,%.png,$(docdot))
 noinst_DATA = \
        devel/upload \
        doc/html \
+       $(BUILT_EXAMPLES) \
        doc/examples/bash_completion \
-       doc/examples/ganeti.cron \
-       doc/examples/ganeti.initd \
-       doc/examples/ganeti-kvm-poweroff.initd \
-       doc/examples/gnt-config-backup \
-       doc/examples/hooks/ipsec \
        $(manhtml)
 
 gnt_scripts = \
@@ -430,25 +484,22 @@ gnt_scripts = \
        scripts/gnt-os
 
 PYTHON_BOOTSTRAP_SBIN = \
-       daemons/ganeti-confd \
        daemons/ganeti-masterd \
        daemons/ganeti-noded \
        daemons/ganeti-watcher \
        daemons/ganeti-rapi \
-       scripts/gnt-backup \
-       scripts/gnt-cluster \
-       scripts/gnt-debug \
-       scripts/gnt-group \
-       scripts/gnt-instance \
-       scripts/gnt-job \
-       scripts/gnt-node \
-       scripts/gnt-os
+       $(gnt_scripts)
+
+if PY_CONFD
+PYTHON_BOOTSTRAP_SBIN += daemons/ganeti-confd
+endif
 
 PYTHON_BOOTSTRAP = \
        $(PYTHON_BOOTSTRAP_SBIN) \
        tools/ensure-dirs
 
 qa_scripts = \
+       qa/__init__.py \
        qa/ganeti-qa.py \
        qa/qa_cluster.py \
        qa/qa_config.py \
@@ -457,6 +508,7 @@ qa_scripts = \
        qa/qa_error.py \
        qa/qa_group.py \
        qa/qa_instance.py \
+       qa/qa_job.py \
        qa/qa_node.py \
        qa/qa_os.py \
        qa/qa_rapi.py \
@@ -475,6 +527,9 @@ install-exec-hook:
                $(LN_S) -f htools \
                           $(DESTDIR)$(bindir)/$$role ; \
        done
+if HS_CONFD
+       mv $(DESTDIR)$(sbindir)/hconfd $(DESTDIR)$(sbindir)/ganeti-confd
+endif
 endif
 
 $(HS_ALL_PROGS): %: %.hs $(HS_LIB_SRCS) $(HS_BUILT_SRCS) Makefile
@@ -487,16 +542,39 @@ $(HS_ALL_PROGS): %: %.hs $(HS_LIB_SRCS) $(HS_BUILT_SRCS) Makefile
          echo "Error: cannot run unittests without the QuickCheck library (see devnotes.rst)" 1>&2; \
          exit 1; \
        fi
+       rm -f $(@:htools/%=%).tix
        BINARY=$(@:htools/%=%); $(GHC) --make \
-         $(HFLAGS) $(HEXTRA) \
+         $(HFLAGS) \
          $(HTOOLS_NOCURL) $(HTOOLS_PARALLEL3) \
-         -osuf $$BINARY.o -hisuf $$BINARY.hi $@
+         -osuf $$BINARY.o -hisuf $$BINARY.hi \
+         $(HEXTRA) $(HEXTRA_INT) $@
+       @touch "$@"
 
 # for the htools/test binary, we need to enable profiling/coverage
-htools/test: HEXTRA=-fhpc -Wwarn -fno-warn-missing-signatures \
+htools/test: HEXTRA_INT=-fhpc -Wwarn -fno-warn-missing-signatures \
        -fno-warn-monomorphism-restriction -fno-warn-orphans \
        -fno-warn-missing-methods -fno-warn-unused-imports
 
+# we compile the hpc-htools binary with the program coverage
+htools/hpc-htools: HEXTRA_INT=-fhpc
+
+# test dependency
+htools/offline-tests.sh: htools/hpc-htools
+
+# rules for building profiling-enabled versions of the haskell
+# programs: hs-prof does the full two-step build, whereas
+# hs-prof-quick does only the final rebuild (hs-prof must have been
+# run before)
+.PHONY: hs-prof hs-prof-quick
+hs-prof:
+       $(MAKE) clean
+       $(MAKE) $(HS_ALL_PROGS) HEXTRA="-osuf .o"
+       rm -f $(HS_ALL_PROGS)
+       $(MAKE) hs-prof-quick
+
+hs-prof-quick:
+       $(MAKE) $(HS_ALL_PROGS) HEXTRA="-osuf .prof_o -prof -auto-all"
+
 dist_sbin_SCRIPTS = \
        tools/ganeti-listrunner
 
@@ -504,21 +582,29 @@ nodist_sbin_SCRIPTS = \
        $(PYTHON_BOOTSTRAP_SBIN) \
        daemons/ganeti-cleaner
 
+if HS_CONFD
+nodist_sbin_SCRIPTS += htools/hconfd
+endif
+
 python_scripts = \
        tools/burnin \
        tools/cfgshell \
        tools/cfgupgrade \
        tools/cfgupgrade12 \
        tools/cluster-merge \
+       tools/confd-client \
+       tools/fmtjson \
        tools/lvmstrap \
        tools/move-instance \
+       tools/ovfconverter \
        tools/setup-ssh \
        tools/sanitize-config
 
 dist_tools_SCRIPTS = \
        $(python_scripts) \
        tools/kvm-console-wrapper \
-       tools/xm-console-wrapper
+       tools/xm-console-wrapper \
+       tools/master-ip-setup
 
 pkglib_python_scripts = \
        daemons/import-export \
@@ -541,8 +627,12 @@ EXTRA_DIST = \
        epydoc.conf.in \
        pylintrc \
        autotools/build-bash-completion \
+       autotools/build-rpc \
+       autotools/check-header \
        autotools/check-python-code \
-       autotools/check-man \
+       autotools/check-imports \
+       autotools/check-man-dashes \
+       autotools/check-man-warnings \
        autotools/check-news \
        autotools/check-tar \
        autotools/check-version \
@@ -550,6 +640,7 @@ EXTRA_DIST = \
        autotools/docpp \
        autotools/gen-coverage \
        autotools/testrunner \
+       autotools/wrong-hardcoded-paths \
        $(RUN_IN_TEMPDIR) \
        daemons/daemon-util.in \
        daemons/ganeti-cleaner.in \
@@ -561,17 +652,14 @@ EXTRA_DIST = \
        $(docrst) \
        doc/conf.py \
        doc/html \
-       doc/examples/ganeti.initd.in \
-       doc/examples/ganeti-kvm-poweroff.initd.in \
-       doc/examples/ganeti.cron.in \
-       doc/examples/gnt-config-backup.in \
+       $(BUILT_EXAMPLES:%=%.in) \
        doc/examples/ganeti.default \
        doc/examples/ganeti.default-debug \
        doc/examples/hooks/ethers \
-       doc/examples/hooks/ipsec.in \
        doc/examples/gnt-debug/README \
        doc/examples/gnt-debug/delay0.json \
        doc/examples/gnt-debug/delay50.json \
+       test/lockperf.py \
        test/testutils.py \
        test/mocks.py \
        $(dist_TESTS) \
@@ -582,7 +670,10 @@ EXTRA_DIST = \
        qa/qa-sample.json \
        $(qa_scripts) \
        $(HS_LIB_SRCS) $(HS_BUILT_SRCS_IN) \
-       $(HS_PROG_SRCS)
+       $(HS_PROG_SRCS) \
+       htools/lint-hints.hs \
+       htools/cli-tests-defs.sh \
+       htools/offline-test.sh
 
 man_MANS = \
        man/ganeti.7 \
@@ -604,6 +695,8 @@ man_MANS = \
        man/gnt-os.8 \
        man/hail.1 \
        man/hbal.1 \
+       man/hcheck.1 \
+       man/hinfo.1 \
        man/hscan.1 \
        man/hspace.1 \
        man/htools.1
@@ -640,14 +733,67 @@ TEST_FILES = \
        test/data/kvm_0.12.5_help.txt \
        test/data/kvm_0.9.1_help.txt \
        test/data/sys_drbd_usermode_helper.txt \
+       test/data/htools/hail-alloc-drbd.json \
+       test/data/htools/hail-change-group.json \
+       test/data/htools/hail-invalid-reloc.json \
+       test/data/htools/hail-node-evac.json \
+       test/data/htools/hail-reloc-drbd.json \
+       test/data/htools/hbal-split-insts.data \
+       test/data/htools/common-suffix.data \
+       test/data/htools/invalid-node.data \
+       test/data/htools/missing-resources.data \
+       test/data/htools/rapi/groups.json \
+       test/data/htools/rapi/info.json \
+       test/data/htools/rapi/instances.json \
+       test/data/htools/rapi/nodes.json \
+       test/data/ovfdata/compr_disk.vmdk.gz \
+       test/data/ovfdata/config.ini \
+       test/data/ovfdata/corrupted_resources.ovf \
+       test/data/ovfdata/empty.ini \
+       test/data/ovfdata/empty.ovf \
+       test/data/ovfdata/ganeti.mf \
+       test/data/ovfdata/ganeti.ovf \
+       test/data/ovfdata/gzip_disk.ovf \
+       test/data/ovfdata/new_disk.vmdk \
+       test/data/ovfdata/no_disk.ini \
+       test/data/ovfdata/no_disk_in_ref.ovf \
+       test/data/ovfdata/no_os.ini \
+       test/data/ovfdata/no_ovf.ova \
+       test/data/ovfdata/ova.ova \
+       test/data/ovfdata/second_disk.vmdk \
+       test/data/ovfdata/rawdisk.raw \
+       test/data/ovfdata/unsafe_path.ini \
+       test/data/ovfdata/virtualbox.ovf \
+       test/data/ovfdata/wrong_extension.ovd \
+       test/data/ovfdata/wrong_config.ini \
+       test/data/ovfdata/wrong_manifest.mf \
+       test/data/ovfdata/wrong_manifest.ovf \
+       test/data/ovfdata/wrong_ova.ova \
+       test/data/ovfdata/wrong_xml.ovf \
+       test/data/ovfdata/other/rawdisk.raw \
        test/data/vgreduce-removemissing-2.02.02.txt \
        test/data/vgreduce-removemissing-2.02.66-fail.txt \
        test/data/vgreduce-removemissing-2.02.66-ok.txt \
        test/data/vgs-missing-pvs-2.02.02.txt \
        test/data/vgs-missing-pvs-2.02.66.txt \
-       test/import-export_unittest-helper
+       test/import-export_unittest-helper \
+       test/gnt-cli.test \
+       test/ganeti-cli.test \
+       test/htools-balancing.test \
+       test/htools-basic.test \
+       test/htools-dynutil.test \
+       test/htools-excl.test \
+       test/htools-hail.test \
+       test/htools-hspace.test \
+       test/htools-invalid.test \
+       test/htools-multi-group.test \
+       test/htools-no-backend.test \
+       test/htools-rapi.test \
+       test/htools-single-group.test \
+       test/htools-text-backend.test
 
 python_tests = \
+       doc/examples/rapi_testutils.py \
        test/ganeti.asyncnotifier_unittest.py \
        test/ganeti.backend_unittest.py \
        test/ganeti.bdev_unittest.py \
@@ -679,12 +825,14 @@ python_tests = \
        test/ganeti.netutils_unittest.py \
        test/ganeti.objects_unittest.py \
        test/ganeti.opcodes_unittest.py \
+       test/ganeti.ovf_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.resources_unittest.py \
        test/ganeti.rapi.rlib2_unittest.py \
+       test/ganeti.rapi.testutils_unittest.py \
        test/ganeti.rpc_unittest.py \
        test/ganeti.runtime_unittest.py \
        test/ganeti.serializer_unittest.py \
@@ -706,9 +854,14 @@ python_tests = \
        test/ganeti.utils.x509_unittest.py \
        test/ganeti.utils_unittest.py \
        test/ganeti.workerpool_unittest.py \
+       test/qa.qa_config_unittest.py \
        test/cfgupgrade_unittest.py \
        test/docs_unittest.py \
+       test/pycurl_reset_unittest.py \
        test/tempfile_fork_unittest.py
+if HAS_FAKEROOT
+python_tests += test/ganeti.utils.io_unittest-runasroot.py
+endif
 
 haskell_tests = htools/test
 
@@ -717,18 +870,26 @@ dist_TESTS = \
        test/daemon-util_unittest.bash \
        test/ganeti-cleaner_unittest.bash \
        test/import-export_unittest.bash \
+       test/cli-test.bash \
        $(python_tests)
 
 nodist_TESTS =
+check_SCRIPTS =
+
 if WANT_HTOOLSTESTS
 nodist_TESTS += $(haskell_tests)
+dist_TESTS += htools/offline-test.sh
+check_SCRIPTS += htools/hpc-htools $(HS_BUILT_TEST_HELPERS)
 endif
 
 TESTS = $(dist_TESTS) $(nodist_TESTS)
 
 # Environment for all tests
 PLAIN_TESTS_ENVIRONMENT = \
-       PYTHONPATH=. TOP_SRCDIR=$(abs_top_srcdir) PYTHON=$(PYTHON) $(RUN_IN_TEMPDIR)
+       PYTHONPATH=. \
+       TOP_SRCDIR=$(abs_top_srcdir) TOP_BUILDDIR=$(abs_top_builddir) \
+       PYTHON=$(PYTHON) FAKEROOT=$(FAKEROOT_PATH) \
+       $(RUN_IN_TEMPDIR)
 
 # Environment for tests run by automake
 TESTS_ENVIRONMENT = \
@@ -761,11 +922,16 @@ srclink_files = \
        test/daemon-util_unittest.bash \
        test/ganeti-cleaner_unittest.bash \
        test/import-export_unittest.bash \
+       test/cli-test.bash \
+       htools/offline-test.sh \
+       htools/cli-tests-defs.sh \
        $(all_python_code) \
        $(HS_LIB_SRCS) $(HS_PROG_SRCS)
 
 check_python_code = \
        $(BUILD_BASH_COMPLETION) \
+       $(CHECK_IMPORTS) \
+       $(CHECK_HEADER) \
        $(DOCPP) \
        $(all_python_code)
 
@@ -776,9 +942,15 @@ lint_python_code = \
        $(python_scripts) \
        $(pkglib_python_scripts) \
        $(BUILD_BASH_COMPLETION) \
+       $(CHECK_IMPORTS) \
+       $(CHECK_HEADER) \
        $(DOCPP) \
        $(PYTHON_BOOTSTRAP)
 
+standalone_python_modules = \
+       lib/rapi/client.py \
+       tools/ganeti-listrunner
+
 pep8_python_code = \
        ganeti \
        ganeti/http/server.py \
@@ -786,6 +958,7 @@ pep8_python_code = \
        $(python_scripts) \
        $(pkglib_python_scripts) \
        $(BUILD_BASH_COMPLETION) \
+       $(CHECK_HEADER) \
        $(DOCPP) \
        $(PYTHON_BOOTSTRAP) \
        qa
@@ -831,7 +1004,13 @@ man/footer.html: man/footer.rst
        $(PANDOC) -f rst -t html -o $@ $<
 
 man/%.gen: man/%.rst lib/query.py lib/build/sphinx_ext.py \
+       lib/build/shell_example_lexer.py \
        | $(RUN_IN_TEMPDIR) $(BUILT_PYTHON_SOURCES)
+       @echo "Checking $< for hardcoded paths..."
+       @if grep -nEf autotools/wrong-hardcoded-paths $<; then \
+         echo "Man page $< has harcoded paths (see above)!" 1>&2 ; \
+         exit 1; \
+       fi
        set -e ; \
        trap 'echo auto-removing $@; rm $@' EXIT; \
        PYTHONPATH=. $(RUN_IN_TEMPDIR) $(CURDIR)/$(DOCPP) < $< > $@ ;\
@@ -841,9 +1020,13 @@ 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 ; \
+       trap 'echo auto-removing $@; rm $@' EXIT; \
        $(PANDOC) -s -f rst -t man -A man/footer.man $< | \
-         sed -e 's/\\@/@/g' > $@
-       if test -n "$(MAN_HAS_WARNINGS)"; then $(CHECK_MAN) $@; fi
+         sed -e 's/\\@/@/g' > $@; \
+       if test -n "$(MAN_HAS_WARNINGS)"; then $(CHECK_MAN_WARNINGS) $@; fi; \
+       $(CHECK_MAN_DASHES) $@; \
+       trap - EXIT
+
 
 man/%.html.in: man/%.gen man/footer.html
        @test -n "$(PANDOC)" || \
@@ -874,12 +1057,16 @@ vcs-version:
          echo "Cannot auto-generate $@ file"; exit 1; \
        fi
 
+.PHONY: clean-vcs-version
+clean-vcs-version:
+       rm -f vcs-version
+
 .PHONY: regen-vcs-version
 regen-vcs-version:
        set -e; \
        cd $(srcdir); \
        if test -d .git; then \
-         rm -f vcs-version; \
+         $(MAKE) clean-vcs-version; \
          $(MAKE) vcs-version; \
        fi
 
@@ -889,7 +1076,9 @@ htools/Ganeti/HTools/Version.hs: htools/Ganeti/HTools/Version.hs.in vcs-version
        sed -e "s/%ver%/$$VCSVER/" < $< > $@
 
 htools/Ganeti/Constants.hs: htools/Ganeti/Constants.hs.in \
-       lib/constants.py lib/_autoconf.py $(CONVERT_CONSTANTS)
+       lib/constants.py lib/_autoconf.py lib/luxi.py \
+       $(CONVERT_CONSTANTS) \
+       | lib/_vcsversion.py
        set -e; \
        { cat $< ; PYTHONPATH=. $(CONVERT_CONSTANTS); } > $@
 
@@ -906,7 +1095,7 @@ lib/_autoconf.py: Makefile | lib/.dir
          echo ''; \
          echo '"""'; \
          echo ''; \
-         echo '# pylint: disable-msg=C0301,C0324'; \
+         echo '# pylint: disable=C0301,C0324'; \
          echo '# because this is autogenerated, we do not want'; \
          echo '# style warnings' ; \
          echo ''; \
@@ -924,6 +1113,7 @@ lib/_autoconf.py: Makefile | lib/.dir
          echo "XEN_BOOTLOADER = '$(XEN_BOOTLOADER)'"; \
          echo "XEN_KERNEL = '$(XEN_KERNEL)'"; \
          echo "XEN_INITRD = '$(XEN_INITRD)'"; \
+         echo "KVM_KERNEL = '$(KVM_KERNEL)'"; \
          echo "FILE_STORAGE_DIR = '$(FILE_STORAGE_DIR)'"; \
          echo "ENABLE_FILE_STORAGE = $(ENABLE_FILE_STORAGE)"; \
          echo "SHARED_FILE_STORAGE_DIR = '$(SHARED_FILE_STORAGE_DIR)'"; \
@@ -938,7 +1128,8 @@ lib/_autoconf.py: Makefile | lib/.dir
          echo "TOOLSDIR = '$(toolsdir)'"; \
          echo "GNT_SCRIPTS = [$(foreach i,$(notdir $(gnt_scripts)),'$(i)',)]"; \
          echo "PKGLIBDIR = '$(pkglibdir)'"; \
-         echo "DRBD_BARRIERS = $(DRBD_BARRIERS)"; \
+         echo "DRBD_BARRIERS = '$(DRBD_BARRIERS)'"; \
+         echo "DRBD_NO_META_FLUSH = $(DRBD_NO_META_FLUSH)"; \
          echo "SYSLOG_USAGE = '$(SYSLOG_USAGE)'"; \
          echo "DAEMONS_GROUP = '$(DAEMONS_GROUP)'"; \
          echo "ADMIN_GROUP = '$(ADMIN_GROUP)'"; \
@@ -951,11 +1142,16 @@ lib/_autoconf.py: Makefile | lib/.dir
          echo "NODED_USER = '$(NODED_USER)'"; \
          echo "NODED_GROUP = '$(NODED_GROUP)'"; \
          echo "DISK_SEPARATOR = '$(DISK_SEPARATOR)'"; \
+         echo "QEMUIMG_PATH = '$(QEMUIMG_PATH)'"; \
          if [ "$(HTOOLS)" ]; then \
            echo "HTOOLS = True"; \
          else \
            echo "HTOOLS = False"; \
          fi; \
+         echo "ENABLE_CONFD = $(ENABLE_CONFD)"; \
+         echo "PY_CONFD = $(PY_CONFD)"; \
+         echo "HS_CONFD = $(HS_CONFD)"; \
+         echo "XEN_CMD = '$(XEN_CMD)'"; \
        } > $@
 
 lib/_vcsversion.py: Makefile vcs-version | lib/.dir
@@ -972,13 +1168,16 @@ lib/_vcsversion.py: Makefile vcs-version | lib/.dir
          echo ''; \
          echo '"""'; \
          echo ''; \
-         echo '# pylint: disable-msg=C0301,C0324'; \
+         echo '# pylint: disable=C0301,C0324'; \
          echo '# because this is autogenerated, we do not want'; \
          echo '# style warnings' ; \
          echo ''; \
          echo "VCS_VERSION = '$$VCSVER'"; \
        } > $@
 
+lib/_generated_rpc.py: lib/rpc_defs.py $(BUILD_RPC)
+       PYTHONPATH=. $(RUN_IN_TEMPDIR) $(CURDIR)/$(BUILD_RPC) lib/rpc_defs.py > $@
+
 $(REPLACE_VARS_SED): Makefile
        set -e; \
        { echo 's#@PREFIX@#$(prefix)#g'; \
@@ -1004,6 +1203,7 @@ $(REPLACE_VARS_SED): Makefile
          echo 's#@GNTCONFDGROUP@#$(CONFD_GROUP)#g'; \
          echo 's#@GNTMASTERDGROUP@#$(MASTERD_GROUP)#g'; \
          echo 's#@GNTDAEMONSGROUP@#$(DAEMONS_GROUP)#g'; \
+         echo 's#@CUSTOM_ENABLE_CONFD@#$(ENABLE_CONFD)#g'; \
        } > $@
 
 # Using deferred evaluation
@@ -1011,6 +1211,7 @@ 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
+$(HS_BUILT_TEST_HELPERS): TESTROLE = $(patsubst test/%,%,$@)
 
 $(PYTHON_BOOTSTRAP): Makefile | $(all_dirfiles)
        test -n "$(MODULE)" || { echo Missing module; exit 1; }
@@ -1021,7 +1222,7 @@ $(PYTHON_BOOTSTRAP): Makefile | $(all_dirfiles)
          echo; \
          echo '"""Bootstrap script for L{$(MODULE)}"""'; \
          echo; \
-         echo '# pylint: disable-msg=C0103'; \
+         echo '# pylint: disable=C0103'; \
          echo '# C0103: Invalid name'; \
          echo; \
          echo 'import sys'; \
@@ -1030,15 +1231,26 @@ $(PYTHON_BOOTSTRAP): Makefile | $(all_dirfiles)
          echo '# Temporarily alias commands until bash completion'; \
          echo '# generator is changed'; \
          echo 'if hasattr(main, "commands"):'; \
-         echo '  commands = main.commands # pylint: disable-msg=E1101'; \
+         echo '  commands = main.commands # pylint: disable=E1101'; \
          echo 'if hasattr(main, "aliases"):'; \
-         echo '  aliases = main.aliases # pylint: disable-msg=E1101'; \
+         echo '  aliases = main.aliases # pylint: disable=E1101'; \
          echo; \
          echo 'if __name__ == "__main__":'; \
          echo '  sys.exit(main.Main())'; \
        } > $@
        chmod u+x $@
 
+$(HS_BUILT_TEST_HELPERS): Makefile
+       @test -n "$(TESTROLE)" || { echo Missing TESTROLE; exit 1; }
+       set -e; \
+       { echo '#!/bin/sh'; \
+         echo '# This file is automatically generated, do not edit!'; \
+         echo "# Edit Makefile.am instead."; \
+         echo; \
+         echo "HTOOLS=$(TESTROLE) exec ./htools/hpc-htools \"\$$@\""; \
+       } > $@
+       chmod u+x $@
+
 # We need to create symlinks because "make distcheck" will not install Python
 # files when building.
 stamp-srclinks: Makefile | $(all_dirfiles)
@@ -1077,10 +1289,13 @@ check-dirs: $(BUILT_SOURCES)
                if test -n "$$error"; then exit 1; else exit 0; fi; \
        }
 
-check-local: check-dirs
+.PHONY: check-local
+check-local: check-dirs $(BUILT_SOURCES)
        $(CHECK_PYTHON_CODE) $(check_python_code)
+       PYTHONPATH=. $(CHECK_HEADER) $(check_python_code)
        $(CHECK_VERSION) $(VERSION) $(top_srcdir)/NEWS
        $(CHECK_NEWS) < $(top_srcdir)/NEWS
+       PYTHONPATH=. $(RUN_IN_TEMPDIR) $(CURDIR)/$(CHECK_IMPORTS) . $(standalone_python_modules)
        expver=$(VERSION_MAJOR).$(VERSION_MINOR); \
        if test "`head -n 1 $(top_srcdir)/README`" != "Ganeti $$expver"; then \
                echo "Incorrect version in README, expected $$expver"; \
@@ -1095,9 +1310,10 @@ check-local: check-dirs
        done
 
 .PHONY: hs-check
-hs-check: htools/test
+hs-check: htools/test htools/hpc-htools $(HS_BUILT_TEST_HELPERS)
        @rm -f test.tix
        ./htools/test
+       HBINARY="./htools/hpc-htools" ./htools/offline-test.sh
 
 # E111: indentation is not a multiple of four
 # E261: at least two spaces before inline comment
@@ -1107,30 +1323,52 @@ PEP8_IGNORE = E111,E261,E501
 # For excluding pep8 expects filenames only, not whole paths
 PEP8_EXCLUDE = $(subst $(space),$(comma),$(strip $(notdir $(BUILT_PYTHON_SOURCES))))
 
+LINT_TARGETS = pylint pylint-qa
+if HAS_PEP8
+LINT_TARGETS += pep8
+endif
+if HAS_HLINT
+LINT_TARGETS += hlint
+endif
+
 .PHONY: lint
-lint: $(BUILT_SOURCES)
+lint: $(LINT_TARGETS)
+
+.PHONY: pylint
+pylint: $(BUILT_SOURCES)
        @test -n "$(PYLINT)" || { echo 'pylint' not found during configure; exit 1; }
-       if test -z "$(PEP8)"; then \
-               echo '"pep8" not found during configure' >&2; \
-       else \
-               $(PEP8) --repeat --ignore='$(PEP8_IGNORE)' --exclude='$(PEP8_EXCLUDE)' \
-                       $(pep8_python_code); \
-       fi
        $(PYLINT) $(LINT_OPTS) $(lint_python_code)
+
+.PHONY: pylint-qa
+pylint-qa: $(BUILT_SOURCES)
+       @test -n "$(PYLINT)" || { echo 'pylint' not found during configure; exit 1; }
        cd $(top_srcdir)/qa && \
          PYTHONPATH=$(abs_top_srcdir) $(PYLINT) $(LINT_OPTS) \
          --rcfile  ../pylintrc $(patsubst qa/%.py,%,$(qa_scripts))
 
+.PHONY: pep8
+pep8: $(BUILT_SOURCES)
+       @test -n "$(PEP8)" || { echo 'pep8' not found during configure; exit 1; }
+       $(PEP8) --ignore='$(PEP8_IGNORE)' --exclude='$(PEP8_EXCLUDE)' \
+               --repeat $(pep8_python_code)
+
 .PHONY: hlint
-hlint: $(HS_BUILT_SRCS)
+hlint: $(HS_BUILT_SRCS) htools/lint-hints.hs
+       @test -n "$(HLINT)" || { echo 'hlint' not found during configure; exit 1; }
        if tty -s; then C="-c"; else C=""; fi; \
-       hlint --report=doc/hs-lint.html $$C htools
+       $(HLINT) --report=doc/hs-lint.html --cross $$C \
+         --ignore "Use first" \
+         --ignore "Use comparing" \
+         --ignore "Use on" \
+         --ignore "Reduce duplication" \
+         --hint htools/lint-hints \
+         $(filter-out htools/Ganeti/THH.hs,$(HS_LIB_SRCS))
 
 # a dist hook rule for updating the vcs-version file; this is
 # hardcoded due to where it needs to build the file...
 dist-hook:
-       $(MAKE) regen-vcs-version && \
-       rm -f $(top_distdir)/vcs-version && \
+       $(MAKE) regen-vcs-version
+       rm -f $(top_distdir)/vcs-version
        cp -p $(srcdir)/vcs-version $(top_distdir)
 
 # a distcheck hook rule for catching revision control directories
@@ -1205,8 +1443,10 @@ hs-apidoc: $(HS_BUILT_SRCS)
            { echo 'haddock' not found during configure; exit 1; }
        rm -rf $(APIDOC_HS_DIR)/*
        @mkdir_p@ $(APIDOC_HS_DIR)/Ganeti/HTools/Program
+       @mkdir_p@ $(APIDOC_HS_DIR)/Ganeti/Confd
        $(HSCOLOUR) -print-css > $(APIDOC_HS_DIR)/Ganeti/hscolour.css
        $(LN_S) ../hscolour.css $(APIDOC_HS_DIR)/Ganeti/HTools/hscolour.css
+       $(LN_S) ../hscolour.css $(APIDOC_HS_DIR)/Ganeti/Confd/hscolour.css
        set -e ; \
        cd htools; \
        if [ "$(HTOOLS_NOCURL)" ]; \
@@ -1253,11 +1493,13 @@ py-coverage: $(BUILT_SOURCES) $(python_tests)
        $(python_tests)
 
 .PHONY: hs-coverage
-hs-coverage: $(haskell_tests)
-       cd htools && rm -f *.tix *.mix && ./test
+hs-coverage: $(haskell_tests) htools/hpc-htools
+       rm -f *.tix
+       $(MAKE) hs-check
        @mkdir_p@ $(COVERAGE_HS_DIR)
-       hpc markup --destdir=$(COVERAGE_HS_DIR) htools/test $(HPCEXCL)
-       hpc report htools/test $(HPCEXCL)
+       hpc combine $(HPCEXCL) test.tix hpc-htools.tix > coverage-htools.tix
+       hpc markup --destdir=$(COVERAGE_HS_DIR) coverage-htools.tix
+       hpc report coverage-htools.tix
        $(LN_S) -f hpc_index.html $(COVERAGE_HS_DIR)/index.html
 
 # Special "kind-of-QA" target for htools, needs special setup (all
@@ -1278,6 +1520,14 @@ live-test: all
 
 commit-check: distcheck lint apidoc
 
+.PHONY: gitignore-check
+gitignore-check:
+       @if [ -n "`git status --short`" ]; then \
+         echo "Git status is not clean!" 1>&2 ; \
+         git status --short; \
+         exit 1; \
+       fi
+
 -include ./Makefile.local
 
 # vim: set noet :
diff --git a/NEWS b/NEWS
index 548c277..1141561 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -2,6 +2,417 @@ News
 ====
 
 
+Version 2.6.0 rc4
+-----------------
+
+*(Released Thu, 19 Jul 2012)*
+
+New features
+~~~~~~~~~~~~
+
+Instance run status
++++++++++++++++++++
+
+The current ``admin_up`` field, which used to denote whether an instance
+should be running or not, has been removed. Instead, ``admin_state`` is
+introduced, with 3 possible values -- ``up``, ``down`` and ``offline``.
+
+The rational behind this is that an instance being “down” can have
+different meanings:
+
+- it could be down during a reboot
+- it could be temporarily be down for a reinstall
+- or it could be down because it is deprecated and kept just for its
+  disk
+
+The previous Boolean state was making it difficult to do capacity
+calculations: should Ganeti reserve memory for a down instance? Now, the
+tri-state field makes it clear:
+
+- in ``up`` and ``down`` state, all resources are reserved for the
+  instance, and it can be at any time brought up if it is down
+- in ``offline`` state, only disk space is reserved for it, but not
+  memory or CPUs
+
+The field can have an extra use: since the transition between ``up`` and
+``down`` and vice-versus is done via ``gnt-instance start/stop``, but
+transition between ``offline`` and ``down`` is done via ``gnt-instance
+modify``, it is possible to given different rights to users. For
+example, owners of an instance could be allowed to start/stop it, but
+not transition it out of the offline state.
+
+Instance policies and specs
++++++++++++++++++++++++++++
+
+In previous Ganeti versions, an instance creation request was not
+limited on the minimum size and on the maximum size just by the cluster
+resources. As such, any policy could be implemented only in third-party
+clients (RAPI clients, or shell wrappers over ``gnt-*``
+tools). Furthermore, calculating cluster capacity via ``hspace`` again
+required external input with regards to instance sizes.
+
+In order to improve these workflows and to allow for example better
+per-node group differentiation, we introduced instance specs, which
+allow declaring:
+
+- minimum instance disk size, disk count, memory size, cpu count
+- maximum values for the above metrics
+- and “standard” values (used in ``hspace`` to calculate the standard
+  sized instances)
+
+The minimum/maximum values can be also customised at node-group level,
+for example allowing more powerful hardware to support bigger instance
+memory sizes.
+
+Beside the instance specs, there are a few other settings belonging to
+the instance policy framework. It is possible now to customise, per
+cluster and node-group:
+
+- the list of allowed disk templates
+- the maximum ratio of VCPUs per PCPUs (to control CPU oversubscription)
+- the maximum ratio of instance to spindles (see below for more
+  information) for local storage
+
+All these together should allow all tools that talk to Ganeti to know
+what are the ranges of allowed values for instances and the
+over-subscription that is allowed.
+
+For the VCPU/PCPU ratio, we already have the VCPU configuration from the
+instance configuration, and the physical CPU configuration from the
+node. For the spindle ratios however, we didn't track before these
+values, so new parameters have been added:
+
+- a new node parameter ``spindle_count``, defaults to 1, customisable at
+  node group or node level
+- at new backend parameter (for instances), ``spindle_use`` defaults to 1
+
+Note that spindles in this context doesn't need to mean actual
+mechanical hard-drives; it's just a relative number for both the node
+I/O capacity and instance I/O consumption.
+
+Instance migration behaviour
+++++++++++++++++++++++++++++
+
+While live-migration is in general desirable over failover, it is
+possible that for some workloads it is actually worse, due to the
+variable time of the “suspend” phase during live migration.
+
+To allow the tools to work consistently over such instances (without
+having to hard-code instance names), a new backend parameter
+``always_failover`` has been added to control the migration/failover
+behaviour. When set to True, all migration requests for an instance will
+instead fall-back to failover.
+
+Instance memory ballooning
+++++++++++++++++++++++++++
+
+Initial support for memory ballooning has been added. The memory for an
+instance is no longer fixed (backend parameter ``memory``), but instead
+can vary between minimum and maximum values (backend parameters
+``minmem`` and ``maxmem``). Currently we only change an instance's
+memory when:
+
+- live migrating or failing over and instance and the target node
+  doesn't have enough memory
+- user requests changing the memory via ``gnt-instance modify
+  --runtime-memory``
+
+Instance CPU pinning
+++++++++++++++++++++
+
+In order to control the use of specific CPUs by instance, support for
+controlling CPU pinning has been added for the Xen, HVM and LXC
+hypervisors. This is controlled by a new hypervisor parameter
+``cpu_mask``; details about possible values for this are in the
+:manpage:`gnt-instance(8)`. Note that use of the most specific (precise
+VCPU-to-CPU mapping) form will work well only when all nodes in your
+cluster have the same amount of CPUs.
+
+Disk parameters
++++++++++++++++
+
+Another area in which Ganeti was not customisable were the parameters
+used for storage configuration, e.g. how many stripes to use for LVM,
+DRBD resync configuration, etc.
+
+To improve this area, we've added disks parameters, which are
+customisable at cluster and node group level, and which allow to
+specify various parameters for disks (DRBD has the most parameters
+currently), for example:
+
+- DRBD resync algorithm and parameters (e.g. speed)
+- the default VG for meta-data volumes for DRBD
+- number of stripes for LVM (plain disk template)
+- the RBD pool
+
+These parameters can be modified via ``gnt-cluster modify -D …`` and
+``gnt-group modify -D …``, and are used at either instance creation (in
+case of LVM stripes, for example) or at disk “activation” time
+(e.g. resync speed).
+
+Rados block device support
+++++++++++++++++++++++++++
+
+A Rados (http://ceph.com/wiki/Rbd) storage backend has been added,
+denoted by the ``rbd`` disk template type. This is considered
+experimental, feedback is welcome. For details on configuring it, see
+the :doc:`install` document and the :manpage:`gnt-cluster(8)` man page.
+
+Master IP setup
++++++++++++++++
+
+The existing master IP functionality works well only in simple setups (a
+single network shared by all nodes); however, if nodes belong to
+different networks, then the ``/32`` setup and lack of routing
+information is not enough.
+
+To allow the master IP to function well in more complex cases, the
+system was reworked as follows:
+
+- a master IP netmask setting has been added
+- the master IP activation/turn-down code was moved from the node daemon
+  to a separate script
+- whether to run the Ganeti-supplied master IP script or a user-supplied
+  on is a ``gnt-cluster init`` setting
+
+Details about the location of the standard and custom setup scripts are
+in the man page :manpage:`gnt-cluster(8)`; for information about the
+setup script protocol, look at the Ganeti-supplied script.
+
+SPICE support
++++++++++++++
+
+The `SPICE <http://www.linux-kvm.org/page/SPICE>`_ support has been
+improved.
+
+It is now possible to use TLS-protected connections, and when renewing
+or changing the cluster certificates (via ``gnt-cluster renew-crypto``,
+it is now possible to specify spice or spice CA certificates. Also, it
+is possible to configure a password for SPICE sessions via the
+hypervisor parameter ``spice_password_file``.
+
+There are also new parameters to control the compression and streaming
+options (e.g. ``spice_image_compression``, ``spice_streaming_video``,
+etc.). For details, see the man page :manpage:`gnt-instance(8)` and look
+for the spice parameters.
+
+Lastly, it is now possible to see the SPICE connection information via
+``gnt-instance console``.
+
+OVF converter
++++++++++++++
+
+A new tool (``tools/ovfconverter``) has been added that supports
+conversion between Ganeti and the `Open Virtualization Format
+<http://en.wikipedia.org/wiki/Open_Virtualization_Format>`_ (both to and
+from).
+
+This relies on the ``qemu-img`` tool to convert the disk formats, so the
+actual compatibility with other virtualization solutions depends on it.
+
+Confd daemon changes
+++++++++++++++++++++
+
+The configuration query daemon (``ganeti-confd``) is now optional, and
+has been rewritten in Haskell; whether to use the daemon at all, use the
+Python (default) or the Haskell version is selectable at configure time
+via the ``--enable-confd`` parameter, which can take one of the
+``haskell``, ``python`` or ``no`` values. If not used, disabling the
+daemon will result in a smaller footprint; for larger systems, we
+welcome feedback on the Haskell version which might become the default
+in future versions.
+
+If you want to use ``gnt-node list-drbd`` you need to have the Haskell
+daemon running. The Python version doesn't implement the new call.
+
+
+User interface changes
+~~~~~~~~~~~~~~~~~~~~~~
+
+We have replaced the ``--disks`` option of ``gnt-instance
+replace-disks`` with a more flexible ``--disk`` option, which allows
+adding and removing disks at arbitrary indices (Issue 188). Furthermore,
+disk size and mode can be changed upon recreation (via ``gnt-instance
+recreate-disks``, which accepts the same ``--disk`` option).
+
+As many people are used to a ``show`` command, we have added that as an
+alias to ``info`` on all ``gnt-*`` commands.
+
+The ``gnt-instance grow-disk`` command has a new mode in which it can
+accept the target size of the disk, instead of the delta; this can be
+more safe since two runs in absolute mode will be idempotent, and
+sometimes it's also easier to specify the desired size directly.
+
+Also the handling of instances with regard to offline secondaries has
+been improved. Instance operations should not fail because one of it's
+secondary nodes is offline, even though it's safe to proceed.
+
+A new command ``list-drbd`` has been added to the ``gnt-node`` script to
+support debugging of DRBD issues on nodes. It provides a mapping of DRBD
+minors to instance name.
+
+API changes
+~~~~~~~~~~~
+
+RAPI coverage has improved, with (for example) new resources for
+recreate-disks, node power-cycle, etc.
+
+Compatibility
+~~~~~~~~~~~~~
+
+There is partial support for ``xl`` in the Xen hypervisor; feedback is
+welcome.
+
+Python 2.7 is better supported, and after Ganeti 2.6 we will investigate
+whether to still support Python 2.4 or move to Python 2.6 as minimum
+required version.
+
+Support for Fedora has been slightly improved; the provided example
+init.d script should work better on it and the INSTALL file should
+document the needed dependencies.
+
+Internal changes
+~~~~~~~~~~~~~~~~
+
+The deprecated ``QueryLocks`` LUXI request has been removed. Use
+``Query(what=QR_LOCK, ...)`` instead.
+
+The LUXI requests :pyeval:`luxi.REQ_QUERY_JOBS`,
+:pyeval:`luxi.REQ_QUERY_INSTANCES`, :pyeval:`luxi.REQ_QUERY_NODES`,
+:pyeval:`luxi.REQ_QUERY_GROUPS`, :pyeval:`luxi.REQ_QUERY_EXPORTS` and
+:pyeval:`luxi.REQ_QUERY_TAGS` are deprecated and will be removed in a
+future version. :pyeval:`luxi.REQ_QUERY` should be used instead.
+
+RAPI client: ``CertificateError`` now derives from
+``GanetiApiError``. This should make it more easy to handle Ganeti
+errors.
+
+Deprecation warnings due to PyCrypto/paramiko import in
+``tools/setup-ssh`` have been silenced, as usually they are safe; please
+make sure to run an up-to-date paramiko version, if you use this tool.
+
+The QA scripts now depend on Python 2.5 or above (the main code base
+still works with Python 2.4).
+
+The configuration file (``config.data``) is now written without
+indentation for performance reasons; if you want to edit it, it can be
+re-formatted via ``tools/fmtjson``.
+
+A number of bugs has been fixed in the cluster merge tool.
+
+``x509`` certification verification (used in import-export) has been
+changed to allow the same clock skew as permitted by the cluster
+verification. This will remove some rare but hard to diagnose errors in
+import-export.
+
+The ``LUXI`` protocol has been made more consistent regarding its
+handling of command arguments. This, however, leads to incompatibility
+issues with previous versions. Please ensure that you restart Ganeti
+daemons after the upgrade, otherwise job submission will fail.
+
+
+Version 2.6.0 rc3
+-----------------
+
+*(Released Fri, 13 Jul 2012)*
+
+Third release candidate for 2.6. The following changes were done from
+rc3 to rc4:
+
+- Fixed ``UpgradeConfig`` w.r.t. to disk parameters on disk objects.
+- Fixed an inconsistency in the LUXI protocol with the provided
+  arguments (NOT backwards compatible)
+- Fixed a bug with node groups ipolicy where ``min`` was greater than
+  the cluster ``std`` value
+- Implemented a new ``gnt-node list-drbd`` call to list DRBD minors for
+  easier instance debugging on nodes (requires ``hconfd`` to work)
+
+
+Version 2.6.0 rc2
+-----------------
+
+*(Released Tue, 03 Jul 2012)*
+
+Second release candidate for 2.6. The following changes were done from
+rc2 to rc3:
+
+- Fixed ``gnt-cluster verify`` regarding ``master-ip-script`` on non
+  master candidates
+- Fixed a RAPI regression on missing beparams/memory
+- Fixed redistribution of files on offline nodes
+- Added possibility to run activate-disks even though secondaries are
+  offline. With this change it relaxes also the strictness on some other
+  commands which use activate disks internally:
+  * ``gnt-instance start|reboot|rename|backup|export``
+- Made it possible to remove safely an instance if its secondaries are
+  offline
+- Made it possible to reinstall even though secondaries are offline
+
+
+Version 2.6.0 rc1
+-----------------
+
+*(Released Mon, 25 Jun 2012)*
+
+First release candidate for 2.6. The following changes were done from
+rc1 to rc2:
+
+- Fixed bugs with disk parameters and ``rbd`` templates as well as
+  ``instance_os_add``
+- Made ``gnt-instance modify`` more consistent regarding new NIC/Disk
+  behaviour. It supports now the modify operation
+- ``hcheck`` implemented to analyze cluster health and possibility of
+  improving health by rebalance
+- ``hbal`` has been improved in dealing with split instances
+
+
+Version 2.6.0 beta2
+-------------------
+
+*(Released Mon, 11 Jun 2012)*
+
+Second beta release of 2.6. The following changes were done from beta2
+to rc1:
+
+- Fixed ``daemon-util`` with non-root user models
+- Fixed creation of plain instances with ``--no-wait-for-sync``
+- Fix wrong iv_names when running ``cfgupgrade``
+- Export more information in RAPI group queries
+- Fixed bug when changing instance network interfaces
+- Extended burnin to do NIC changes
+- query: Added ``<``, ``>``, ``<=``, ``>=`` comparison operators
+- Changed default for DRBD barriers
+- Fixed DRBD error reporting for syncer rate
+- Verify the options on disk parameters
+
+And of course various fixes to documentation and improved unittests and
+QA.
+
+
+Version 2.6.0 beta1
+-------------------
+
+*(Released Wed, 23 May 2012)*
+
+First beta release of 2.6. The following changes were done from beta1 to
+beta2:
+
+- integrated patch for distributions without ``start-stop-daemon``
+- adapted example init.d script to work on Fedora
+- fixed log handling in Haskell daemons
+- adapted checks in the watcher for pycurl linked against libnss
+- add partial support for ``xl`` instead of ``xm`` for Xen
+- fixed a type issue in cluster verification
+- fixed ssconf handling in the Haskell code (was breaking confd in IPv6
+  clusters)
+
+Plus integrated fixes from the 2.5 branch:
+
+- fixed ``kvm-ifup`` to use ``/bin/bash``
+- fixed parallel build failures
+- KVM live migration when using a custom keymap
+
+
 Version 2.5.2
 -------------
 
@@ -411,6 +822,7 @@ Many bug-fixes and a few new small features:
 And as usual, various improvements to the error messages, documentation
 and man pages.
 
+
 Version 2.4.1
 -------------
 
@@ -921,8 +1333,8 @@ Internal changes:
   server endpoint
 
 
-Version 2.2.0 beta 0
---------------------
+Version 2.2.0 beta0
+-------------------
 
 *(Released Thu, 17 Jun 2010)*
 
@@ -1578,16 +1990,16 @@ Version 2.0.1
   error handling path called a wrong function name)
 
 
-Version 2.0.0 final
--------------------
+Version 2.0.0
+-------------
 
 *(Released Wed, 27 May 2009)*
 
 - no changes from rc5
 
 
-Version 2.0 release candidate 5
--------------------------------
+Version 2.0 rc5
+---------------
 
 *(Released Wed, 20 May 2009)*
 
@@ -1597,8 +2009,8 @@ Version 2.0 release candidate 5
 - make watcher automatically start the master daemon if down
 
 
-Version 2.0 release candidate 4
--------------------------------
+Version 2.0 rc4
+---------------
 
 *(Released Mon, 27 Apr 2009)*
 
@@ -1612,8 +2024,8 @@ Version 2.0 release candidate 4
 - miscellaneous doc and man pages fixes
 
 
-Version 2.0 release candidate 3
--------------------------------
+Version 2.0 rc3
+---------------
 
 *(Released Wed, 8 Apr 2009)*
 
@@ -1626,8 +2038,8 @@ Version 2.0 release candidate 3
   toolchains
 
 
-Version 2.0 release candidate 2
--------------------------------
+Version 2.0 rc2
+---------------
 
 *(Released Fri, 27 Mar 2009)*
 
@@ -1639,8 +2051,8 @@ Version 2.0 release candidate 2
 - Some documentation fixes and updates
 
 
-Version 2.0 release candidate 1
--------------------------------
+Version 2.0 rc1
+---------------
 
 *(Released Mon, 2 Mar 2009)*
 
@@ -1653,8 +2065,8 @@ Version 2.0 release candidate 1
 - Fix an issue related to $libdir/run/ganeti and cluster creation
 
 
-Version 2.0 beta 2
-------------------
+Version 2.0 beta2
+-----------------
 
 *(Released Thu, 19 Feb 2009)*
 
@@ -1671,8 +2083,8 @@ Version 2.0 beta 2
 - Many other bugfixes and small improvements
 
 
-Version 2.0 beta 1
-------------------
+Version 2.0 beta1
+-----------------
 
 *(Released Mon, 26 Jan 2009)*
 
@@ -1885,8 +2297,8 @@ Version 1.2.0
 - Change parsing of lvm commands to ignore stderr
 
 
-Version 1.2b3
--------------
+Version 1.2 beta3
+-----------------
 
 *(Released Wed, 28 Nov 2007)*
 
@@ -1897,8 +2309,8 @@ Version 1.2b3
 - QA updates
 
 
-Version 1.2b2
--------------
+Version 1.2 beta2
+-----------------
 
 *(Released Tue, 13 Nov 2007)*
 
diff --git a/README b/README
index c89d5bb..67d5d48 100644 (file)
--- a/README
+++ b/README
@@ -1,4 +1,4 @@
-Ganeti 2.5
+Ganeti 2.6
 ==========
 
 For installation instructions, read the INSTALL and the doc/install.rst
index ec9c0a6..365dad9 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 #
 
-# Copyright (C) 2009 Google Inc.
+# Copyright (C) 2009, 2010, 2011, 2012 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
@@ -28,6 +28,7 @@
 
 import os
 import re
+import itertools
 from cStringIO import StringIO
 
 from ganeti import constants
@@ -39,6 +40,10 @@ from ganeti import build
 # making an exception here because this script is only used at build time.
 from ganeti import _autoconf
 
+#: Regular expression describing desired format of option names. Long names can
+#: contain lowercase characters, numbers and dashes only.
+_OPT_NAME_RE = re.compile(r"^-[a-zA-Z0-9]|--[a-z][-a-z0-9]+$")
+
 
 def WritePreamble(sw):
   """Writes the script preamble.
@@ -49,7 +54,7 @@ def WritePreamble(sw):
   sw.Write("# This script is automatically generated at build time.")
   sw.Write("# Do not modify manually.")
 
-  sw.Write("_ganeti_dbglog() {")
+  sw.Write("_gnt_log() {")
   sw.IncIndent()
   try:
     sw.Write("if [[ -n \"$GANETI_COMPL_LOG\" ]]; then")
@@ -187,7 +192,7 @@ def WritePreamble(sw):
 
   # Params: <long options with equal sign> <all options>
   # Result variable: $optcur
-  sw.Write("_ganeti_checkopt() {")
+  sw.Write("_gnt_checkopt() {")
   sw.IncIndent()
   try:
     sw.Write("""if [[ -n "$1" && "$cur" == @($1) ]]; then""")
@@ -206,7 +211,7 @@ def WritePreamble(sw):
       sw.DecIndent()
     sw.Write("fi")
 
-    sw.Write("_ganeti_dbglog optcur=\"'$optcur'\"")
+    sw.Write("_gnt_log optcur=\"'$optcur'\"")
 
     sw.Write("return 1")
   finally:
@@ -215,18 +220,18 @@ def WritePreamble(sw):
 
   # Params: <compgen options>
   # Result variable: $COMPREPLY
-  sw.Write("_ganeti_compgen() {")
+  sw.Write("_gnt_compgen() {")
   sw.IncIndent()
   try:
     sw.Write("""COMPREPLY=( $(compgen "$@") )""")
-    sw.Write("_ganeti_dbglog COMPREPLY=\"${COMPREPLY[@]}\"")
+    sw.Write("_gnt_log COMPREPLY=\"${COMPREPLY[@]}\"")
   finally:
     sw.DecIndent()
   sw.Write("}")
 
 
 def WriteCompReply(sw, args, cur="\"$cur\""):
-  sw.Write("_ganeti_compgen %s -- %s", args, cur)
+  sw.Write("_gnt_compgen %s -- %s", args, cur)
   sw.Write("return")
 
 
@@ -244,6 +249,11 @@ class CompletionWriter:
       # pylint. pylint: disable=W0212
       opt.all_names = sorted(opt._short_opts + opt._long_opts)
 
+      invalid = list(itertools.ifilterfalse(_OPT_NAME_RE.match, opt.all_names))
+      if invalid:
+        raise Exception("Option names don't match regular expression '%s': %s" %
+                        (_OPT_NAME_RE.pattern, utils.CommaJoin(invalid)))
+
   def _FindFirstArgument(self, sw):
     ignore = []
     skip_one = []
@@ -309,7 +319,7 @@ class CompletionWriter:
 
     wrote_opt = False
 
-    for (suggest, allnames) in values.iteritems():
+    for (suggest, allnames) in values.items():
       longnames = [i for i in allnames if i.startswith("--")]
 
       if wrote_opt:
@@ -317,7 +327,7 @@ class CompletionWriter:
       else:
         condcmd = "if"
 
-      sw.Write("%s _ganeti_checkopt %s %s; then", condcmd,
+      sw.Write("%s _gnt_checkopt %s %s; then", condcmd,
                utils.ShellQuote("|".join(["%s=*" % i for i in longnames])),
                utils.ShellQuote("|".join(allnames)))
       sw.IncIndent()
@@ -354,7 +364,7 @@ class CompletionWriter:
             sw.DecIndent()
           sw.Write("fi")
 
-          sw.Write("_ganeti_dbglog pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
+          sw.Write("_gnt_log pfx=\"'$pfx'\" curvalue=\"'$curvalue'\""
                    " node1=\"'$node1'\"")
 
           sw.Write("for i in $(_ganeti_nodes); do")
@@ -409,10 +419,6 @@ class CompletionWriter:
       varlen_arg_idx = None
       wrote_arg = False
 
-      # Write some debug comments
-      for idx, arg in enumerate(self.args):
-        sw.Write("# %s: %r", idx, arg)
-
       sw.Write("compgenargs=")
 
       for idx, arg in enumerate(self.args):
@@ -521,9 +527,9 @@ def WriteCompletion(sw, scriptname, funcname,
              ' prev="${COMP_WORDS[COMP_CWORD-1]}"'
              ' i first_arg_idx choices compgenargs arg_idx optcur')
 
-    sw.Write("_ganeti_dbglog cur=\"$cur\" prev=\"$prev\"")
+    sw.Write("_gnt_log cur=\"$cur\" prev=\"$prev\"")
     sw.Write("[[ -n \"$GANETI_COMPL_LOG\" ]] &&"
-             " _ganeti_dbglog \"$(set | grep ^COMP_)\"")
+             " _gnt_log \"$(set | grep ^COMP_)\"")
 
     sw.Write("COMPREPLY=()")
 
@@ -543,20 +549,25 @@ def WriteCompletion(sw, scriptname, funcname,
         sw.DecIndent()
       sw.Write("fi")
 
-      # We're doing options and arguments to commands
-      sw.Write("""case "${COMP_WORDS[1]}" in""")
-      for cmd, (_, argdef, optdef, _, _) in commands.iteritems():
+      # Group commands by arguments and options
+      grouped_cmds = {}
+      for cmd, (_, argdef, optdef, _, _) in commands.items():
         if not (argdef or optdef):
           continue
+        grouped_cmds.setdefault((tuple(argdef), tuple(optdef)), set()).add(cmd)
 
-        # TODO: Group by arguments and options
-        sw.Write("%s)", utils.ShellQuote(cmd))
+      # We're doing options and arguments to commands
+      sw.Write("""case "${COMP_WORDS[1]}" in""")
+      sort_grouped = sorted(grouped_cmds.items(),
+                            key=lambda (_, y): sorted(y)[0])
+      for ((argdef, optdef), cmds) in sort_grouped:
+        assert argdef or optdef
+        sw.Write("%s)", "|".join(map(utils.ShellQuote, sorted(cmds))))
         sw.IncIndent()
         try:
           CompletionWriter(1, optdef, argdef).WriteTo(sw)
         finally:
           sw.DecIndent()
-
         sw.Write(";;")
       sw.Write("esac")
   finally:
@@ -601,7 +612,7 @@ def GetCommands(filename, module):
   aliases = getattr(module, "aliases", {})
   if aliases:
     commands = commands.copy()
-    for name, target in aliases.iteritems():
+    for name, target in aliases.items():
       commands[name] = commands[target]
 
   return commands
diff --git a/autotools/build-rpc b/autotools/build-rpc
new file mode 100755 (executable)
index 0000000..a862d94
--- /dev/null
@@ -0,0 +1,211 @@
+#!/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 to generate RPC code.
+
+"""
+
+# pylint: disable=C0103
+# [C0103] Invalid name
+
+import sys
+import re
+import itertools
+import textwrap
+from cStringIO import StringIO
+
+from ganeti import utils
+from ganeti import compat
+from ganeti import build
+
+
+_SINGLE = "single-node"
+_MULTI = "multi-node"
+
+#: Expected length of a rpc definition
+_RPC_DEF_LEN = 8
+
+
+def _WritePreamble(sw):
+  """Writes a preamble for the RPC wrapper output.
+
+  """
+  sw.Write("# This code is automatically generated at build time.")
+  sw.Write("# Do not modify manually.")
+  sw.Write("")
+  sw.Write("\"\"\"Automatically generated RPC client wrappers.")
+  sw.Write("")
+  sw.Write("\"\"\"")
+  sw.Write("")
+  sw.Write("from ganeti import rpc_defs")
+  sw.Write("")
+
+
+def _WrapCode(line):
+  """Wraps Python code.
+
+  """
+  return textwrap.wrap(line, width=70, expand_tabs=False,
+                       fix_sentence_endings=False, break_long_words=False,
+                       replace_whitespace=True,
+                       subsequent_indent=utils.ShellWriter.INDENT_STR)
+
+
+def _WriteDocstring(sw, name, timeout, kind, args, desc):
+  """Writes a docstring for an RPC wrapper.
+
+  """
+  sw.Write("\"\"\"Wrapper for RPC call '%s'", name)
+  sw.Write("")
+  if desc:
+    sw.Write(desc)
+    sw.Write("")
+
+  note = ["This is a %s call" % kind]
+  if timeout and not callable(timeout):
+    note.append(" with a timeout of %s" % utils.FormatSeconds(timeout))
+  sw.Write("@note: %s", "".join(note))
+
+  if kind == _SINGLE:
+    sw.Write("@type node: string")
+    sw.Write("@param node: Node name")
+  else:
+    sw.Write("@type node_list: list of string")
+    sw.Write("@param node_list: List of node names")
+
+  if args:
+    for (argname, _, argtext) in args:
+      if argtext:
+        docline = "@param %s: %s" % (argname, argtext)
+        for line in _WrapCode(docline):
+          sw.Write(line)
+  sw.Write("")
+  sw.Write("\"\"\"")
+
+
+def _WriteBaseClass(sw, clsname, calls):
+  """Write RPC wrapper class.
+
+  """
+  sw.Write("")
+  sw.Write("class %s(object):", clsname)
+  sw.IncIndent()
+  try:
+    sw.Write("# E1101: Non-existent members")
+    sw.Write("# R0904: Too many public methods")
+    sw.Write("# pylint: disable=E1101,R0904")
+
+    if not calls:
+      sw.Write("pass")
+      return
+
+    sw.Write("_CALLS = rpc_defs.CALLS[%r]", clsname)
+    sw.Write("")
+
+    for v in calls:
+      if len(v) != _RPC_DEF_LEN:
+        raise ValueError("Procedure %s has only %d elements, expected %d" %
+                         (v[0], len(v), _RPC_DEF_LEN))
+
+    for (name, kind, _, timeout, args, _, _, desc) in calls:
+      funcargs = ["self"]
+
+      if kind == _SINGLE:
+        funcargs.append("node")
+      elif kind == _MULTI:
+        funcargs.append("node_list")
+      else:
+        raise Exception("Unknown kind '%s'" % kind)
+
+      funcargs.extend(map(compat.fst, args))
+
+      funcargs.append("_def=_CALLS[%r]" % name)
+
+      funcdef = "def call_%s(%s):" % (name, utils.CommaJoin(funcargs))
+      for line in _WrapCode(funcdef):
+        sw.Write(line)
+
+      sw.IncIndent()
+      try:
+        _WriteDocstring(sw, name, timeout, kind, args, desc)
+
+        buf = StringIO()
+        buf.write("return ")
+
+        # In case line gets too long and is wrapped in a bad spot
+        buf.write("( ")
+
+        buf.write("self._Call(_def, ")
+        if kind == _SINGLE:
+          buf.write("[node]")
+        else:
+          buf.write("node_list")
+
+        buf.write(", [%s])" %
+                  # Function arguments
+                  utils.CommaJoin(map(compat.fst, args)))
+
+        if kind == _SINGLE:
+          buf.write("[node]")
+        buf.write(")")
+
+        for line in _WrapCode(buf.getvalue()):
+          sw.Write(line)
+      finally:
+        sw.DecIndent()
+      sw.Write("")
+  finally:
+    sw.DecIndent()
+
+
+def main():
+  """Main function.
+
+  """
+  buf = StringIO()
+  sw = utils.ShellWriter(buf)
+
+  _WritePreamble(sw)
+
+  for filename in sys.argv[1:]:
+    sw.Write("# Definitions from '%s'", filename)
+
+    module = build.LoadModule(filename)
+
+    # Call types are re-defined in definitions file to avoid imports. Verify
+    # here to ensure they're equal to local constants.
+    assert module.SINGLE == _SINGLE
+    assert module.MULTI == _MULTI
+
+    dups = utils.FindDuplicates(itertools.chain(*map(lambda value: value.keys(),
+                                                     module.CALLS.values())))
+    if dups:
+      raise Exception("Found duplicate RPC definitions for '%s'" %
+                      utils.CommaJoin(sorted(dups)))
+
+    for (clsname, calls) in module.CALLS.items():
+      _WriteBaseClass(sw, clsname, calls.values())
+
+  print buf.getvalue()
+
+
+if __name__ == "__main__":
+  main()
diff --git a/autotools/check-header b/autotools/check-header
new file mode 100755 (executable)
index 0000000..7529fe6
--- /dev/null
@@ -0,0 +1,139 @@
+#!/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 to verify file header.
+
+"""
+
+# pylint: disable=C0103
+# [C0103] Invalid name
+
+import sys
+import re
+import itertools
+
+from ganeti import constants
+from ganeti import utils
+from ganeti import compat
+
+
+#: Assume header is always in the first 8kB of a file
+_READ_SIZE = 8 * 1024
+
+_GPLv2 = [
+  "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.",
+  ]
+
+
+_SHEBANG = re.compile(r"^#(?:|!(?:/usr/bin/python(?:| -u)|/bin/(?:|ba)sh))$")
+_COPYRIGHT_YEAR = r"20[01][0-9]"
+_COPYRIGHT = re.compile(r"# Copyright \(C\) (%s(?:, %s)*) Google Inc\.$" %
+                        (_COPYRIGHT_YEAR, _COPYRIGHT_YEAR))
+_COPYRIGHT_DESC = "Copyright (C) <year>[, <year> ...] Google Inc."
+_AUTOGEN = "# This file is automatically generated, do not edit!"
+
+
+class HeaderError(Exception):
+  pass
+
+
+def _Fail(lineno, msg):
+  raise HeaderError("Line %s: %s" % (lineno, msg))
+
+
+def _CheckHeader(getline_fn):
+  (lineno, line) = getline_fn()
+
+  if line == _AUTOGEN:
+    return
+
+  if not _SHEBANG.match(line):
+    _Fail(lineno, ("Must contain nothing but a hash character (#) or a"
+                   " shebang line (e.g. #!/bin/bash)"))
+
+  (lineno, line) = getline_fn()
+
+  if line == _AUTOGEN:
+    return
+
+  if line != "#":
+    _Fail(lineno, "Must contain nothing but hash character (#)")
+
+  (lineno, line) = getline_fn()
+  if line:
+    _Fail(lineno, "Must be empty")
+
+  (lineno, line) = getline_fn()
+  if not _COPYRIGHT.match(line):
+    _Fail(lineno, "Must contain copyright information (%s)" % _COPYRIGHT_DESC)
+
+  (lineno, line) = getline_fn()
+  if line != "#":
+    _Fail(lineno, "Must contain nothing but hash character (#)")
+
+  for licence_line in _GPLv2:
+    (lineno, line) = getline_fn()
+    if line != ("# %s" % licence_line).rstrip():
+      _Fail(lineno, "Does not match expected licence line (%s)" % licence_line)
+
+  (lineno, line) = getline_fn()
+  if line:
+    _Fail(lineno, "Must be empty")
+
+
+def Main():
+  """Main program.
+
+  """
+  fail = False
+
+  for filename in sys.argv[1:]:
+    content = utils.ReadFile(filename, size=_READ_SIZE)
+    lines = zip(itertools.count(1), content.splitlines())
+
+    try:
+      _CheckHeader(compat.partial(lines.pop, 0))
+    except HeaderError, err:
+      report = str(err)
+      print "%s: %s" % (filename, report)
+      fail = True
+
+  if fail:
+    sys.exit(constants.EXIT_FAILURE)
+  else:
+    sys.exit(constants.EXIT_SUCCESS)
+
+
+if __name__ == "__main__":
+  Main()
diff --git a/autotools/check-imports b/autotools/check-imports
new file mode 100755 (executable)
index 0000000..d50cb31
--- /dev/null
@@ -0,0 +1,92 @@
+#!/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 to check module imports.
+
+"""
+
+# pylint: disable=C0103
+# C0103: Invalid name
+
+import sys
+
+# All modules imported after this line are removed from the global list before
+# importing a module to be checked
+_STANDARD_MODULES = sys.modules.keys()
+
+import os.path
+
+from ganeti import build
+
+
+def main():
+  args = sys.argv[1:]
+
+  # Get references to functions used later on
+  load_module = build.LoadModule
+  abspath = os.path.abspath
+  commonprefix = os.path.commonprefix
+  normpath = os.path.normpath
+
+  script_path = abspath(__file__)
+  srcdir = normpath(abspath(args.pop(0)))
+
+  assert "ganeti" in sys.modules
+
+  for filename in args:
+    # Reset global state
+    for name in sys.modules.keys():
+      if name not in _STANDARD_MODULES:
+        sys.modules.pop(name, None)
+
+    assert "ganeti" not in sys.modules
+
+    # Load module (this might import other modules)
+    module = load_module(filename)
+
+    result = []
+
+    for (name, checkmod) in sorted(sys.modules.items()):
+      if checkmod is None or checkmod == module:
+        continue
+
+      try:
+        checkmodpath = getattr(checkmod, "__file__")
+      except AttributeError:
+        # Built-in module
+        pass
+      else:
+        abscheckmodpath = os.path.abspath(checkmodpath)
+
+        if abscheckmodpath == script_path:
+          # Ignore check script
+          continue
+
+        if commonprefix([abscheckmodpath, srcdir]) == srcdir:
+          result.append(name)
+
+    if result:
+      raise Exception("Module '%s' has illegal imports: %s" %
+                      (filename, ", ".join(result)))
+
+
+if __name__ == "__main__":
+  main()
diff --git a/autotools/check-man-dashes b/autotools/check-man-dashes
new file mode 100755 (executable)
index 0000000..3ddd3ba
--- /dev/null
@@ -0,0 +1,24 @@
+#!/bin/bash
+#
+
+# Copyright (C) 2012 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.
+
+set -e
+
+! grep -F -q '\[em]' "$1" || \
+  { echo "Unescaped dashes found in $1, use \\-- instead of --" 1>&2; exit 1; }
similarity index 95%
rename from autotools/check-man
rename to autotools/check-man-warnings
index f52415d..8c8235e 100755 (executable)
@@ -1,7 +1,7 @@
 #!/bin/bash
 #
 
-# Copyright (C) 2010 Google Inc.
+# Copyright (C) 2010, 2012 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
index 51a0403..9a22995 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 #
 
-# Copyright (C) 2011 Google Inc.
+# Copyright (C) 2011, 2012 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
@@ -23,6 +23,9 @@
 
 """
 
+# pylint: disable=C0103
+# [C0103] Invalid name
+
 import sys
 import time
 import datetime
@@ -35,23 +38,63 @@ DASHES_RE = re.compile(r"^\s*-+\s*$")
 RELEASED_RE = re.compile(r"^\*\(Released (?P<day>[A-Z][a-z]{2}),"
                          r" (?P<date>.+)\)\*$")
 UNRELEASED_RE = re.compile(r"^\*\(unreleased\)\*$")
+VERSION_RE = re.compile(r"^Version \d+(\.\d+)+( (beta|rc)\d+)?$")
+
+errors = []
+
+
+def Error(msg):
+  """Log an error for later display.
+
+  """
+  errors.append(msg)
+
+
+def ReqNLines(req, count_empty, lineno, line):
+  """Check if we have N empty lines.
+
+  """
+  if count_empty < req:
+    Error("Line %s: Missing empty line(s) before %s,"
+          " %d needed but got only %d" %
+          (lineno, line, req, count_empty))
+  if count_empty > req:
+    Error("Line %s: Too many empty lines before %s,"
+          " %d needed but got %d" %
+          (lineno, line, req, count_empty))
 
 
 def main():
   # Ensure "C" locale is used
   curlocale = locale.getlocale()
   if curlocale != (None, None):
-    raise Exception("Invalid locale %s" % curlocale)
+    Error("Invalid locale %s" % curlocale)
 
   prevline = None
   expect_date = False
+  count_empty = 0
 
   for line in fileinput.input():
     line = line.rstrip("\n")
 
+    if VERSION_RE.match(line):
+      ReqNLines(2, count_empty, fileinput.filelineno(), line)
+
+    if UNRELEASED_RE.match(line) or RELEASED_RE.match(line):
+      ReqNLines(1, count_empty, fileinput.filelineno(), line)
+
+    if line:
+      count_empty = 0
+    else:
+      count_empty += 1
+
     if DASHES_RE.match(line):
-      if not prevline.startswith("Version "):
-        raise Exception("Line %s: Invalid title" % (fileinput.filelineno() - 1))
+      if not VERSION_RE.match(prevline):
+        Error("Line %s: Invalid title" %
+              (fileinput.filelineno() - 1))
+      if len(line) != len(prevline):
+        Error("Line %s: Invalid dashes length" %
+              (fileinput.filelineno()))
       expect_date = True
 
     elif expect_date:
@@ -66,8 +109,7 @@ def main():
 
       m = RELEASED_RE.match(line)
       if not m:
-        raise Exception("Line %s: Invalid release line" %
-                        fileinput.filelineno())
+        Error("Line %s: Invalid release line" % fileinput.filelineno())
 
       # Including the weekday in the date string does not work as time.strptime
       # would return an inconsistent result if the weekday is incorrect.
@@ -77,15 +119,20 @@ def main():
 
       # Check weekday
       if m.group("day") != weekday:
-        raise Exception("Line %s: %s was/is a %s, not %s" %
-                        (fileinput.filelineno(), parsed, weekday,
-                         m.group("day")))
+        Error("Line %s: %s was/is a %s, not %s" %
+              (fileinput.filelineno(), parsed, weekday,
+               m.group("day")))
 
       expect_date = False
 
     prevline = line
 
-  sys.exit(0)
+  if errors:
+    for msg in errors:
+      print >> sys.stderr, msg
+    sys.exit(1)
+  else:
+    sys.exit(0)
 
 
 if __name__ == "__main__":
index 166e12d..051ef71 100755 (executable)
@@ -1,7 +1,7 @@
 #!/bin/bash
 #
 
-# Copyright (C) 2009 Google Inc.
+# Copyright (C) 2009, 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
@@ -20,6 +20,9 @@
 
 set -e
 
+# Ensure the checks always use the same locale
+export LC_ALL=C
+
 readonly maxlinelen=$(for ((i=0; i<81; ++i)); do echo -n .; done)
 
 if [[ "${#maxlinelen}" != 81 ]]; then
@@ -58,6 +61,13 @@ for script; do
     let ++problems
     echo "Longest line in $script is longer than 80 characters" >&2
   fi
+
+  if grep -n -H -E -i \
+    '#.*\bpylint[[:space:]]*:[[:space:]]*disable-msg\b' "$script"
+  then
+    let ++problems
+    echo "Found old-style pylint disable pragma in $script" >&2
+  fi
 done
 
 if [[ "$problems" -gt 0 ]]; then
index 3f2b362..dbf0b13 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 #
 
-# Copyright (C) 2011 Google Inc.
+# Copyright (C) 2011, 2012 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
 """
 
 import re
+import types
 
+from ganeti import compat
 from ganeti import constants
+from ganeti import luxi
 
-CONSTANT_RE = re.compile("^[A-Z][A-Z0-9_]+$")
+#: Constant name regex
+CONSTANT_RE = re.compile("^[A-Z][A-Z0-9_-]+$")
+
+#: Private name regex
+PRIVATE_RE = re.compile("^__.+__$")
+
+#: The type of regex objects
+RE_TYPE = type(CONSTANT_RE)
 
 
 def NameRules(name):
   """Converts the upper-cased Python name to Haskell camelCase.
 
   """
+  name = name.replace("-", "_")
   elems = name.split("_")
   return elems[0].lower() + "".join(e.capitalize() for e in elems[1:])
 
@@ -46,44 +57,176 @@ def StringValueRules(value):
   return value
 
 
-def Convert():
+def DictKeyName(dict_name, key_name):
+  """Converts a dict plus key name to a full name.
+
+  """
+  return"%s_%s" % (dict_name, str(key_name).upper())
+
+
+def HaskellTypeVal(value):
+  """Returns the Haskell type and value for a Python value.
+
+  Note that this only work for 'plain' Python types.
+
+  @returns: (string, string) or None, if we can't determine the type.
+
+  """
+  if isinstance(value, basestring):
+    return ("String", "\"%s\"" % StringValueRules(value))
+  elif isinstance(value, int):
+    return ("Int", "%d" % value)
+  elif isinstance(value, long):
+    return ("Integer", "%d" % value)
+  elif isinstance(value, float):
+    return ("Double", "%f" % value)
+  else:
+    return None
+
+
+def IdentifyOrigin(all_items, value):
+  """Tries to identify a constant name from a constant's value.
+
+  This uses a simple algorithm: is there a constant (and only one)
+  with the same value? If so, then it returns that constants' name.
+
+  @note: it is recommended to use this only for tuples/lists/sets, and
+      not for individual (top-level) values
+  @param all_items: a dictionary of name/values for the current module
+  @param value: the value for which we try to find an origin
+
+  """
+  found = [name for (name, v) in all_items.items() if v is value]
+  if len(found) == 1:
+    return found[0]
+  else:
+    return None
+
+
+def FormatListElems(all_items, pfx_name, ovals, tvals):
+  """Formats a list's elements.
+
+  This formats the elements as either values or, if we find all
+  origins, as names.
+
+  @param all_items: a dictionary of name/values for the current module
+  @param pfx_name: the prefix name currently used
+  @param ovals: the list of actual (Python) values
+  @param tvals: the list of values we want to format in the Haskell form
+
+  """
+  origins = [IdentifyOrigin(all_items, v) for v in ovals]
+  if compat.all(x is not None for x in origins):
+    values = [NameRules(pfx_name + origin) for origin in origins]
+  else:
+    values = tvals
+  return ", ".join(values)
+
+
+def ConvertVariable(prefix, name, value, all_items):
+  """Converts a given variable to Haskell code.
+
+  @param prefix: a prefix for the Haskell name (useful for module
+      identification)
+  @param name: the Python name
+  @param value: the value
+  @param all_items: a dictionary of name/value for the module being
+      processed
+  @return: a list of Haskell code lines
+
+  """
+  lines = []
+  if prefix:
+    pfx_name = prefix + "_"
+    fqn = prefix + "." + name
+  else:
+    pfx_name = ""
+    fqn = name
+  hs_name = NameRules(pfx_name + name)
+  hs_typeval = HaskellTypeVal(value)
+  if (isinstance(value, types.ModuleType) or callable(value) or
+      PRIVATE_RE.match(name)):
+    # no sense in marking these, as we don't _want_ to convert them; the
+    # message in the next if block is for datatypes we don't _know_
+    # (yet) how to convert
+    pass
+  elif not CONSTANT_RE.match(name):
+    lines.append("-- Skipped %s %s, not constant" % (fqn, type(value)))
+  elif hs_typeval is not None:
+    # this is a simple value
+    (hs_type, hs_val) = hs_typeval
+    lines.append("-- | Converted from Python constant %s" % fqn)
+    lines.append("%s :: %s" % (hs_name, hs_type))
+    lines.append("%s = %s" % (hs_name, hs_val))
+  elif isinstance(value, dict):
+    if value:
+      lines.append("-- Following lines come from dictionary %s" % fqn)
+      for k in sorted(value.keys()):
+        lines.extend(ConvertVariable(prefix, DictKeyName(name, k),
+                                     value[k], all_items))
+  elif isinstance(value, tuple):
+    tvs = [HaskellTypeVal(elem) for elem in value]
+    if compat.all(e is not None for e in tvs):
+      ttypes = ", ".join(e[0] for e in tvs)
+      tvals = FormatListElems(all_items, pfx_name, value, [e[1] for e in tvs])
+      lines.append("-- | Converted from Python tuple %s" % fqn)
+      lines.append("%s :: (%s)" % (hs_name, ttypes))
+      lines.append("%s = (%s)" % (hs_name, tvals))
+    else:
+      lines.append("-- Skipped tuple %s, cannot convert all elements" % fqn)
+  elif isinstance(value, (list, set, frozenset)):
+    # Lists and frozensets are handled the same in Haskell: as lists,
+    # since lists are immutable and we don't need for constants the
+    # high-speed of an actual Set type. However, we can only convert
+    # them if they have the same type for all elements (which is a
+    # normal expectation for constants, our code should be well
+    # behaved); note that this is different from the tuples case,
+    # where we always (for some values of always) can convert
+    tvs = [HaskellTypeVal(elem) for elem in value]
+    if compat.all(e is not None for e in tvs):
+      ttypes, tvals = zip(*tvs)
+      uniq_types = set(ttypes)
+      if len(uniq_types) == 1:
+        values = FormatListElems(all_items, pfx_name, value, tvals)
+        lines.append("-- | Converted from Python list or set %s" % fqn)
+        lines.append("%s :: [%s]" % (hs_name, uniq_types.pop()))
+        lines.append("%s = [%s]" % (hs_name, values))
+      else:
+        lines.append("-- | Skipped list/set %s, is not homogeneous" % fqn)
+    else:
+      lines.append("-- | Skipped list/set %s, cannot convert all elems" % fqn)
+  elif isinstance(value, RE_TYPE):
+    tvs = HaskellTypeVal(value.pattern)
+    assert tvs is not None
+    lines.append("-- | Converted from Python RE object %s" % fqn)
+    lines.append("%s :: %s" % (hs_name, tvs[0]))
+    lines.append("%s = %s" % (hs_name, tvs[1]))
+  else:
+    lines.append("-- Skipped %s, %s not handled" % (fqn, type(value)))
+  return lines
+
+
+def Convert(module, prefix):
   """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("")
+  all_items = dict((name, getattr(module, name)) for name in dir(module))
+
+  for name in sorted(all_items.keys()):
+    value = all_items[name]
+    new_lines = ConvertVariable(prefix, name, value, all_items)
+    if new_lines:
+      lines.extend(new_lines)
+      lines.append("")
 
   return "\n".join(lines)
 
 
 def main():
-  print Convert()
+  print Convert(constants, "")
+  print Convert(luxi, "luxi")
 
 
 if __name__ == "__main__":
index 0970bcb..4ad506b 100755 (executable)
@@ -30,15 +30,21 @@ from ganeti import query
 from ganeti.build import sphinx_ext
 
 
-_QUERY_FIELDS_RE = re.compile(r"^@QUERY_FIELDS_(?P<kind>[A-Z]+)@$")
+_DOC_RE = re.compile(r"^@(?P<class>[A-Z_]+)_(?P<kind>[A-Z]+)@$")
+
+_DOC_CLASSES_DATA = {
+  "CONSTANTS": (sphinx_ext.DOCUMENTED_CONSTANTS, sphinx_ext.BuildValuesDoc),
+  "QUERY_FIELDS": (query.ALL_FIELDS, sphinx_ext.BuildQueryFields),
+  }
 
 
 def main():
   for line in fileinput.input():
-    m = _QUERY_FIELDS_RE.match(line)
+    m = _DOC_RE.match(line)
     if m:
-      fields = query.ALL_FIELDS[m.group("kind").lower()]
-      for i in sphinx_ext.BuildQueryFields(fields):
+      fields_dict, builder = _DOC_CLASSES_DATA[m.group("class")]
+      fields = fields_dict[m.group("kind").lower()]
+      for i in builder(fields):
         print i
     else:
       print line,
index 8d6c2d4..91bd004 100755 (executable)
@@ -1,7 +1,7 @@
 #!/bin/bash
 #
 
-# Copyright (C) 2010 Google Inc.
+# 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
@@ -19,6 +19,7 @@
 # 02110-1301, USA.
 
 set -e
+set -u
 
 : ${COVERAGE:=coverage}
 : ${PYTHON:=python}
@@ -26,21 +27,30 @@ set -e
 : ${TEXT_COVERAGE:?}
 : ${GANETI_TEMP_DIR:?}
 
-omit=$($PYTHON -c 'import sys; import os;
-print ",".join("%s/" % i for i in set([sys.prefix, sys.exec_prefix,
-   os.environ["GANETI_TEMP_DIR"] + "/test"]))')
-omit="--omit=$omit"
+reportargs=(
+  '--include=*'
+  '--omit=test/*'
+  )
 
 $COVERAGE erase
 
 for script; do
-  $COVERAGE run --branch --append $script
+  if [[ "$script" == *-runasroot.py ]]; then
+    if [[ -z "$FAKEROOT" ]]; then
+      echo "FAKEROOT variable not set and needed for $script" >&2
+      exit 1
+    fi
+    cmdprefix="$FAKEROOT"
+  else
+    cmdprefix=
+  fi
+  $cmdprefix $COVERAGE run --branch --append "${reportargs[@]}" $script
 done
 
 echo "Writing text report to $TEXT_COVERAGE ..." >&2
-$COVERAGE report $omit | tee "$TEXT_COVERAGE"
+$COVERAGE report "${reportargs[@]}" | tee "$TEXT_COVERAGE"
 
 if [[ -n "$HTML_COVERAGE" ]]; then
   echo "Generating HTML report in $HTML_COVERAGE ..." >&2
-  $COVERAGE html $omit -d "$HTML_COVERAGE"
+  $COVERAGE html "${reportargs[@]}" -d "$HTML_COVERAGE"
 fi
index e32f863..316b547 100755 (executable)
@@ -8,11 +8,18 @@ set -e
 tmpdir=$(mktemp -d -t gntbuild.XXXXXXXX)
 trap "rm -rf $tmpdir" EXIT
 
-cp -r autotools daemons scripts lib tools test $tmpdir
+mkdir $tmpdir/doc
+
+cp -r autotools daemons scripts lib tools test qa $tmpdir
+cp -r doc/examples $tmpdir/doc
+
 mv $tmpdir/lib $tmpdir/ganeti
+ln -T -s $tmpdir/ganeti $tmpdir/lib
 mkdir -p $tmpdir/htools
-if [ -e htools/test ]; then
-  cp -p htools/test $tmpdir/htools/
-fi
+for htest in htools hpc-htools test offline-test.sh cli-tests-defs.sh; do
+  if [ -e htools/$htest ]; then
+    cp -p htools/$htest $tmpdir/htools/
+  fi
+done
 
 cd $tmpdir && GANETI_TEMP_DIR="$tmpdir" "$@"
index d933613..cbd1de9 100755 (executable)
@@ -1,7 +1,7 @@
 #!/bin/bash
 #
 
-# Copyright (C) 2010 Google Inc.
+# 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
@@ -22,7 +22,17 @@ set -e
 
 filename=$1
 
+execasroot() {
+  if [[ -z "$FAKEROOT" ]]; then
+    echo "FAKEROOT variable not set" >&2
+    exit 1
+  fi
+  exec "$FAKEROOT" "$@"
+}
+
 case "$filename" in
+  *-runasroot.py) execasroot $PYTHON "$@" ;;
   *.py) exec $PYTHON "$@" ;;
+  *-runasroot) execasroot "$@" ;;
   *) exec "$@" ;;
 esac
diff --git a/autotools/wrong-hardcoded-paths b/autotools/wrong-hardcoded-paths
new file mode 100644 (file)
index 0000000..b6d0ef3
--- /dev/null
@@ -0,0 +1,3 @@
+/etc/ganeti
+/usr/(local/)?lib/ganeti
+/(usr/local/)?var/(lib|run|log)/ganeti
index 44d7f67..0d0630a 100644 (file)
@@ -1,8 +1,8 @@
 # Configure script for Ganeti
 m4_define([gnt_version_major], [2])
-m4_define([gnt_version_minor], [5])
-m4_define([gnt_version_revision], [2])
-m4_define([gnt_version_suffix], [])
+m4_define([gnt_version_minor], [6])
+m4_define([gnt_version_revision], [0])
+m4_define([gnt_version_suffix], [~rc4])
 m4_define([gnt_version_full],
           m4_format([%d.%d.%d%s],
                     gnt_version_major, gnt_version_minor,
@@ -83,21 +83,43 @@ AC_SUBST(XEN_BOOTLOADER, $xen_bootloader)
 # --with-xen-kernel=...
 AC_ARG_WITH([xen-kernel],
   [AS_HELP_STRING([--with-xen-kernel=PATH],
-    [DomU kernel image for Xen hypervisor (default is /boot/vmlinuz-2.6-xenU)]
+    [DomU kernel image for Xen hypervisor (default is /boot/vmlinuz-3-xenU)]
   )],
   [xen_kernel="$withval"],
-  [xen_kernel="/boot/vmlinuz-2.6-xenU"])
+  [xen_kernel="/boot/vmlinuz-3-xenU"])
 AC_SUBST(XEN_KERNEL, $xen_kernel)
 
 # --with-xen-initrd=...
 AC_ARG_WITH([xen-initrd],
   [AS_HELP_STRING([--with-xen-initrd=PATH],
-    [DomU initrd image for Xen hypervisor (default is /boot/initrd-2.6-xenU)]
+    [DomU initrd image for Xen hypervisor (default is /boot/initrd-3-xenU)]
   )],
   [xen_initrd="$withval"],
-  [xen_initrd="/boot/initrd-2.6-xenU"])
+  [xen_initrd="/boot/initrd-3-xenU"])
 AC_SUBST(XEN_INITRD, $xen_initrd)
 
+# --with-xen-cmd=...
+AC_ARG_WITH([xen-cmd],
+  [AS_HELP_STRING([--with-xen-cmd=CMD],
+    [Sets the xen cli interface command (default is xm)]
+  )],
+  [xen_cmd="$withval"],
+  [xen_cmd="xm"])
+AC_SUBST(XEN_CMD, $xen_cmd)
+
+if ! test "$XEN_CMD" = xl -o "$XEN_CMD" = xm; then
+  AC_MSG_ERROR([Unsupported xen command specified])
+fi
+
+# --with-kvm-kernel=...
+AC_ARG_WITH([kvm-kernel],
+  [AS_HELP_STRING([--with-kvm-kernel=PATH],
+    [Guest kernel image for KVM hypervisor (default is /boot/vmlinuz-3-kvmU)]
+  )],
+  [kvm_kernel="$withval"],
+  [kvm_kernel="/boot/vmlinuz-3-kvmU"])
+AC_SUBST(KVM_KERNEL, $kvm_kernel)
+
 # --with-file-storage-dir=...
 AC_ARG_WITH([file-storage-dir],
   [AS_HELP_STRING([--with-file-storage-dir=PATH],
@@ -147,7 +169,7 @@ AC_SUBST(KVM_PATH, $kvm_path)
 # --with-lvm-stripecount=...
 AC_ARG_WITH([lvm-stripecount],
   [AS_HELP_STRING([--with-lvm-stripecount=NUM],
-    [the number of stripes to use for LVM volumes]
+    [the default number of stripes to use for LVM volumes]
     [ (default is 1)]
   )],
   [lvm_stripecount="$withval"],
@@ -208,15 +230,20 @@ AC_MSG_NOTICE([Group for clients is $group_admin])
 # --enable-drbd-barriers
 AC_ARG_ENABLE([drbd-barriers],
   [AS_HELP_STRING([--enable-drbd-barriers],
-    [enable the DRBD barrier functionality (>= 8.0.12) (default: enabled)])],
+    [enable by default the DRBD barriers functionality (>= 8.0.12) (default: enabled)])],
   [[if test "$enableval" != no; then
-      DRBD_BARRIERS=True
+      DRBD_BARRIERS=n
+      DRBD_NO_META_FLUSH=False
     else
-      DRBD_BARRIERS=False
+      DRBD_BARRIERS=bf
+      DRBD_NO_META_FLUSH=True
     fi
   ]],
-  [DRBD_BARRIERS=True])
+  [DRBD_BARRIERS=n
+   DRBD_NO_META_FLUSH=False
+  ])
 AC_SUBST(DRBD_BARRIERS, $DRBD_BARRIERS)
+AC_SUBST(DRBD_NO_META_FLUSH, $DRBD_NO_META_FLUSH)
 
 # --enable-syslog[=no/yes/only]
 AC_ARG_ENABLE([syslog],
@@ -261,6 +288,42 @@ AC_ARG_ENABLE([htools-rapi],
         [],
         [enable_htools_rapi=no])
 
+# --enable-htools
+ENABLE_CONFD=
+AC_ARG_ENABLE([confd],
+  [AS_HELP_STRING([--enable-confd],
+  [enable the ganeti-confd daemon (default: python, options haskell/python/no)])],
+  [[case "$enableval" in
+      no)
+        enable_confd=False
+        py_confd=False
+        hs_confd=False
+        ;;
+      yes|python)
+        enable_confd=True
+        py_confd=True
+        hs_confd=False
+        ;;
+      haskell)
+        enable_confd=True
+        py_confd=False
+        hs_confd=True
+        ;;
+      *)
+        echo "Invalid value for enable-confd '$enableval'"
+        exit 1
+        ;;
+    esac
+  ]],
+  [enable_confd=True;py_confd=True;hs_confd=False])
+AC_SUBST(ENABLE_CONFD, $enable_confd)
+AC_SUBST(PY_CONFD, $py_confd)
+AC_SUBST(HS_CONFD, $hs_confd)
+
+AM_CONDITIONAL([WANT_CONFD], [test x$enable_confd = xTrue])
+AM_CONDITIONAL([PY_CONFD], [test x$py_confd = xTrue])
+AM_CONDITIONAL([HS_CONFD], [test x$hs_confd = xTrue])
+
 # --with-disk-separator=...
 AC_ARG_WITH([disk-separator],
   [AS_HELP_STRING([--with-disk-separator=STRING],
@@ -324,6 +387,7 @@ if test -z "$PEP8"
 then
   AC_MSG_WARN([pep8 not found, checking code will not be complete])
 fi
+AM_CONDITIONAL([HAS_PEP8], [test "$PEP8"])
 
 # Check for socat
 AC_ARG_VAR(SOCAT, [socat path])
@@ -333,6 +397,14 @@ then
   AC_MSG_ERROR([socat not found])
 fi
 
+# Check for qemu-img
+AC_ARG_VAR(QEMUIMG_PATH, [qemu-img path])
+AC_PATH_PROG(QEMUIMG_PATH, [qemu-img], [])
+if test -z "$QEMUIMG_PATH"
+then
+  AC_MSG_WARN([qemu-img not found, using ovfconverter will not be possible])
+fi
+
 if test "$enable_htools" != "no"; then
 
 # Check for ghc
@@ -447,11 +519,34 @@ if test "$HADDOCK" && test "$HSCOLOUR"; then
 fi
 AC_SUBST(HTOOLS_APIDOC)
 
+# Check for hlint
+HLINT=no
+AC_ARG_VAR(HLINT, [hlint path])
+AC_PATH_PROG(HLINT, [hlint], [])
+if test -z "$HLINT"; then
+  AC_MSG_WARN([hlint not found, checking code will not be possible])
+fi
+
 fi # end if enable_htools, define automake conditions
 
+if test "$HTOOLS" != "yes" && test "$HS_CONFD" = "True"; then
+   AC_MSG_ERROR(m4_normalize([cannot enable Haskell version of ganeti-confd if
+                              htools support is not enabled]))
+fi
+
 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])
+AM_CONDITIONAL([HAS_HLINT], [test "$HLINT"])
+
+# Check for fakeroot
+AC_ARG_VAR(FAKEROOT_PATH, [fakeroot path])
+AC_PATH_PROG(FAKEROOT_PATH, [fakeroot], [])
+if test -z "$FAKEROOT_PATH"; then
+  AC_MSG_WARN(m4_normalize([fakeroot not found, tests that must run as root
+                            will not be executed]))
+fi
+AM_CONDITIONAL([HAS_FAKEROOT], [test "x$FAKEROOT_PATH" != x])
 
 SOCAT_USE_ESCAPE=
 AC_ARG_ENABLE([socat-escape],
@@ -518,6 +613,7 @@ AC_PYTHON_MODULE(simplejson, t)
 AC_PYTHON_MODULE(pyparsing, t)
 AC_PYTHON_MODULE(pyinotify, t)
 AC_PYTHON_MODULE(pycurl, t)
+AC_PYTHON_MODULE(affinity)
 
 # This is optional but then we've limited functionality
 AC_PYTHON_MODULE(paramiko)
index 819fd6b..b754e7f 100644 (file)
@@ -1,7 +1,7 @@
 #!/bin/bash
 #
 
-# Copyright (C) 2009 Google Inc.
+# Copyright (C) 2009, 2011, 2012 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
@@ -29,9 +29,12 @@ DAEMONS=(
   ganeti-noded
   ganeti-masterd
   ganeti-rapi
-  ganeti-confd
   )
 
+if [[ "@CUSTOM_ENABLE_CONFD@" == True ]]; then
+  DAEMONS+=( ganeti-confd )
+fi
+
 NODED_ARGS=
 MASTERD_ARGS=
 CONFD_ARGS=
@@ -42,6 +45,12 @@ if [[ -s $defaults_file ]]; then
   . $defaults_file
 fi
 
+# Meant to facilitate use utilities in /etc/rc.d/init.d/functions in case
+# start-stop-daemon is not available.
+_ignore_error() {
+  eval "$@" || :
+}
+
 _daemon_pidfile() {
   echo "@LOCALSTATEDIR@/run/ganeti/$1.pid"
 }
@@ -108,6 +117,30 @@ check_exitcode() {
   return 0
 }
 
+# Prints path to PID file for a daemon.
+daemon_pidfile() {
+  if [[ "$#" -lt 1 ]]; then
+    echo 'Missing daemon name.' >&2
+    return 1
+  fi
+
+  local name="$1"; shift
+
+  _daemon_pidfile $name
+}
+
+# Prints path to daemon executable.
+daemon_executable() {
+  if [[ "$#" -lt 1 ]]; then
+    echo 'Missing daemon name.' >&2
+    return 1
+  fi
+
+  local name="$1"; shift
+
+  _daemon_executable $name
+}
+
 # Prints a list of all daemons in the order in which they should be started
 list_start_daemons() {
   local name
@@ -149,9 +182,17 @@ check() {
   fi
 
   local name="$1"; shift
-
-  start-stop-daemon --stop --signal 0 --quiet \
-    --pidfile $(_daemon_pidfile $name)
+  local pidfile=$(_daemon_pidfile $name)
+  local daemonexec=$(_daemon_executable $name)
+
+  if type -p start-stop-daemon >/dev/null; then
+    start-stop-daemon --stop --signal 0 --quiet \
+      --pidfile $pidfile
+  else
+    _ignore_error status \
+      -p $pidfile \
+      $daemonexec
+  fi
 }
 
 # Starts a daemon
@@ -162,21 +203,38 @@ start() {
   fi
 
   local name="$1"; shift
-
   # Convert daemon name to uppercase after removing "ganeti-" prefix
   local plain_name=${name#ganeti-}
   local ucname=$(tr a-z A-Z <<<$plain_name)
+  local pidfile=$(_daemon_pidfile $name)
+  local usergroup=$(_daemon_usergroup $plain_name)
+  local daemonexec=$(_daemon_executable $name)
+
+  if [[ "$name" == ganeti-confd &&
+        "@CUSTOM_ENABLE_CONFD@" == False ]]; then
+    echo 'ganeti-confd disabled at build time' >&2
+    return 1
+  fi
 
   # Read $<daemon>_ARGS and $EXTRA_<daemon>_ARGS
   eval local args="\"\$${ucname}_ARGS \$EXTRA_${ucname}_ARGS\""
 
   @PKGLIBDIR@/ensure-dirs
 
-  start-stop-daemon --start --quiet --oknodo \
-    --pidfile $(_daemon_pidfile $name) \
-    --startas $(_daemon_executable $name) \
-    --chuid $(_daemon_usergroup $plain_name) \
-    -- $args "$@"
+  if type -p start-stop-daemon >/dev/null; then
+    start-stop-daemon --start --quiet --oknodo \
+      --pidfile $pidfile \
+      --startas $daemonexec \
+      --chuid $usergroup \
+      -- $args "$@"
+  else
+    # TODO: Find a way to start daemon with a group, until then the group must
+    # be removed
+    _ignore_error daemon \
+      --pidfile $pidfile \
+      --user ${usergroup%:*} \
+      $daemonexec $args "$@"
+  fi
 }
 
 # Stops a daemon
@@ -187,9 +245,14 @@ stop() {
   fi
 
   local name="$1"; shift
+  local pidfile=$(_daemon_pidfile $name)
 
-  start-stop-daemon --stop --quiet --oknodo --retry 30 \
-    --pidfile $(_daemon_pidfile $name)
+  if type -p start-stop-daemon >/dev/null; then
+    start-stop-daemon --stop --quiet --oknodo --retry 30 \
+      --pidfile $pidfile
+  else
+    _ignore_error killproc -p $pidfile $name
+  fi
 }
 
 # Starts a daemon if it's not yet running
@@ -242,6 +305,12 @@ reload_ssh_keys() {
   @RPL_SSH_INITD_SCRIPT@ restart
 }
 
+# Read @SYSCONFDIR@/rc.d/init.d/functions if start-stop-daemon not available
+if ! type -p start-stop-daemon >/dev/null && \
+   [[ -f @SYSCONFDIR@/rc.d/init.d/functions ]]; then
+  _ignore_error . @SYSCONFDIR@/rc.d/init.d/functions
+fi
+
 if [[ "$#" -lt 1 ]]; then
   echo "Usage: $0 <action>" >&2
   exit 1
index 8603460..0163dff 100755 (executable)
@@ -200,7 +200,7 @@ class StatusFile:
 
     self._data.mtime = time.time()
     utils.WriteFile(self._path,
-                    data=serializer.DumpJson(self._data.ToDict(), indent=True),
+                    data=serializer.DumpJson(self._data.ToDict()),
                     mode=0400)
 
 
index 665c0e6..8850187 100644 (file)
@@ -5,7 +5,7 @@ Documents Ganeti version |version|
 
 .. contents::
 
-.. highlight:: text
+.. highlight:: shell-example
 
 Introduction
 ------------
@@ -72,7 +72,9 @@ Depending on the role, each node will run a set of daemons:
   this node's hardware resources; it runs on all nodes which are in a
   cluster
 - the :command:`ganeti-confd` daemon (Ganeti 2.1+) which runs on all
-  nodes, but is only functional on master candidate nodes
+  nodes, but is only functional on master candidate nodes; this daemon
+  can be disabled at configuration time if you don't need its
+  functionality
 - the :command:`ganeti-rapi` daemon which runs on the master node and
   offers an HTTP-based API for the cluster
 - the :command:`ganeti-masterd` daemon which runs on the master node and
@@ -113,7 +115,7 @@ The are multiple options for the storage provided to an instance; while
 the instance sees the same virtual drive in all cases, the node-level
 configuration varies between them.
 
-There are four disk templates you can choose from:
+There are five disk templates you can choose from:
 
 diskless
   The instance has no disks. Only used for special purpose operating
@@ -136,6 +138,10 @@ drbd
   to obtain a highly available instance that can be failed over to a
   remote node should the primary one fail.
 
+rbd
+  The instance will use Volumes inside a RADOS cluster as backend for its
+  disks. It will access them using the RADOS block device (RBD).
+
 IAllocator
 ~~~~~~~~~~
 
@@ -167,8 +173,8 @@ or to nodes or instances. They are useful as a very simplistic
 information store for helping with cluster administration, for example
 by attaching owner information to each instance after it's created::
 
-  gnt-instance add … instance1
-  gnt-instance add-tags instance1 owner:user2
+  $ gnt-instance add … %instance1%
+  $ gnt-instance add-tags %instance1% %owner:user2%
 
 And then by listing each instance and its tags, this information could
 be used for contacting the users of each instance.
@@ -178,7 +184,7 @@ Jobs and OpCodes
 
 While not directly visible by an end-user, it's useful to know that a
 basic cluster operation (e.g. starting an instance) is represented
-internall by Ganeti as an *OpCode* (abbreviation from operation
+internally by Ganeti as an *OpCode* (abbreviation from operation
 code). These OpCodes are executed as part of a *Job*. The OpCodes in a
 single Job are processed serially by Ganeti, but different Jobs will be
 processed (depending on resource availability) in parallel. They will
@@ -232,11 +238,11 @@ installed any iallocator script.
 
 With the above parameters in mind, the command is::
 
-  gnt-instance add \
-    -n TARGET_NODE:SECONDARY_NODE \
-    -o OS_TYPE \
-    -t DISK_TEMPLATE -s DISK_SIZE \
-    INSTANCE_NAME
+  $ gnt-instance add \
+    -n %TARGET_NODE%:%SECONDARY_NODE% \
+    -o %OS_TYPE% \
+    -t %DISK_TEMPLATE% -s %DISK_SIZE% \
+    %INSTANCE_NAME%
 
 The instance name must be resolvable (e.g. exist in DNS) and usually
 points to an address in the same subnet as the cluster itself.
@@ -244,7 +250,8 @@ points to an address in the same subnet as the cluster itself.
 The above command has the minimum required options; other options you
 can give include, among others:
 
-- The memory size (``-B memory``)
+- The maximum/minimum memory size (``-B maxmem``, ``-B minmem``)
+  (``-B memory`` can be used to specify only one size)
 
 - The number of virtual CPUs (``-B vcpus``)
 
@@ -258,7 +265,7 @@ For example if you want to create an highly available instance, with a
 single disk of 50GB and the default memory size, having primary node
 ``node1`` and secondary node ``node3``, use the following command::
 
-  gnt-instance add -n node1:node3 -o debootstrap -t drbd \
+  $ gnt-instance add -n node1:node3 -o debootstrap -t drbd -s 50G \
     instance1
 
 There is a also a command for batch instance creation from a
@@ -275,7 +282,9 @@ Removing an instance is even easier than creating one. This operation is
 irreversible and destroys all the contents of your instance. Use with
 care::
 
-  gnt-instance remove INSTANCE_NAME
+  $ gnt-instance remove %INSTANCE_NAME%
+
+.. _instance-startup-label:
 
 Startup/shutdown
 ~~~~~~~~~~~~~~~~
@@ -283,11 +292,28 @@ Startup/shutdown
 Instances are automatically started at instance creation time. To
 manually start one which is currently stopped you can run::
 
-  gnt-instance startup INSTANCE_NAME
+  $ gnt-instance startup %INSTANCE_NAME%
+
+Ganeti will start an instance with up to its maximum instance memory. If
+not enough memory is available Ganeti will use all the available memory
+down to the instance minumum memory. If not even that amount of memory
+is free Ganeti will refuse to start the instance.
+
+Note, that this will not work when an instance is in a permanently
+stopped state ``offline``. In this case, you will first have to
+put it back to online mode by running::
+
+  $ gnt-instance modify --online %INSTANCE_NAME%
 
-While the command to stop one is::
+The command to stop the running instance is::
 
-  gnt-instance shutdown INSTANCE_NAME
+  $ gnt-instance shutdown %INSTANCE_NAME%
+
+If you want to shut the instance down more permanently, so that it
+does not require dynamically allocated resources (memory and vcpus),
+after shutting down an instance, execute the following::
+
+  $ gnt-instance modify --offline %INSTANCE_NAME%
 
 .. warning:: Do not use the Xen or KVM commands directly to stop
    instances. If you run for example ``xm shutdown`` or ``xm destroy``
@@ -304,7 +330,7 @@ instances.
 
 The command to see all the instances configured and their status is::
 
-  gnt-instance list
+  $ gnt-instance list
 
 The command can return a custom set of information when using the ``-o``
 option (as always, check the manpage for a detailed specification). Each
@@ -313,13 +339,34 @@ this output via the usual shell utilities (grep, sed, etc.).
 
 To get more detailed information about an instance, you can run::
 
-  gnt-instance info INSTANCE
+  $ gnt-instance info %INSTANCE%
 
 which will give a multi-line block of information about the instance,
 it's hardware resources (especially its disks and their redundancy
 status), etc. This is harder to parse and is more expensive than the
 list operation, but returns much more detailed information.
 
+Changing an instance's runtime memory
++++++++++++++++++++++++++++++++++++++
+
+Ganeti will always make sure an instance has a value between its maximum
+and its minimum memory available as runtime memory. As of version 2.6
+Ganeti will only choose a size different than the maximum size when
+starting up, failing over, or migrating an instance on a node with less
+than the maximum memory available. It won't resize other instances in
+order to free up space for an instance.
+
+If you find that you need more memory on a node any instance can be
+manually resized without downtime, with the command::
+
+  $ gnt-instance modify -m %SIZE% %INSTANCE_NAME%
+
+The same command can also be used to increase the memory available on an
+instance, provided that enough free memory is available on its node, and
+the specified size is not larger than the maximum memory size the
+instance had when it was first booted (an instance will be unable to see
+new memory above the maximum that was specified to the hypervisor at its
+boot time, if it needs to grow further a reboot becomes necessary).
 
 Export/Import
 +++++++++++++
@@ -328,7 +375,7 @@ You can create a snapshot of an instance disk and its Ganeti
 configuration, which then you can backup, or import into another
 cluster. The way to export an instance is::
 
-  gnt-backup export -n TARGET_NODE INSTANCE_NAME
+  $ gnt-backup export -n %TARGET_NODE% %INSTANCE_NAME%
 
 
 The target node can be any node in the cluster with enough space under
@@ -342,8 +389,8 @@ them out of the Ganeti exports directory.
 Importing an instance is similar to creating a new one, but additionally
 one must specify the location of the snapshot. The command is::
 
-  gnt-backup import -n TARGET_NODE \
-    --src-node=NODE --src-dir=DIR INSTANCE_NAME
+  $ gnt-backup import -n %TARGET_NODE% \
+    --src-node=%NODE% --src-dir=%DIR% %INSTANCE_NAME%
 
 By default, parameters will be read from the export information, but you
 can of course pass them in via the command line - most of the options
@@ -361,8 +408,8 @@ For this, ensure that the original, non-managed instance is stopped,
 then create a Ganeti instance in the usual way, except that instead of
 passing the disk information you specify the current volumes::
 
-  gnt-instance add -t plain -n HOME_NODE ... \
-    --disk 0:adopt=lv_name[,vg=vg_name] INSTANCE_NAME
+  $ gnt-instance add -t plain -n %HOME_NODE% ... \
+    --disk 0:adopt=%lv_name%[,vg=%vg_name%] %INSTANCE_NAME%
 
 This will take over the given logical volumes, rename them to the Ganeti
 standard (UUID-based), and without installing the OS on them start
@@ -457,11 +504,23 @@ fail it over to its secondary node, even if the primary has somehow
 failed and it's not up anymore. Doing it is really easy, on the master
 node you can just run::
 
-  gnt-instance failover INSTANCE_NAME
+  $ gnt-instance failover %INSTANCE_NAME%
 
 That's it. After the command completes the secondary node is now the
 primary, and vice-versa.
 
+The instance will be started with an amount of memory between its
+``maxmem`` and its ``minmem`` value, depending on the free memory on its
+target node, or the operation will fail if that's not possible. See
+:ref:`instance-startup-label` for details.
+
+If the instance's disk template is of type rbd, then you can specify
+the target node (which can be any node) explicitly, or specify an
+iallocator plugin. If you omit both, the default iallocator will be
+used to determine the target node::
+
+  $ gnt-instance failover -n %TARGET_NODE% %INSTANCE_NAME%
+
 Live migrating an instance
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -469,19 +528,33 @@ If an instance is built in highly available mode, it currently runs and
 both its nodes are running fine, you can at migrate it over to its
 secondary node, without downtime. On the master node you need to run::
 
-  gnt-instance migrate INSTANCE_NAME
+  $ gnt-instance migrate %INSTANCE_NAME%
 
 The current load on the instance and its memory size will influence how
 long the migration will take. In any case, for both KVM and Xen
 hypervisors, the migration will be transparent to the instance.
 
+If the destination node has less memory than the instance's current
+runtime memory, but at least the instance's minimum memory available
+Ganeti will automatically reduce the instance runtime memory before
+migrating it, unless the ``--no-runtime-changes`` option is passed, in
+which case the target node should have at least the instance's current
+runtime memory free.
+
+If the instance's disk template is of type rbd, then you can specify
+the target node (which can be any node) explicitly, or specify an
+iallocator plugin. If you omit both, the default iallocator will be
+used to determine the target node::
+
+   $ gnt-instance migrate -n %TARGET_NODE% %INSTANCE_NAME%
+
 Moving an instance (offline)
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 If an instance has not been create as mirrored, then the only way to
 change its primary node is to execute the move command::
 
-  gnt-instance move -n NEW_NODE INSTANCE
+  $ gnt-instance move -n %NEW_NODE% %INSTANCE%
 
 This has a few prerequisites:
 
@@ -510,13 +583,11 @@ for LVM, this means that the LVM commands must not return failures; it
 is common that after a complete disk failure, any LVM command aborts
 with an error similar to::
 
-  # vgs
+  $ vgs
   /dev/sdb1: read failed after 0 of 4096 at 0: Input/output error
-  /dev/sdb1: read failed after 0 of 4096 at 750153695232: Input/output
-  error
+  /dev/sdb1: read failed after 0 of 4096 at 750153695232: Input/output error
   /dev/sdb1: read failed after 0 of 4096 at 0: Input/output error
-  Couldn't find device with uuid
-  't30jmN-4Rcf-Fr5e-CURS-pawt-z0jU-m1TgeJ'.
+  Couldn't find device with uuid 't30jmN-4Rcf-Fr5e-CURS-pawt-z0jU-m1TgeJ'.
   Couldn't find all physical volumes for volume group xenvg.
 
 Before restoring an instance's disks to healthy status, it's needed to
@@ -527,7 +598,7 @@ process:
 #. first, if the disk is completely gone and LVM commands exit with
    “Couldn't find device with uuid…” then you need to run the command::
 
-    vgreduce --removemissing VOLUME_GROUP
+    $ vgreduce --removemissing %VOLUME_GROUP%
 
 #. after the above command, the LVM commands should be executing
    normally (warnings are normal, but the commands will not fail
@@ -536,7 +607,7 @@ process:
 #. if the failed disk is still visible in the output of the ``pvs``
    command, you need to deactivate it from allocations by running::
 
-    pvs -x n /dev/DISK
+    $ pvs -x n /dev/%DISK%
 
 At this point, the volume group should be consistent and any bad
 physical volumes should not longer be available for allocation.
@@ -565,19 +636,19 @@ though everything is already fine.
 For all three cases, the ``replace-disks`` operation can be used::
 
   # re-create disks on the primary node
-  gnt-instance replace-disks -p INSTANCE_NAME
+  $ gnt-instance replace-disks -p %INSTANCE_NAME%
   # re-create disks on the current secondary
-  gnt-instance replace-disks -s INSTANCE_NAME
+  $ gnt-instance replace-disks -s %INSTANCE_NAME%
   # change the secondary node, via manual specification
-  gnt-instance replace-disks -n NODE INSTANCE_NAME
+  $ gnt-instance replace-disks -n %NODE% %INSTANCE_NAME%
   # change the secondary node, via an iallocator script
-  gnt-instance replace-disks -I SCRIPT INSTANCE_NAME
+  $ gnt-instance replace-disks -I %SCRIPT% %INSTANCE_NAME%
   # since Ganeti 2.1: automatically fix the primary or secondary node
-  gnt-instance replace-disks -a INSTANCE_NAME
+  $ gnt-instance replace-disks -a %INSTANCE_NAME%
 
 Since the process involves copying all data from the working node to the
 target node, it will take a while, depending on the instance's disk
-size, node I/O system and network speed. But it is (baring any network
+size, node I/O system and network speed. But it is (barring any network
 interruption) completely transparent for the instance.
 
 Re-creating disks for non-redundant instances
@@ -590,7 +661,7 @@ re-create the disks. But it's possible to at-least re-create empty
 disks, after which a reinstall can be run, via the ``recreate-disks``
 command::
 
-  gnt-instance recreate-disks INSTANCE
+  $ gnt-instance recreate-disks %INSTANCE%
 
 Note that this will fail if the disks already exists.
 
@@ -602,17 +673,17 @@ It is possible to convert between a non-redundant instance of type
 modify`` command::
 
   # start with a non-redundant instance
-  gnt-instance add -t plain ... INSTANCE
+  $ gnt-instance add -t plain ... %INSTANCE%
 
   # later convert it to redundant
-  gnt-instance stop INSTANCE
-  gnt-instance modify -t drbd -n NEW_SECONDARY INSTANCE
-  gnt-instance start INSTANCE
+  $ gnt-instance stop %INSTANCE%
+  $ gnt-instance modify -t drbd -n %NEW_SECONDARY% %INSTANCE%
+  $ gnt-instance start %INSTANCE%
 
   # and convert it back
-  gnt-instance stop INSTANCE
-  gnt-instance modify -t plain INSTANCE
-  gnt-instance start INSTANCE
+  $ gnt-instance stop %INSTANCE%
+  $ gnt-instance modify -t plain %INSTANCE%
+  $ gnt-instance start %INSTANCE%
 
 The conversion must be done while the instance is stopped, and
 converting from plain to drbd template presents a small risk, especially
@@ -633,7 +704,7 @@ instance, or will break replication and your data will be
 inconsistent. The correct way to access an instance's disks is to run
 (on the master node, as usual) the command::
 
-  gnt-instance activate-disks INSTANCE
+  $ gnt-instance activate-disks %INSTANCE%
 
 And then, *on the primary node of the instance*, access the device that
 gets created. For example, you could mount the given disks, then edit
@@ -642,22 +713,24 @@ files on the filesystem, etc.
 Note that with partitioned disks (as opposed to whole-disk filesystems),
 you will need to use a tool like :manpage:`kpartx(8)`::
 
-  node1# gnt-instance activate-disks instance1
-  …
-  node1# ssh node3
-  node3# kpartx -l /dev/…
-  node3# kpartx -a /dev/…
-  node3# mount /dev/mapper/… /mnt/
+  # on node1
+  $ gnt-instance activate-disks %instance1%
+  node3:disk/0:…
+  $ ssh node3
+  # on node 3
+  $ kpartx -l /dev/…
+  $ kpartx -a /dev/…
+  $ mount /dev/mapper/… /mnt/
   # edit files under mnt as desired
-  node3# umount /mnt/
-  node3# kpartx -d /dev/…
-  node3# exit
-  node1#
+  $ umount /mnt/
+  $ kpartx -d /dev/…
+  $ exit
+  # back to node 1
 
 After you've finished you can deactivate them with the deactivate-disks
 command, which works in the same way::
 
-  gnt-instance deactivate-disks INSTANCE
+  $ gnt-instance deactivate-disks %INSTANCE%
 
 Note that if any process started by you is still using the disks, the
 above command will error out, and you **must** cleanup and ensure that
@@ -669,7 +742,7 @@ Accessing an instance's console
 
 The command to access a running instance's console is::
 
-  gnt-instance console INSTANCE_NAME
+  $ gnt-instance console %INSTANCE_NAME%
 
 Use the console normally and then type ``^]`` when done, to exit.
 
@@ -681,7 +754,7 @@ Reboot
 
 There is a wrapper command for rebooting instances::
 
-  gnt-instance reboot instance2
+  $ gnt-instance reboot %instance2%
 
 By default, this does the equivalent of shutting down and then starting
 the instance, but it accepts parameters to perform a soft-reboot (via
@@ -695,7 +768,7 @@ Instance OS definitions debugging
 Should you have any problems with instance operating systems the command
 to see a complete status for all your nodes is::
 
-   gnt-os diagnose
+   $ gnt-os diagnose
 
 .. _instance-relocation-label:
 
@@ -707,13 +780,13 @@ nodes ``(C, D)`` in a single move, it is possible to do so in a few
 steps::
 
   # instance is located on A, B
-  node1# gnt-instance replace -n nodeC instance1
+  $ gnt-instance replace -n %nodeC% %instance1%
   # instance has moved from (A, B) to (A, C)
   # we now flip the primary/secondary nodes
-  node1# gnt-instance migrate instance1
+  $ gnt-instance migrate %instance1%
   # instance lives on (C, A)
   # we can then change A to D via:
-  node1# gnt-instance replace -n nodeD instance1
+  $ gnt-instance replace -n %nodeD% %instance1%
 
 Which brings it into the final configuration of ``(C, D)``. Note that we
 needed to do two replace-disks operation (two copies of the instance
@@ -732,7 +805,7 @@ Add/readd
 It is at any time possible to extend the cluster with one more node, by
 using the node add operation::
 
-  gnt-node add NEW_NODE
+  $ gnt-node add %NEW_NODE%
 
 If the cluster has a replication network defined, then you need to pass
 the ``-s REPLICATION_IP`` parameter to this option.
@@ -741,7 +814,7 @@ A variation of this command can be used to re-configure a node if its
 Ganeti configuration is broken, for example if it has been reinstalled
 by mistake::
 
-  gnt-node add --readd EXISTING_NODE
+  $ gnt-node add --readd %EXISTING_NODE%
 
 This will reinitialise the node as if it's been newly added, but while
 keeping its existing configuration in the cluster (primary/secondary IP,
@@ -760,7 +833,7 @@ Failing over the master node
 If you want to promote a different node to the master role (for whatever
 reason), run on any other master-candidate node the command::
 
-  gnt-cluster master-failover
+  $ gnt-cluster master-failover
 
 and the node you ran it on is now the new master. In case you try to run
 this on a non master-candidate node, you will get an error telling you
@@ -772,13 +845,13 @@ Changing between the other roles
 The ``gnt-node modify`` command can be used to select a new role::
 
   # change to master candidate
-  gnt-node modify -C yes NODE
+  $ gnt-node modify -C yes %NODE%
   # change to drained status
-  gnt-node modify -D yes NODE
+  $ gnt-node modify -D yes %NODE%
   # change to offline status
-  gnt-node modify -O yes NODE
+  $ gnt-node modify -O yes %NODE%
   # change to regular mode (reset all flags)
-  gnt-node modify -O no -D no -C no NODE
+  $ gnt-node modify -O no -D no -C no %NODE%
 
 Note that the cluster requires that at any point in time, a certain
 number of nodes are master candidates, so changing from master candidate
@@ -803,8 +876,8 @@ For this step, you can use either individual instance move
 commands (as seen in :ref:`instance-change-primary-label`) or the bulk
 per-node versions; these are::
 
-  gnt-node migrate NODE
-  gnt-node evacuate NODE
+  $ gnt-node migrate %NODE%
+  $ gnt-node evacuate -s %NODE%
 
 Note that the instance “move” command doesn't currently have a node
 equivalent.
@@ -821,8 +894,8 @@ Secondary instance evacuation
 For the evacuation of secondary instances, a command called
 :command:`gnt-node evacuate` is provided and its syntax is::
 
-  gnt-node evacuate -I IALLOCATOR_SCRIPT NODE
-  gnt-node evacuate -n DESTINATION_NODE NODE
+  $ gnt-node evacuate -I %IALLOCATOR_SCRIPT% %NODE%
+  $ gnt-node evacuate -n %DESTINATION_NODE% %NODE%
 
 The first version will compute the new secondary for each instance in
 turn using the given iallocator script, whereas the second one will
@@ -834,7 +907,7 @@ Removal
 Once a node no longer has any instances (neither primary nor secondary),
 it's easy to remove it from the cluster::
 
-  gnt-node remove NODE_NAME
+  $ gnt-node remove %NODE_NAME%
 
 This will deconfigure the node, stop the ganeti daemons on it and leave
 it hopefully like before it joined to the cluster.
@@ -854,7 +927,7 @@ This is a command specific to LVM handling. It allows listing the
 logical volumes on a given node or on all nodes and their association to
 instances via the ``volumes`` command::
 
-  node1# gnt-node volumes
+  $ gnt-node volumes
   Node  PhysDev   VG    Name             Size Instance
   node1 /dev/sdb1 xenvg e61fbc97-….disk0 512M instance17
   node1 /dev/sdb1 xenvg ebd1a7d1-….disk0 512M instance19
@@ -878,7 +951,7 @@ uses.
 
 First is listing the backend storage and their space situation::
 
-  node1# gnt-node list-storage
+  $ gnt-node list-storage
   Node  Name        Size Used   Free
   node1 /dev/sda7 673.8G   0M 673.8G
   node1 /dev/sdb1 698.6G 1.5G 697.1G
@@ -888,7 +961,7 @@ First is listing the backend storage and their space situation::
 The default is to list LVM physical volumes. It's also possible to list
 the LVM volume groups::
 
-  node1# gnt-node list-storage -t lvm-vg
+  $ gnt-node list-storage -t lvm-vg
   Node  Name  Size
   node1 xenvg 1.3T
   node2 xenvg 1.3T
@@ -896,14 +969,14 @@ the LVM volume groups::
 Next is repairing storage units, which is currently only implemented for
 volume groups and does the equivalent of ``vgreduce --removemissing``::
 
-  node1# gnt-node repair-storage node2 lvm-vg xenvg
+  $ gnt-node repair-storage %node2% lvm-vg xenvg
   Sun Oct 25 22:21:45 2009 Repairing storage unit 'xenvg' on node2 ...
 
 Last is the modification of volume properties, which is (again) only
 implemented for LVM physical volumes and allows toggling the
 ``allocatable`` value::
 
-  node1# gnt-node modify-storage --allocatable=no node2 lvm-pv /dev/sdb1
+  $ gnt-node modify-storage --allocatable=no %node2% lvm-pv /dev/%sdb1%
 
 Use of the storage commands
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -935,14 +1008,14 @@ Standard operations
 One of the few commands that can be run on any node (not only the
 master) is the ``getmaster`` command::
 
-  node2# gnt-cluster getmaster
+  # on node2
+  $ gnt-cluster getmaster
   node1.example.com
-  node2#
 
 It is possible to query and change global cluster parameters via the
 ``info`` and ``modify`` commands::
 
-  node1# gnt-cluster info
+  $ gnt-cluster info
   Cluster name: cluster.example.com
   Cluster UUID: 07805e6f-f0af-4310-95f1-572862ee939c
   Creation time: 2009-09-25 05:04:15
@@ -982,7 +1055,7 @@ commands as follows:
 For detailed option list see the :manpage:`gnt-cluster(8)` man page.
 
 The cluster version can be obtained via the ``version`` command::
-  node1# gnt-cluster version
+  $ gnt-cluster version
   Software version: 2.1.0
   Internode protocol: 20
   Configuration format: 2010000
@@ -997,8 +1070,8 @@ Global node commands
 There are two commands provided for replicating files to all nodes of a
 cluster and for running commands on all the nodes::
 
-  node1# gnt-cluster copyfile /path/to/file
-  node1# gnt-cluster command ls -l /path/to/file
+  $ gnt-cluster copyfile %/path/to/file%
+  $ gnt-cluster command %ls -l /path/to/file%
 
 These are simple wrappers over scp/ssh and more advanced usage can be
 obtained using :manpage:`dsh(1)` and similar commands. But they are
@@ -1012,7 +1085,7 @@ one is ``verify`` which gives an overview on the cluster state,
 highlighting any issues. In normal operation, this command should return
 no ``ERROR`` messages::
 
-  node1# gnt-cluster verify
+  $ gnt-cluster verify
   Sun Oct 25 23:08:58 2009 * Verifying global settings
   Sun Oct 25 23:08:58 2009 * Gathering data (2 nodes)
   Sun Oct 25 23:09:00 2009 * Verifying node status
@@ -1028,7 +1101,7 @@ The second command is ``verify-disks``, which checks that the instance's
 disks have the correct status based on the desired instance state
 (up/down)::
 
-  node1# gnt-cluster verify-disks
+  $ gnt-cluster verify-disks
 
 Note that this command will show no output when disks are healthy.
 
@@ -1036,7 +1109,7 @@ The last command is used to repair any discrepancies in Ganeti's
 recorded disk size and the actual disk size (disk size information is
 needed for proper activation and growth of DRBD-based disks)::
 
-  node1# gnt-cluster repair-disk-sizes
+  $ gnt-cluster repair-disk-sizes
   Sun Oct 25 23:13:16 2009  - INFO: Disk 0 of instance instance1 has mismatched size, correcting: recorded 512, actual 2048
   Sun Oct 25 23:13:17 2009  - WARNING: Invalid result from node node4, ignoring node results
 
@@ -1052,8 +1125,7 @@ and other nodes, due to some node problems or if you manually modified
 configuration files, you can force an push of the master configuration
 to all other nodes via the ``redist-conf`` command::
 
-  node1# gnt-cluster redist-conf
-  node1#
+  $ gnt-cluster redist-conf
 
 This command will be silent unless there are problems sending updates to
 the other nodes.
@@ -1066,12 +1138,12 @@ It is possible to rename a cluster, or to change its IP address, via the
 ``rename`` command. If only the IP has changed, you need to pass the
 current name and Ganeti will realise its IP has changed::
 
-  node1# gnt-cluster rename cluster.example.com
+  $ gnt-cluster rename %cluster.example.com%
   This will rename the cluster to 'cluster.example.com'. If
   you are connected over the network to the cluster name, the operation
   is very dangerous as the IP address will be removed from the node and
   the change may not go through. Continue?
-  y/[n]/?: y
+  y/[n]/?: %y%
   Failure: prerequisites not met for this operation:
   Neither the name nor the IP address of the cluster has changed
 
@@ -1084,14 +1156,14 @@ Queue operations
 The job queue execution in Ganeti 2.0 and higher can be inspected,
 suspended and resumed via the ``queue`` command::
 
-  node1~# gnt-cluster queue info
+  $ gnt-cluster queue info
   The drain flag is unset
-  node1~# gnt-cluster queue drain
-  node1~# gnt-instance stop instance1
+  $ gnt-cluster queue drain
+  $ gnt-instance stop %instance1%
   Failed to submit job for instance1: Job queue is drained, refusing job
-  node1~# gnt-cluster queue info
+  $ gnt-cluster queue info
   The drain flag is set
-  node1~# gnt-cluster queue undrain
+  $ gnt-cluster queue undrain
 
 This is most useful if you have an active cluster and you need to
 upgrade the Ganeti software, or simply restart the software on any node:
@@ -1116,23 +1188,22 @@ via commenting out the cron job is not so good as this can be
 forgotten. Thus there are some commands for automated control of the
 watcher: ``pause``, ``info`` and ``continue``::
 
-  node1~# gnt-cluster watcher info
+  $ gnt-cluster watcher info
   The watcher is not paused.
-  node1~# gnt-cluster watcher pause 1h
+  $ gnt-cluster watcher pause %1h%
   The watcher is paused until Mon Oct 26 00:30:37 2009.
-  node1~# gnt-cluster watcher info
+  $ gnt-cluster watcher info
   The watcher is paused until Mon Oct 26 00:30:37 2009.
-  node1~# ganeti-watcher -d
+  $ ganeti-watcher -d
   2009-10-25 23:30:47,984:  pid=28867 ganeti-watcher:486 DEBUG Pause has been set, exiting
-  node1~# gnt-cluster watcher continue
+  $ gnt-cluster watcher continue
   The watcher is no longer paused.
-  node1~# ganeti-watcher -d
+  $ ganeti-watcher -d
   2009-10-25 23:31:04,789:  pid=28976 ganeti-watcher:345 DEBUG Archived 0 jobs, left 0
   2009-10-25 23:31:05,884:  pid=28976 ganeti-watcher:280 DEBUG Got data from cluster, writing instance status file
   2009-10-25 23:31:06,061:  pid=28976 ganeti-watcher:150 DEBUG Data didn't change, just touching status file
-  node1~# gnt-cluster watcher info
+  $ gnt-cluster watcher info
   The watcher is not paused.
-  node1~#
 
 The exact details of the argument to the ``pause`` command are available
 in the manpage.
@@ -1192,6 +1263,10 @@ of a cluster installation by following these steps on all of the nodes:
 6. Remove the ganeti state directory (``rm -rf /var/lib/ganeti/*``),
    replacing the path with the correct path for your installation.
 
+7. If using RBD, run ``rbd unmap /dev/rbdN`` to unmap the RBD disks.
+   Then remove the RBD disk images used by Ganeti, identified by their
+   UUIDs (``rbd rm uuid.rbd.diskN``).
+
 On the master node, remove the cluster from the master-netdev (usually
 ``xen-br0`` for bridged mode, otherwise ``eth0`` or similar), by running
 ``ip a del $clusterip/32 dev xen-br0`` (use the correct cluster ip and
@@ -1230,9 +1305,9 @@ Operations
 
 Tags can be added via ``add-tags``::
 
-  gnt-instance add-tags INSTANCE a b c
-  gnt-node add-tags INSTANCE a b c
-  gnt-cluster add-tags a b c
+  $ gnt-instance add-tags %INSTANCE% %a% %b% %c%
+  $ gnt-node add-tags %INSTANCE% %a% %b% %c%
+  $ gnt-cluster add-tags %a% %b% %c%
 
 
 The above commands add three tags to an instance, to a node and to the
@@ -1245,13 +1320,13 @@ argument. The file is expected to contain one tag per line.
 
 Tags can also be remove via a syntax very similar to the add one::
 
-  gnt-instance remove-tags INSTANCE a b c
+  $ gnt-instance remove-tags %INSTANCE% %a% %b% %c%
 
 And listed via::
 
-  gnt-instance list-tags
-  gnt-node list-tags
-  gnt-cluster list-tags
+  $ gnt-instance list-tags
+  $ gnt-node list-tags
+  $ gnt-cluster list-tags
 
 Global tag search
 +++++++++++++++++
@@ -1259,14 +1334,14 @@ Global tag search
 It is also possible to execute a global search on the all tags defined
 in the cluster configuration, via a cluster command::
 
-  gnt-cluster search-tags REGEXP
+  $ gnt-cluster search-tags %REGEXP%
 
 The parameter expected is a regular expression (see
 :manpage:`regex(7)`). This will return all tags that match the search,
 together with the object they are defined in (the names being show in a
 hierarchical kind of way)::
 
-  node1# gnt-cluster search-tags o
+  $ gnt-cluster search-tags %o%
   /cluster foo
   /instances/instance1 owner:bar
 
@@ -1280,7 +1355,7 @@ examined, canceled and archived by various invocations of the
 
 First is the job list command::
 
-  node1# gnt-job list
+  $ gnt-job list
   17771 success INSTANCE_QUERY_DATA
   17773 success CLUSTER_VERIFY_DISKS
   17775 success CLUSTER_REPAIR_DISK_SIZES
@@ -1291,7 +1366,7 @@ First is the job list command::
 More detailed information about a job can be found via the ``info``
 command::
 
-  node1# gnt-job info 17776
+  $ gnt-job info %17776%
   Job ID: 17776
     Status: error
     Received:         2009-10-25 23:18:02.180569
@@ -1314,9 +1389,9 @@ During the execution of a job, it's possible to follow the output of a
 job, similar to the log that one get from the ``gnt-`` commands, via the
 watch command::
 
-  node1# gnt-instance add --submit … instance1
+  $ gnt-instance add --submit … %instance1%
   JobID: 17818
-  node1# gnt-job watch 17818
+  $ gnt-job watch %17818%
   Output from job 17818 follows
   -----------------------------
   Mon Oct 26 00:22:48 2009  - INFO: Selected nodes for instance instance1 via iallocator dumb: node1, node2
@@ -1327,33 +1402,31 @@ watch command::
   Mon Oct 26 00:23:03 2009 creating os for instance instance1 on node node1
   Mon Oct 26 00:23:03 2009 * running the instance OS create scripts...
   Mon Oct 26 00:23:13 2009 * starting instance...
-  node1#
+  $
 
 This is useful if you need to follow a job's progress from multiple
 terminals.
 
 A job that has not yet started to run can be canceled::
 
-  node1# gnt-job cancel 17810
+  $ gnt-job cancel %17810%
 
 But not one that has already started execution::
 
-  node1# gnt-job cancel 17805
+  $ gnt-job cancel %17805%
   Job 17805 is no longer waiting in the queue
 
 There are two queues for jobs: the *current* and the *archive*
 queue. Jobs are initially submitted to the current queue, and they stay
 in that queue until they have finished execution (either successfully or
-not). At that point, they can be moved into the archive queue, and the
-ganeti-watcher script will do this automatically after 6 hours. The
-ganeti-cleaner script will remove the jobs from the archive directory
+not). At that point, they can be moved into the archive queue using e.g.
+``gnt-job autoarchive all``. The ``ganeti-watcher`` script will do this
+automatically 6 hours after a job is finished. The ``ganeti-cleaner``
+script will then remove archived the jobs from the archive directory
 after three weeks.
 
-Note that only jobs in the current queue can be viewed via the list and
-info commands; Ganeti itself doesn't examine the archive directory. If
-you need to see an older job, either move the file manually in the
-top-level queue directory, or look at its contents (it's a
-JSON-formatted file).
+Note that ``gnt-job list`` only shows jobs in the current queue.
+Archived jobs can be viewed using ``gnt-job info <id>``.
 
 Special Ganeti deployments
 --------------------------
@@ -1375,11 +1448,11 @@ should not be considered in the normal capacity planning, evacuation
 strategies, etc. In order to accomplish this, mark these nodes as
 non-``vm_capable``::
 
-  node1# gnt-node modify --vm-capable=no node3
+  $ gnt-node modify --vm-capable=no %node3%
 
 The vm_capable status can be listed as usual via ``gnt-node list``::
 
-  node1# gnt-node list -oname,vm_capable
+  $ gnt-node list -oname,vm_capable
   Node  VMCapable
   node1 Y
   node2 Y
@@ -1407,7 +1480,7 @@ candidates, either manually or automatically.
 
 As usual, the node modify operation can change this flag::
 
-  node1# gnt-node modify --auto-promote --master-capable=no node3
+  $ gnt-node modify --auto-promote --master-capable=no %node3%
   Fri Jan  7 06:23:07 2011  - INFO: Demoting from master candidate
   Fri Jan  7 06:23:08 2011  - INFO: Promoted nodes to master candidate role: node4
   Modified node node3
@@ -1416,7 +1489,7 @@ As usual, the node modify operation can change this flag::
 
 And the node list operation will list this flag::
 
-  node1# gnt-node list -oname,master_capable node1 node2 node3
+  $ gnt-node list -oname,master_capable %node1% %node2% %node3%
   Node  MasterCapable
   node1 Y
   node2 Y
index e15902f..57b0aae 100644 (file)
@@ -16,29 +16,37 @@ import sys, os
 # If extensions (or modules to document with autodoc) are in another directory,
 # add these directories to sys.path here. If the directory is relative to the
 # documentation root, use os.path.abspath to make it absolute, like shown here.
-#sys.path.append(os.path.abspath('.'))
+#sys.path.append(os.path.abspath("."))
 
 # -- General configuration -----------------------------------------------------
 
+# If your documentation needs a minimal Sphinx version, state it here.
+#needs_sphinx = "1.0"
+
 # 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', "ganeti.build.sphinx_ext"]
+# coming with Sphinx (named "sphinx.ext.*") or your custom ones.
+extensions = [
+  "sphinx.ext.todo",
+  "sphinx.ext.graphviz",
+  "ganeti.build.sphinx_ext",
+  "ganeti.build.shell_example_lexer",
+  ]
 
 # Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
+templates_path = ["_templates"]
 
 # The suffix of source filenames.
-source_suffix = '.rst'
+source_suffix = ".rst"
 
 # The encoding of source files.
-source_encoding = 'utf-8'
+source_encoding = "utf-8"
 
 # The master toctree document.
-master_doc = 'index'
+master_doc = "index"
 
 # General information about the project.
-project = u'Ganeti'
-copyright = u'2006, 2007, 2008, 2009, 2010, Google Inc.'
+project = u"Ganeti"
+copyright = u"2006, 2007, 2008, 2009, 2010, 2011, 2012, Google Inc."
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
@@ -52,25 +60,30 @@ copyright = u'2006, 2007, 2008, 2009, 2010, Google Inc.'
 
 # The language for content autogenerated by Sphinx. Refer to documentation
 # for a list of supported languages.
-language = 'en'
+language = "en"
 
 # There are two options for replacing |today|: either, you set today to some
 # non-false value, then it is used:
-#today = ''
+#today = ""
 # Else, today_fmt is used as the format for a strftime call.
-#today_fmt = '%B %d, %Y'
+#today_fmt = "%B %d, %Y"
 
 # List of documents that shouldn't be included in the build.
 #unused_docs = []
 
 # List of directories, relative to source directory, that shouldn't be searched
 # for source files.
-exclude_trees = ['_build', 'examples', 'api']
+exclude_trees = [
+  "_build",
+  "api",
+  "coverage"
+  "examples",
+  ]
 
 # The reST default role (used for this markup: `text`) to use for all documents.
 #default_role = None
 
-# If true, '()' will be appended to :func: etc. cross-reference text.
+# If true, "()" will be appended to :func: etc. cross-reference text.
 #add_function_parentheses = True
 
 # If true, the current module name will be prepended to all description
@@ -82,7 +95,7 @@ exclude_trees = ['_build', 'examples', 'api']
 #show_authors = False
 
 # The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
+pygments_style = "sphinx"
 
 # A list of ignored prefixes for module index sorting.
 #modindex_common_prefix = []
@@ -90,9 +103,9 @@ pygments_style = 'sphinx'
 
 # -- Options for HTML output ---------------------------------------------------
 
-# The theme to use for HTML and HTML Help pages.  Major themes that come with
-# Sphinx are currently 'default' and 'sphinxdoc'.
-html_theme = 'default'
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+html_theme = "default"
 
 # Theme options are theme-specific and customize the look and feel of a theme
 # further.  For a list of options available for each theme, see the
@@ -123,9 +136,9 @@ html_theme = 'default'
 # so a file named "default.css" will overwrite the builtin "default.css".
 html_static_path = []
 
-# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# If not "", a "Last updated on:" timestamp is inserted at every page bottom,
 # using the given strftime format.
-#html_last_updated_fmt = '%b %d, %Y'
+#html_last_updated_fmt = "%b %d, %Y"
 
 # If true, SmartyPants will be used to convert quotes and dashes to
 # typographically correct entities.
@@ -150,31 +163,37 @@ html_use_index = False
 # If true, links to the reST sources are added to the pages.
 #html_show_sourcelink = True
 
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+#html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+#html_show_copyright = True
+
 # If true, an OpenSearch description file will be output, and all pages will
 # contain a <link> tag referring to it.  The value of this option must be the
 # base URL from which the finished HTML is served.
-#html_use_opensearch = ''
+#html_use_opensearch = ""
 
 # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
-#html_file_suffix = ''
+#html_file_suffix = ""
 
 # Output file base name for HTML help builder.
-htmlhelp_basename = 'Ganetidoc'
+htmlhelp_basename = "Ganetidoc"
 
 
 # -- Options for LaTeX output --------------------------------------------------
 
-# The paper size ('letter' or 'a4').
-#latex_paper_size = 'letter'
+# The paper size ("letter" or "a4").
+#latex_paper_size = "a4"
 
-# The font size ('10pt', '11pt' or '12pt').
-#latex_font_size = '10pt'
+# The font size ("10pt", "11pt" or "12pt").
+#latex_font_size = "10pt"
 
 # Grouping the document tree into LaTeX files. List of tuples
 # (source start file, target name, title, author, documentclass [howto/manual]).
 latex_documents = [
-  ('index', 'Ganeti.tex', u'Ganeti Documentation',
-   u'Google Inc.', 'manual'),
+  ("index", "Ganeti.tex", u"Ganeti Documentation",
+   u"Google Inc.", "manual"),
 ]
 
 # The name of an image file (relative to this directory) to place at the top of
@@ -185,8 +204,14 @@ latex_documents = [
 # not chapters.
 #latex_use_parts = False
 
+# If true, show page references after internal links.
+#latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+#latex_show_urls = False
+
 # Additional stuff for the LaTeX preamble.
-#latex_preamble = ''
+#latex_preamble = ""
 
 # Documents to append as an appendix to all manuals.
 #latex_appendices = []
index a48f4fd..d30be86 100644 (file)
@@ -518,27 +518,34 @@ A confd query will look like this, on the wire::
     "hmac": "4a4139b2c3c5921f7e439469a0a45ad200aead0f"
   }
 
-"plj0" is a fourcc that details the message content. It stands for plain
+``plj0`` is a fourcc that details the message content. It stands for plain
 json 0, and can be changed as we move on to different type of protocols
 (for example protocol buffers, or encrypted json). What follows is a
 json encoded string, with the following fields:
 
-- 'msg' contains a JSON-encoded query, its fields are:
+- ``msg`` contains a JSON-encoded query, its fields are:
 
-  - 'protocol', integer, is the confd protocol version (initially just
-    constants.CONFD_PROTOCOL_VERSION, with a value of 1)
-  - 'type', integer, is the query type. For example "node role by name"
-    or "node primary ip by instance ip". Constants will be provided for
-    the actual available query types.
-  - 'query', string, is the search key. For example an ip, or a node
-    name.
-  - 'rsalt', string, is the required response salt. The client must use
-    it to recognize which answer it's getting.
+  - ``protocol``, integer, is the confd protocol version (initially
+    just ``constants.CONFD_PROTOCOL_VERSION``, with a value of 1)
+  - ``type``, integer, is the query type. For example "node role by
+    name" or "node primary ip by instance ip". Constants will be
+    provided for the actual available query types
+  - ``query`` is a multi-type field (depending on the ``type`` field):
 
-- 'salt' must be the current unix timestamp, according to the client.
-  Servers can refuse messages which have a wrong timing, according to
-  their configuration and clock.
-- 'hmac' is an hmac signature of salt+msg, with the cluster hmac key
+    - it can be missing, when the request is fully determined by the
+      ``type`` field
+    - it can contain a string which denotes the search key: for
+      example an IP, or a node name
+    - it can contain a dictionary, in which case the actual details
+      vary further per request type
+
+  - ``rsalt``, string, is the required response salt; the client must
+    use it to recognize which answer it's getting.
+
+- ``salt`` must be the current unix timestamp, according to the
+  client; servers should refuse messages which have a wrong timing,
+  according to their configuration and clock
+- ``hmac`` is an hmac signature of salt+msg, with the cluster hmac key
 
 If an answer comes back (which is optional, since confd works over UDP)
 it will be in this format::
@@ -554,24 +561,25 @@ it will be in this format::
 
 Where:
 
-- 'plj0' the message type magic fourcc, as discussed above
-- 'msg' contains a JSON-encoded answer, its fields are:
-
-  - 'protocol', integer, is the confd protocol version (initially just
-    constants.CONFD_PROTOCOL_VERSION, with a value of 1)
-  - 'status', integer, is the error code. Initially just 0 for 'ok' or
-    '1' for 'error' (in which case answer contains an error detail,
-    rather than an answer), but in the future it may be expanded to have
-    more meanings (eg: 2, the answer is compressed)
-  - 'answer', is the actual answer. Its type and meaning is query
-    specific. For example for "node primary ip by instance ip" queries
+- ``plj0`` the message type magic fourcc, as discussed above
+- ``msg`` contains a JSON-encoded answer, its fields are:
+
+  - ``protocol``, integer, is the confd protocol version (initially
+    just constants.CONFD_PROTOCOL_VERSION, with a value of 1)
+  - ``status``, integer, is the error code; initially just ``0`` for
+    'ok' or ``1`` for 'error' (in which case answer contains an error
+    detail, rather than an answer), but in the future it may be
+    expanded to have more meanings (e.g. ``2`` if the answer is
+    compressed)
+  - ``answer``, is the actual answer; its type and meaning is query
+    specific: for example for "node primary ip by instance ip" queries
     it will be a string containing an IP address, for "node role by
-    name" queries it will be an integer which encodes the role (master,
-    candidate, drained, offline) according to constants.
+    name" queries it will be an integer which encodes the role
+    (master, candidate, drained, offline) according to constants
 
-- 'salt' is the requested salt from the query. A client can use it to
-  recognize what query the answer is answering.
-- 'hmac' is an hmac signature of salt+msg, with the cluster hmac key
+- ``salt`` is the requested salt from the query; a client can use it
+  to recognize what query the answer is answering.
+- ``hmac`` is an hmac signature of salt+msg, with the cluster hmac key
 
 
 Redistribute Config
index 63e5644..629a386 100644 (file)
@@ -11,6 +11,10 @@ Design document drafts
    design-cpu-pinning.rst
    design-ovf-support.rst
    design-network.rst
+   design-node-state-cache.rst
+   design-resource-model.rst
+   design-virtual-clusters.rst
+   design-query-splitting.rst
 
 .. vim: set textwidth=72 :
 .. Local Variables:
index a7318d4..174c236 100644 (file)
@@ -49,9 +49,9 @@ 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.
+NIC slot called ``network``, a new ``Network`` configuration object
+(cluster level) and logic to perform IP address pool management, i.e.
+maintain a set of available and occupied IP addresses.
 
 Configuration changes
 +++++++++++++++++++++
@@ -70,10 +70,15 @@ containing (at least) the following data:
   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.
+Each network will be connected to any number of node groups. During the
+connection of a network to a nodegroup, we define the corresponding
+connectivity mode (bridged or routed) and the host interface (br100 or
+routing_table_200). This is achieved by adding a ``networks`` slot to
+the NodeGroup object and using the networks' UUIDs as keys. The value
+for each key is a dictionary containing the network's ``mode`` and
+``link`` (netparams). Every NIC assigned to the network will eventually
+inherit the network's netparams, as its nicparams.
+
 
 IP pool management
 ++++++++++++++++++
@@ -107,29 +112,34 @@ 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
-++++++++++++++++
+New NIC parameter: network
+++++++++++++++++++++++++++
 
 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.
+compatibility with the current networking model, a new NIC parameter is
+introduced, called ``network`` to reflect the fact that the given NIC
+belongs to the given network and its configuration is managed by Ganeti
+itself. To keep backwards compatibility, existing code is executed if
+the ``network`` value is 'none' or omitted during NIC creation. If we
+want our NIC to be assigned to a network, then only the ip (optional)
+and the network parameters should be passed. Mode and link are inherited
+from the network-nodegroup mapping configuration (netparams). This
+provides the desired abstraction between the VM's network and the
+node-specific underlying infrastructure.
 
 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
+for NICs belonging to a network. A NIC's IP address can also be
+specified manually, as long as it is contained in the network the NIC
 is connected to.
 
 
 Hooks
 +++++
 
+Introduce new hooks concerning network operations:
+
 ``OP_NETWORK_ADD``
   Add a network to Ganeti
 
@@ -137,83 +147,93 @@ Hooks
   :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.
+``OP_NETWORK_REMOVE``
+  Remove a network from Ganeti
+
+  :directory: network-remove
+  :pre-execution: master node
+  :post-execution: master node
 
-  :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_SET_PARAMS``
+  Modify a network
 
-``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-modify
+  :pre-execution: master node
+  :post-execution: master node
 
-  :directory: network-disconnect
-  :pre-execution: master node, all nodes in the connected group
-  :post-execution: master node, all nodes in the connected group
+For connect/disconnect operations use existing:
 
-``OP_NETWORK_REMOVE``
-  Remove a network from Ganeti
+``OP_GROUP_SET_PARAMS``
+  Modify a nodegroup
 
-  :directory: network-add
-  :pre-execution: master node, all nodes
-  :post-execution: master node, all nodes
+  :directory: group-modify
+  :pre-execution: master node
+  :post-execution: master node
 
 Hook variables
 ^^^^^^^^^^^^^^
 
-``INSTANCE_NICn_MANAGED``
-  Non-zero if NIC n is a managed-mode NIC
+During instance related operations:
 
 ``INSTANCE_NICn_NETWORK``
   The friendly name of the network
 
-``INSTANCE_NICn_NETWORK_UUID``
-  The network's UUID
+During network related operations:
 
-``INSTANCE_NICn_NETWORK_TAGS``
-  The network's tags
+``NETWORK_NAME``
+  The friendly name of the network
+
+``NETWORK_SUBNET``
+  The ip range of the network
 
-``INSTANCE_NICn_NETWORK_IPV4_CIDR``, ``INSTANCE_NICn_NETWORK_IPV6_CIDR``
-  The subnet in CIDR notation
+``NETWORK_GATEWAY``
+  The gateway of the network
 
-``INSTANCE_NICn_NETWORK_IPV4_GATEWAY``, ``INSTANCE_NICn_NETWORK_IPV6_GATEWAY``
-  The subnet's default gateway
+During nodegroup related operations:
+
+``GROUP_NETWORK``
+  The friendly name of the network
 
+``GROUP_NETWORK_MODE``
+  The mode (bridged or routed) of the netparams
+
+``GROUP_NETWORK_LINK``
+  The link of the netparams
 
 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.
+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 mode and link
+replaced by the *connectivity mode* and *host interface* (netparams) 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 scripts detect if a NIC is assigned to a Network
+by the presence of the new environment variable:
 
 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
+Conflicting IPs
++++++++++++++++
+
+To ensure IP uniqueness inside a nodegroup, we introduce the term
+``conflicting ips``. Conflicting IPs occur: (a) when creating a
+networkless NIC with IP contained in a network already connected to the
+instance's nodegroup  (b) when connecting/disconnecting a network
+to/from a nodegroup and at the same time instances with IPs inside the
+network's range still exist. Conflicting IPs produce prereq errors.
 
-``NETWORK_TAGS``
-  The network's tags
+Handling of conflicting IP with --force option:
 
-``NETWORK_IPv4_CIDR``, ``NETWORK_IPv6_CIDR``
-  The subnet in CIDR notation
+For case (a) reserve the IP and assign the NIC to the Network.
+For case (b) during connect same as (a), during disconnect release IP and
+reset NIC's network parameter to None
 
-``NETWORK_IPV4_GATEWAY``, ``NETWORK_IPV6_GATEWAY``
-  The subnet's default gateway
 
 Userland interface
 ++++++++++++++++++
@@ -225,77 +245,68 @@ 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
+ gnt-network add --network=192.168.100.0/28 --gateway=192.168.100.1 \
+                 --network6=2001:db8:2ffc::/64 --gateway6=2001:db8:2ffc::1 \
+                 --reserved-ips=192.168.100.10,192.168.100.11 net100
+  (Checks for already exising name and valid IP values)
+ gnt-network remove network_name
+  (Checks if not connected to any nodegroup)
 
 
 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
+ gnt-network modify --gateway=192.168.100.5 net100
+  (Changes the gateway only if ip is available)
+ gnt-network modify --reserved-ips=192.168.100.11 net100
+  (Toggles externally reserved ip)
 
 
 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)
+ gnt-network connect net100 nodegroup1 bridged br100
+  (Checks for existing bridge among nodegroup)
+ gnt-network connect net100 nodegroup2 routed rt_table
+  (Checks for conflicting IPs)
+ gnt-network disconnect net101 nodegroup1
+  (Checks for conflicting IPs)
 
-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      Subnet           Gateway       NodeGroups GroupList
+ net100       192.168.100.0/28 192.168.100.1          1 default(bridged, br100)
+ net101       192.168.101.0/28 192.168.101.1          1 default(routed, rt_tab)
 
 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
-   ..
+ gnt-network info testnet1
+
+ Network name: testnet1
+  subnet: 192.168.100.0/28
+  gateway: 192.168.100.1
+  size: 16
+  free: 10 (62.50%)
+  usage map:
+        0 XXXXX..........X                                                 63
+          (X) used    (.) free
+  externally reserved IPs:
+    192.168.100.0, 192.168.100.1, 192.168.100.15
+  connected to node groups:
+    default(bridged, br100)
+  used by 3 instances:
+    test1 : 0:192.168.100.4
+    test2 : 0:192.168.100.2
+    test3 : 0:192.168.100.3
 
 
 IAllocator changes
diff --git a/doc/design-node-state-cache.rst b/doc/design-node-state-cache.rst
new file mode 100644 (file)
index 0000000..28218ef
--- /dev/null
@@ -0,0 +1,146 @@
+================
+Node State Cache
+================
+
+.. contents:: :depth: 4
+
+This is a design doc about the optimization of machine info retrieval.
+
+
+Current State
+=============
+
+Currently every RPC call is quite expensive as a TCP handshake has to be
+made as well as SSL negotiation. This especially is visible when getting
+node and instance info over and over again.
+
+This data, however, is quite easy to cache but needs some changes to how
+we retrieve data in the RPC as this is spread over several RPC calls
+and are hard to unify.
+
+
+Proposed changes
+================
+
+To overcome this situation with multiple information retrieval calls we
+introduce one single RPC call to get all the info in a organized manner,
+for easy store in the cache.
+
+As of now we have 3 different information RPC calls:
+
+- ``call_node_info``: To retrieve disk and hyper-visor information
+- ``call_instance_info``: To retrieve hyper-visor information for one
+  instance
+- ``call_all_instance_info``: To retrieve hyper-visor information for
+  all instances
+
+Not to mention that ``call_all_instance_info`` and
+``call_instance_info`` return different information in the dict.
+
+To unify the data and organize them we introduce a new RPC call
+``call_node_snapshot`` doing all of the above in one go. Which
+data we want to know will be specified about a dict of request
+types: CACHE_REQ_HV, CACHE_REQ_DISKINFO, CACHE_REQ_BOOTID
+
+As this cache is representing the state of a given node we use the
+name of a node as the key to retrieve the data from the cache. A
+name-space separation of node and instance data is not possible at the
+current point. This is due to the fact that some of the node hyper-visor
+information like free memory is correlating with instances running.
+
+An example of how the data for a node in the cache looks like::
+
+  {
+    constants.CACHE_REQ_HV: {
+      constants.HT_XEN_PVM: {
+        _NODE_DATA: {
+          "memory_total": 32763,
+          "memory_free": 9159,
+          "memory_dom0": 1024,
+          "cpu_total": 4,
+          "cpu_sockets": 2
+        },
+        _INSTANCES_DATA: {
+          "inst1": {
+            "memory": 4096,
+            "state": "-b----",
+            "time": 102399.3,
+            "vcpus": 1
+          },
+          "inst2": {
+            "memory": 4096,
+            "state": "-b----",
+            "time": 12280.0,
+            "vcpus": 3
+          }
+        }
+      }
+    },
+    constants.CACHE_REQ_DISKINFO: {
+      "xenvg": {
+        "vg_size": 1048576,
+        "vg_free": 491520
+      },
+    }
+    constants.CACHE_REQ_BOOTID: "0dd0983c-913d-4ce6-ad94-0eceb77b69f9"
+  }
+
+This way we get easy to organize information which can simply be arranged in
+the cache.
+
+The 3 RPC calls mentioned above will remain for compatibility reason but
+will be simple wrappers around this RPC call.
+
+
+Cache invalidation
+------------------
+
+The cache is invalidated at every RPC call which is not proven to not
+modify the state of a given node. This is to avoid inconsistency between
+cache and actual node state.
+
+There are some corner cases which invalidates the whole cache at once as
+they usually affect other nodes states too:
+
+ - migrate/failover
+ - import/export
+
+A request will be served from the cache if and only if it can be
+fulfilled entirely from it (i.e. all the CACHE_REQ_* entries are already
+present). Otherwise, we will invalidate the cache and actually do the
+remote call.
+
+In addition, every cache entry will have a TTL of about 10 minutes which
+should be enough to accommodate most use cases.
+
+We also allow an option to the calls to bypass the cache completely and
+do a force remote call. However, this will invalidate the present
+entries and populate the cache with the new retrieved values.
+
+
+Additional cache population
+---------------------------
+
+Besides of the commands which calls above RPC calls, a full cache
+population can also be done by a separate new op-code run by
+``ganeti-watcher`` periodically. This op-code will be used instead of
+the old ones.
+
+
+Possible regressions
+====================
+
+As we change from getting "one hyper-visor information" to "get all we
+know about this hyper-visor"-style we have a regression in time of
+execution. The execution time is about 1.8x more in process execution
+time. However, this does not include the latency and negotiation time
+needed for each separate RPC call. Also if we hit the cache all 3 costs
+will be 0. The only time taken is to look up the info in the cache and
+the deserialization of the data. Which takes down the time from today
+~300ms to ~100ms.
+
+.. vim: set textwidth=72 :
+.. Local Variables:
+.. mode: rst
+.. fill-column: 72
+.. End:
index f6aebbc..78e468b 100644 (file)
@@ -4,42 +4,47 @@ Ganeti Node OOB Management Framework
 Objective
 ---------
 
-Extend Ganeti with Out of Band Cluster Node Management Capabilities.
+Extend Ganeti with Out of Band (:term:`OOB`) Cluster Node Management
+Capabilities.
 
 Background
 ----------
 
-Ganeti currently has no support for Out of Band management of the nodes in a
-cluster. It relies on the OS running on the nodes and has therefore limited
-possibilities when the OS is not responding. The command ``gnt-node powercycle``
-can be issued to attempt a reboot of a node that crashed but there are no means
-to power a node off and power it back on. Supporting this is very handy in the
-following situations:
-
-  * **Emergency Power Off**: During emergencies, time is critical and manual
-    tasks just add latency which can be avoided through automation. If a server
-    room overheats, halting the OS on the nodes is not enough. The nodes need
-    to be powered off cleanly to prevent damage to equipment.
-  * **Repairs**: In most cases, repairing a node means that the node has to be
-    powered off.
-  * **Crashes**: Software bugs may crash a node. Having an OS independent way to
-    power-cycle a node helps to recover the node without human intervention.
+Ganeti currently has no support for Out of Band management of the nodes
+in a cluster. It relies on the OS running on the nodes and has therefore
+limited possibilities when the OS is not responding. The command
+``gnt-node powercycle`` can be issued to attempt a reboot of a node that
+crashed but there are no means to power a node off and power it back
+on. Supporting this is very handy in the following situations:
+
+  * **Emergency Power Off**: During emergencies, time is critical and
+    manual tasks just add latency which can be avoided through
+    automation. If a server room overheats, halting the OS on the nodes
+    is not enough. The nodes need to be powered off cleanly to prevent
+    damage to equipment.
+  * **Repairs**: In most cases, repairing a node means that the node has
+    to be powered off.
+  * **Crashes**: Software bugs may crash a node. Having an OS
+    independent way to power-cycle a node helps to recover the node
+    without human intervention.
 
 Overview
 --------
 
-Ganeti will be extended with OOB capabilities through adding a new **Cluster
-Parameter** (``--oob-program``), a new **Node Property** (``--oob-program``), a
-new **Node State (powered)** and support in ``gnt-node`` for invoking an
-**External Helper Command** which executes the actual OOB command (``gnt-node
-<command> nodename ...``). The supported commands are: ``power on``,
-``power off``, ``power cycle``, ``power status`` and ``health``.
+Ganeti will be extended with OOB capabilities through adding a new
+**Cluster Parameter** (``--oob-program``), a new **Node Property**
+(``--oob-program``), a new **Node State (powered)** and support in
+``gnt-node`` for invoking an **External Helper Command** which executes
+the actual OOB command (``gnt-node <command> nodename ...``). The
+supported commands are: ``power on``, ``power off``, ``power cycle``,
+``power status`` and ``health``.
 
 .. note::
-  The new **Node State (powered)** is a **State of Record
-  (SoR)**, not a **State of World (SoW)**.  The maximum execution time of the
-  **External Helper Command** will be limited to 60s to prevent the cluster from
-  getting locked for an undefined amount of time.
+  The new **Node State (powered)** is a **State of Record**
+  (:term:`SoR`), not a **State of World** (:term:`SoW`).  The maximum
+  execution time of the **External Helper Command** will be limited to
+  60s to prevent the cluster from getting locked for an undefined amount
+  of time.
 
 Detailed Design
 ---------------
@@ -64,19 +69,20 @@ New ``gnt-cluster epo`` Command
 |          ``--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.
+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.
+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.
+  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
 +++++++++++++++++++++++++
@@ -87,9 +93,10 @@ New ``gnt-node`` Property
 | Options: ``--oob-program``: executable OOB program (absolute path)
 
 .. note::
-  If ``--oob-program`` is set to ``!`` then the node has no OOB capabilities.
-  Otherwise, we will inherit the node group respectively the cluster wide
-  value. I.e. the nodes have to opt out from OOB capabilities.
+  If ``--oob-program`` is set to ``!`` then the node has no OOB
+  capabilities.  Otherwise, we will inherit the node group respectively
+  the cluster wide value. I.e. the nodes have to opt out from OOB
+  capabilities.
 
 Addition to ``gnt-cluster verify``
 ++++++++++++++++++++++++++++++++++
@@ -100,12 +107,12 @@ Addition to ``gnt-cluster verify``
 | Option: None
 | Additional Checks:
 
-  1. existence and execution flag of OOB program on all Master Candidates if
-     the cluster parameter ``--oob-program`` is set or at least one node has
-     the property ``--oob-program`` set. The OOB helper is just invoked on the
-     master
-  2. check if node state powered matches actual power state of the machine for
-     those nodes where ``--oob-program`` is set
+  1. existence and execution flag of OOB program on all Master
+     Candidates if the cluster parameter ``--oob-program`` is set or at
+     least one node has the property ``--oob-program`` set. The OOB
+     helper is just invoked on the master
+  2. check if node state powered matches actual power state of the
+     machine for those nodes where ``--oob-program`` is set
 
 New Node State
 ++++++++++++++
@@ -113,26 +120,27 @@ New Node State
 Ganeti supports the following two boolean states related to the nodes:
 
 **drained**
-  The cluster still communicates with drained nodes but excludes them from
-  allocation operations
+  The cluster still communicates with drained nodes but excludes them
+  from allocation operations
 
 **offline**
-  if offline, the cluster does not communicate with offline nodes; useful for
-  nodes that are not reachable in order to avoid delays
+  if offline, the cluster does not communicate with offline nodes;
+  useful for nodes that are not reachable in order to avoid delays
 
 And will extend this list with the following boolean state:
 
 **powered**
-  if not powered, the cluster does not communicate with not powered nodes if
-  the node property ``--oob-program`` is not set, the state powered is not
-  displayed
+  if not powered, the cluster does not communicate with not powered
+  nodes if the node property ``--oob-program`` is not set, the state
+  powered is not displayed
 
 Additionally modify the meaning of the offline state as follows:
 
 **offline**
-  if offline, the cluster does not communicate with offline nodes (**with the
-  exception of OOB commands for nodes where** ``--oob-program`` **is set**);
-  useful for nodes that are not reachable in order to avoid delays
+  if offline, the cluster does not communicate with offline nodes
+  (**with the exception of OOB commands for nodes where**
+  ``--oob-program`` **is set**); useful for nodes that are not reachable
+  in order to avoid delays
 
 The corresponding command extensions are:
 
@@ -141,14 +149,15 @@ The corresponding command extensions are:
 | Parameter:  [ ``nodename`` ... ]
 | Option: None
 
-Additional Output (SoR, ommited if node property ``--oob-program`` is not set):
+Additional Output (:term:`SoR`, ommited if node property
+``--oob-program`` is not set):
 powered: ``[True|False]``
 
 | Program: ``gnt-node``
 | Command: ``modify``
 | Parameter: nodename
 | Option: [ ``--powered=yes|no`` ]
-| Reasoning: sometimes you will need to sync the SoR with the SoW manually
+| Reasoning: sometimes you will need to sync the :term:`SoR` with the :term:`SoW` manually
 | Caveat: ``--powered`` can only be modified if ``--oob-program`` is set for
 |         the node in question
 
@@ -161,76 +170,78 @@ New ``gnt-node`` commands: ``power [on|off|cycle|status]``
 | Options: None
 | Caveats:
 
-  * If no nodenames are passed to ``power [on|off|cycle]``, the user will be
-    prompted with ``"Do you really want to power [on|off|cycle] the following
-    nodes: <display list of OOB capable nodes in the cluster)? (y/n)"``
+  * If no nodenames are passed to ``power [on|off|cycle]``, the user
+    will be prompted with ``"Do you really want to power [on|off|cycle]
+    the following nodes: <display list of OOB capable nodes in the
+    cluster)? (y/n)"``
   * For ``power-status``, nodename is optional, if omitted, we list the
-    power-status of all OOB capable nodes in the cluster (SoW)
+    power-status of all OOB capable nodes in the cluster (:term:`SoW`)
   * User should be warned and needs to confirm with yes if s/he tries to
     ``power [off|cycle]`` a node with running instances.
 
 Error Handling
 ^^^^^^^^^^^^^^
 
-+------------------------------+-----------------------------------------------+
-| Exception                    | Error Message                                 |
-+==============================+===============================================+
-| OOB program return code != 0 | OOB program execution failed ($ERROR_MSG)     |
-+------------------------------+-----------------------------------------------+
-| OOB program execution time   | OOB program execution timeout exceeded, OOB   |
-| exceeds 60s                  | program execution aborted                     |
-+------------------------------+-----------------------------------------------+
++-----------------------------+----------------------------------------------+
+| Exception                   | Error Message                                |
++=============================+==============================================+
+| OOB program return code != 0| OOB program execution failed ($ERROR_MSG)    |
++-----------------------------+----------------------------------------------+
+| OOB program execution time  | OOB program execution timeout exceeded, OOB  |
+| exceeds 60s                 | program execution aborted                    |
++-----------------------------+----------------------------------------------+
 
 Node State Changes
 ^^^^^^^^^^^^^^^^^^
 
-+----------------+-----------------+----------------+--------------------------+
-| State before   | Command         | State after    | Comment                  |
-| execution      |                 | execution      |                          |
-+================+=================+================+==========================+
-| powered: False | ``power off``   | powered: False | FYI: IPMI will complain  |
-|                |                 |                | if you try to power off  |
-|                |                 |                | a machine that is already|
-|                |                 |                | powered off              |
-+----------------+-----------------+----------------+--------------------------+
-| powered: False | ``power cycle`` | powered: False | FYI: IPMI will complain  |
-|                |                 |                | if you try to cycle a    |
-|                |                 |                | machine that is already  |
-|                |                 |                | powered off              |
-+----------------+-----------------+----------------+--------------------------+
-| powered: False | ``power on``    | powered: True  |                          |
-+----------------+-----------------+----------------+--------------------------+
-| powered: True  | ``power off``   | powered: False |                          |
-+----------------+-----------------+----------------+--------------------------+
-| powered: True  | ``power cycle`` | powered: True  |                          |
-+----------------+-----------------+----------------+--------------------------+
-| powered: True  | ``power on``    | powered: True  | FYI: IPMI will complain  |
-|                |                 |                | if you try to power on   |
-|                |                 |                | a machine that is already|
-|                |                 |                | powered on               |
-+----------------+-----------------+----------------+--------------------------+
++----------------+---------------+----------------+--------------------------+
+| State before   |Command        | State after    | Comment                  |
+| execution      |               | execution      |                          |
++================+===============+================+==========================+
+| powered: False |``power off``  | powered: False | FYI: IPMI will complain  |
+|                |               |                | if you try to power off  |
+|                |               |                | a machine that is already|
+|                |               |                | powered off              |
++----------------+---------------+----------------+--------------------------+
+| powered: False |``power cycle``| powered: False | FYI: IPMI will complain  |
+|                |               |                | if you try to cycle a    |
+|                |               |                | machine that is already  |
+|                |               |                | powered off              |
++----------------+---------------+----------------+--------------------------+
+| powered: False |``power on``   | powered: True  |                          |
++----------------+---------------+----------------+--------------------------+
+| powered: True  |``power off``  | powered: False |                          |
++----------------+---------------+----------------+--------------------------+
+| powered: True  |``power cycle``| powered: True  |                          |
++----------------+---------------+----------------+--------------------------+
+| powered: True  |``power on``   | powered: True  | FYI: IPMI will complain  |
+|                |               |                | if you try to power on   |
+|                |               |                | a machine that is already|
+|                |               |                | powered on               |
++----------------+---------------+----------------+--------------------------+
 
 .. note::
 
   * If the command fails, the Node State remains unchanged.
   * We will not prevent the user from trying to power off a node that is
-    already powered off since the powered state represents the **SoR** only and
-    not the **SoW**. This can however create problems when the cluster
-    administrator wants to bring the **SoR** in sync with the **SoW** without
-    actually having to mess with the node(s). For this case, we allow direct
-    modification of the powered state through the gnt-node modify
-    ``--powered=[yes|no]`` command as long as the node has OOB capabilities
-    (i.e. ``--oob-program`` is set).
+    already powered off since the powered state represents the
+    :term:`SoR` only and not the :term:`SoW`. This can however create
+    problems when the cluster administrator wants to bring the
+    :term:`SoR` in sync with the :term:SoW` without actually having to
+    mess with the node(s). For this case, we allow direct modification
+    of the powered state through the gnt-node modify
+    ``--powered=[yes|no]`` command as long as the node has OOB
+    capabilities (i.e. ``--oob-program`` is set).
   * All node power state changes will be logged
 
-Node Power Status Listing (SoW)
-+++++++++++++++++++++++++++++++
+Node Power Status Listing (:term:`SoW`)
++++++++++++++++++++++++++++++++++++++++
 
 | Program: ``gnt-node``
 | Command: ``power-status``
 | Parameters: [ ``nodename`` ... ]
 
-Example output (represents **SoW**)::
+Example output (represents :term:`SoW`)::
 
   gnt-node oob power-status
   Node                      Power Status
@@ -241,23 +252,24 @@ Example output (represents **SoW**)::
 
 .. note::
 
-  * We use ``unknown`` in case the Helper Program could not determine the power
-    state.
-  * If no nodenames are provided, we will list the power state of all nodes
-    which are not opted out from OOB management.
-  * Only nodes which are not opted out from OOB management will be listed.
-    Invoking the command on a node that does not meet this condition will
-    result in an error message "Node X does not support OOB commands".
+  * We use ``unknown`` in case the Helper Program could not determine
+    the power state.
+  * If no nodenames are provided, we will list the power state of all
+    nodes which are not opted out from OOB management.
+  * Only nodes which are not opted out from OOB management will be
+    listed.  Invoking the command on a node that does not meet this
+    condition will result in an error message "Node X does not support
+    OOB commands".
 
-Node Power Status Listing (SoR)
-+++++++++++++++++++++++++++++++
+Node Power Status Listing (:term:`SoR`)
++++++++++++++++++++++++++++++++++++++++
 
 | Program: ``gnt-node``
 | Command: ``info``
 | Parameter:  [ ``nodename`` ... ]
 | Option: None
 
-Example output (represents **SoR**)::
+Example output (represents :term:`SoR`)::
 
   gnt-node info node1.example.com
   Node name: node1.example.com
@@ -278,8 +290,8 @@ Example output (represents **SoR**)::
       - inst7.example.com
 
 .. note::
-  Only nodes which are not opted out from OOB management will
-  report the powered state.
+  Only nodes which are not opted out from OOB management will report the
+  powered state.
 
 New ``gnt-node`` oob subcommand: ``health``
 +++++++++++++++++++++++++++++++++++++++++++
@@ -292,11 +304,12 @@ New ``gnt-node`` oob subcommand: ``health``
 
 Caveats:
 
-  * If no nodename(s) are provided, we will report the health of all nodes in
-    the cluster which have ``--oob-program`` set.
-  * Only nodes which are not opted out from OOB management will report their
-    health. Invoking the command on a node that does not meet this condition
-    will result in an error message "Node does not support OOB commands".
+  * If no nodename(s) are provided, we will report the health of all
+    nodes in the cluster which have ``--oob-program`` set.
+  * Only nodes which are not opted out from OOB management will report
+    their health. Invoking the command on a node that does not meet this
+    condition will result in an error message "Node does not support OOB
+    commands".
 
 For error handling see `Error Handling`_
 
@@ -313,79 +326,81 @@ OOB Program (Helper Program) Parameters, Return Codes and Data Format
 Return Codes
 ^^^^^^^^^^^^
 
-+---------------+--------------------------+
-| Return code   | Meaning                  |
-+===============+==========================+
-| 0             | Command succeeded        |
-+---------------+--------------------------+
-| 1             | Command failed           |
-+---------------+--------------------------+
-| others        | Unsupported/undefined    |
-+---------------+--------------------------+
-
-Error messages are passed from the helper program to Ganeti through StdErr
-(return code == 1).  On StdOut, the helper program will send data back to
-Ganeti (return code == 0). The format of the data is JSON.
-
-+------------------+-------------------------------+
-| Command          | Expected output               |
-+==================+===============================+
-| ``power-on``     | None                          |
-+------------------+-------------------------------+
-| ``power-off``    | None                          |
-+------------------+-------------------------------+
-| ``power-cycle``  | None                          |
-+------------------+-------------------------------+
-| ``power-status`` | ``{ "powered": true|false }`` |
-+------------------+-------------------------------+
-| ``health``       | ::                            |
-|                  |                               |
-|                  |   [[item, status],            |
-|                  |    [item, status],            |
-|                  |    ...]                       |
-+------------------+-------------------------------+
++-------------+-------------------------+
+| Return code | Meaning                 |
++=============+=========================+
+| 0           | Command succeeded       |
++-------------+-------------------------+
+| 1           | Command failed          |
++-------------+-------------------------+
+| others      | Unsupported/undefined   |
++-------------+-------------------------+
+
+Error messages are passed from the helper program to Ganeti through
+:manpage:`stderr(3)` (return code == 1).  On :manpage:`stdout(3)`, the
+helper program will send data back to Ganeti (return code == 0). The
+format of the data is JSON.
+
++-----------------+------------------------------+
+| Command         | Expected output              |
++=================+==============================+
+| ``power-on``    | None                         |
++-----------------+------------------------------+
+| ``power-off``   | None                         |
++-----------------+------------------------------+
+| ``power-cycle`` | None                         |
++-----------------+------------------------------+
+| ``power-status``| ``{ "powered": true|false }``|
++-----------------+------------------------------+
+| ``health``      | ::                           |
+|                 |                              |
+|                 |   [[item, status],           |
+|                 |    [item, status],           |
+|                 |    ...]                      |
++-----------------+------------------------------+
 
 Data Format
 ^^^^^^^^^^^
 
 For the health output, the fields are:
 
-+--------+--------------------------------------------------------------------+
-| Field  | Meaning                                                            |
-+========+====================================================================+
-| item   | String identifier of the item we are querying the health of,       |
-|        | examples:                                                          |
-|        |                                                                    |
-|        |   * Ambient Temp                                                   |
-|        |   * PS Redundancy                                                  |
-|        |   * FAN 1 RPM                                                      |
-+--------+--------------------------------------------------------------------+
-| status | String; Can take one of the following four values:                 |
-|        |                                                                    |
-|        |   * OK                                                             |
-|        |   * WARNING                                                        |
-|        |   * CRITICAL                                                       |
-|        |   * UNKNOWN                                                        |
-+--------+--------------------------------------------------------------------+
++--------+------------------------------------------------------------------+
+| Field  | Meaning                                                          |
++========+==================================================================+
+| item   | String identifier of the item we are querying the health of,     |
+|        | examples:                                                        |
+|        |                                                                  |
+|        |   * Ambient Temp                                                 |
+|        |   * PS Redundancy                                                |
+|        |   * FAN 1 RPM                                                    |
++--------+------------------------------------------------------------------+
+| status | String; Can take one of the following four values:               |
+|        |                                                                  |
+|        |   * OK                                                           |
+|        |   * WARNING                                                      |
+|        |   * CRITICAL                                                     |
+|        |   * UNKNOWN                                                      |
++--------+------------------------------------------------------------------+
 
 .. note::
 
-  * The item output list is defined by the Helper Program. It is up to the
-    author of the Helper Program to decide which items should be monitored and
-    what each corresponding return status is.
-  * Ganeti will currently not take any actions based on the item status. It
-    will however create log entries for items with status WARNING or CRITICAL
-    for each run of the ``gnt-node oob health nodename`` command. Automatic
-    actions (regular monitoring of the item status) is considered a new service
-    and will be treated in a separate design document.
+  * The item output list is defined by the Helper Program. It is up to
+    the author of the Helper Program to decide which items should be
+    monitored and what each corresponding return status is.
+  * Ganeti will currently not take any actions based on the item
+    status. It will however create log entries for items with status
+    WARNING or CRITICAL for each run of the ``gnt-node oob health
+    nodename`` command. Automatic actions (regular monitoring of the
+    item status) is considered a new service and will be treated in a
+    separate design document.
 
 Logging
 -------
 
-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``.
+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:
index 060d476..1b972ae 100644 (file)
@@ -38,14 +38,14 @@ 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.
+There are no limitations regarding disk images used, as long as the
+description is provided. Any hardware described in a proper format
+(i.e. CIM - Common Information Model) 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):
+OVF package should contain exactly one file with ``.ovf`` extension,
+which is an XML file specifying the following (per virtual machine):
 
 - virtual disks
 - network description
@@ -58,12 +58,19 @@ 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).
 
+In order to provide secure means of distribution for OVF packages, the
+manifest and certificate are provided. Manifest (``.mf`` file) contains
+checksums for all the files in OVF package, whereas certificate
+(``.cert`` file) contains X.509 certificate and a checksum of manifest
+file. Both files are not compulsory, but certificate requires manifest
+to be present.
+
 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.
+interested in is which formats are supported by VM managers that
+currently use OVF.
 
 - VMWare: ``.vmdk`` (which comes in at least 3 different flavours:
   ``sparse``, ``compressed`` and ``streamOptimized``)
@@ -74,24 +81,20 @@ that currently use OVF.
 - Red Hat Enterprise Virtualization: ``.raw`` (raw disk format),
   ``.cow`` (qemu's ``QCOW2``)
 - other: AbiCloud, OpenNode Cloud, SUSE Studio, Morfeo Claudia,
-  OpenStack
+  OpenStack: mostly ``.vmdk``
 
-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:
+In our implementation of the OVF we allow a choice between raw, cow and
+vmdk disk formats for both import and export. Other formats covertable
+using ``qemu-img`` are allowed in import mode, but not tested.
+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?]
+- Cow is used in Qemu
 - 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.
-
 Import and export - the closer look
 ===================================
 
@@ -119,11 +122,21 @@ The basic structure of Ganeti ``.ovf`` file is the following::
             <gnt:VersionId/>
             <gnt:AutoBalance/>
             <gnt:Tags></gnt:Tags>
-            <gnt:OSParameters></gnt:OSParameters>
+            <gnt:DiskTemplate</gnt:DiskTemplate>
+            <gnt:OperatingSystem>
+                <gnt:Name/>
+                <gnt:Parameters></gnt:Parameters>
+            </gnt:OperatingSystem>
             <gnt:Hypervisor>
-                <gnt:HypervisorParameters>
-                </gnt:HypervisorParameters>
+                <gnt:Name/>
+                <gnt:Parameters></gnt:Parameters>
             </gnt:Hypervisor>
+            <gnt:Network>
+            <gnt:Mode/>
+            <gnt:MACAddress/>
+            <gnt:Link/>
+            <gnt:IPAddress/>
+            </gnt:Network>
         </gnt:GanetiSection>
     </Envelope>
 
@@ -137,18 +150,19 @@ Whereas Ganeti's export info is of the following form, ``=>`` showing
 where will the data be in OVF format::
 
   [instance]
-      disk0_dump = filename     => References
-      disk0_ivname = name       => ignored
-      disk0_size = size_in_mb   => DiskSection
-      disk_count = number       => ignored
-      disk_template = disk_type => References
-      hypervisor = hyp-name     => gnt:HypervisorSection
+      disk0_dump = filename     => File in References
+      disk0_ivname = name       => generated automatically
+      disk0_size = size_in_mb   => calculated after disk conversion
+      disk_count = number       => generated automatically
+      disk_template = disk_type => gnt:DiskTemplate
+      hypervisor = hyp-name     => gnt:Name in gnt:Hypervisor
       name = inst-name          => Name in VirtualSystem
-      nic0_ip = ip              => Item in VirtualHardwareSection
-      nic0_link = link          => Item in VirtualHardwareSection
-      nic0_mac = mac            => Item in VirtualHardwareSection
-      nic0_mode = mode          => Network in NetworkSection
-      nic_count = number        => ignored
+      nic0_ip = ip              => gnt:IPAddress in gnt:Network
+      nic0_link = link          => gnt:Link in gnt:Network
+      nic0_mac = mac            => gnt:MACAddress in gnt:Network or
+                                   Item in VirtualHardwareSection
+      nic0_mode = mode          => gnt:Mode in gnt:Network
+      nic_count = number        => generated automatically
       tags                      => gnt:Tags
 
   [backend]
@@ -157,15 +171,16 @@ where will the data be in OVF format::
       vcpus = number            => Item in VirtualHardwareSection
 
   [export]
-      compression               => DiskSection
-      os                        => OperatingSystemSection
+      compression              => ignored
+      os                        => gnt:Name in gnt:OperatingSystem
       source                    => ignored
       timestamp                 => ignored
-      version                   => gnt:VersionId
+      version                   => gnt:VersionId or
+                                   constants.EXPORT_VERSION
 
-  [os]                          => gnt:OSParameters
+  [os]                          => gnt:Parameters in gnt:OperatingSystem
 
-  [hypervisor]                  => gnt:HypervisorParameters
+  [hypervisor]                  => gnt:Parameters in gnt:Hypervisor
 
 In case of multiple networks/disks used by an instance, they will
 all be saved in appropriate sections as specified above for the first
@@ -178,10 +193,11 @@ e.g. VirtualBox, some fields required for Ganeti to properly handle
 import may be missing. Most often it will happen that such OVF package
 will lack the ``gnt:GanetiSection``.
 
-If this happens, the tool will simply ask for all the necessary
-information or otherwise you can specify all the missing parameters in
-the command line. For the latter, please refer to [TODO: reference to
-command line options]
+If this happens you can specify all the missing parameters in
+the command line. Please refer to `Command Line`_ section.
+
+In the :doc:`ovfconverter` we provide examples of
+options when converting from VirtualBox, VMWare and OpenSuseStudio.
 
 Export to other virtualization software
 ---------------------------------------
@@ -194,8 +210,8 @@ instance. If that is the case please do one of the two:
 cause to skip the non-standard information.
 
 2. Manually remove the gnt:GanetiSection from the ``.ovf`` file. You
-will also have to recompute sha1 sum (``sha1sum`` command) and update
-your ``.mf`` file with new value.
+will also have to recompute sha1 sum (``sha1sum`` command) of the .ovf
+file and update your ``.mf`` file with new value.
 
 .. note::
     Manual change option is only recommended when you have exported your
@@ -209,9 +225,8 @@ 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.
+unsupported element will be to ignore this element's tag without
+interruption of the import process.
 
 Package
 -------
@@ -233,42 +248,272 @@ 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``.
+As mentioned, Ganeti will allow export in  ``raw``, ``cow`` and ``vmdk``
+formats.  This means i.e. that the appropriate ``ovf:format``
+will be provided.
+As for import, we will support all formats that ``qemu-img`` can convert
+to ``raw``. At this point this means ``raw``, ``cow``, ``qcow``,
+``qcow2``, ``vmdk`` and ``cloop``.  We do not plan for now to support
+``vdi`` or ``vhd`` unless they become part of qemu-img supported formats.
 
-We plan to support compression both for import and export - in tar.gz
+We plan to support compression both for import and export - in gzip
 format. There is also a possibility to provide virtual disk in chunks
-of equal size.
+of equal size. The latter will not be implemented in the first version,
+but we do plan to support it eventually.
+
 
-When no ``ovf:format`` tag is provided during import, we assume that
-the disk is to be created on import and proceed accordingly.
+The ``ovf:format`` tag is not used in our case when importing. Instead
+we use ``qemu-img info``, which provides enough information for our
+purposes and is better standardized.
+
+Please note, that due to security reasons we require the disk image to
+be in the same directory as the ``.ovf`` description file for both
+import and export.
+
+In order to completely ignore disk-related information in resulting
+config file, please use ``--disk-template=diskless`` option.
 
 Network
 -------
 
-There are no known limitations regarding network support.
+Ganeti provides support for routed and bridged mode for the networks.
+Since the standard OVF format does not contain any information regarding
+used network type, we add our own source of such information in
+``gnt:GanetiSection``. In case this additional information is not
+present, we perform a simple check - if network name specified in
+``NetworkSection`` contains words ``bridged`` or ``routed``, we consider
+this to be the network type. Otherwise option ``auto`` is chosen, in
+which case the cluster's default value for that field will be used when
+importing.
+This provides a safe fallback in case of NAT networks usage, which are
+commonly used e.g. in VirtualBox.
 
 Hardware
 --------
 
-TODO
+The supported hardware is limited to virtual CPUs, RAM memory, disks and
+networks. In particular, no USB support is currently provided, as Ganeti
+does not support them.
 
 Operating Systems
 -----------------
 
-TODO
+Support for different operating systems depends solely on their
+accessibility for Ganeti instances. List of installed OSes can be
+checked using ``gnt-os list`` command.
+
+References
+----------
+
+Files listed in ``ovf:References`` section cannot be hyperlinks.
 
 Other
 -----
 
+The instance name (``gnt:VirtualSystem\gnt:Name`` or command line's
+``--name`` option ) has to be resolvable in order for successful import
+using ``gnt-backup import``.
+
+
+_`Command Line`
+===============
+
+The basic usage of the ovf tool is one of the following::
+
+    ovfconverter import filename
+    ovfconverter export --format=<format> filename
+
+This will result in a conversion based solely on the content of provided
+file. In case some information required to make the conversion is
+missing, an error will occur.
+
+If output directory should be different than the standard Ganeti export
+directory (usually ``/srv/ganeti/export``), option ``--output-dir``
+can be used.
+
+If name of resulting entity should be different than the one read from
+the file, use ``--name`` option.
+
+Import options
+--------------
+
+Import options that ``ovfconverter`` supports include options for
+backend, disks, hypervisor, networks and operating system. If an option
+is given, it overrides the values provided in the OVF file.
+
+Backend
+^^^^^^^
+``--backend=option=value`` can be used to set auto balance, number of
+vcpus and amount of RAM memory.
+
+Please note that when you do not provide full set of options, the
+omitted ones will be set to cluster defaults (``auto``).
+
+Disks
+^^^^^
+``--disk-template=diskless`` causes the converter to ignore all other
+disk option - both from .ovf file and the command line. Other disk
+template options include ``plain``, ``drdb``, ``file``, ``sharedfile``
+and ``blockdev``.
+
+``--disk=number:size=value`` causes to create disks instead of
+converting them from OVF package; numbers should start with ``0`` and be
+consecutive.
+
+Hypervisor
+^^^^^^^^^^
+``-H hypervisor_name`` and ``-H hypervisor_name:option=value``
+provide options for hypervisor.
+
+Network
+^^^^^^^
+``--no-nics`` option causes converter to ignore any network information
+provided.
+
+``--network=number:option=value`` sets network information according to
+provided data, ignoring the OVF package configuration.
+
+Operating System
+^^^^^^^^^^^^^^^^
+``--os-type=type`` sets os type accordingly, this option is **required**
+when importing from OVF instance not created from Ganeti config file.
+
+``--os-parameters`` provides options for chosen operating system.
+
+Tags
+^^^^
+``--tags=tag1,tag2,tag3`` is a means of providing tags specific for the
+instance.
+
+
+After the conversion is completed, you may use ``gnt-backup import`` to
+import the instance into Ganeti.
+
+Example::
+
+       ovfconverter import file.ovf --disk-template=diskless \
+          --os-type=lenny-image \
+          --backend=vcpus=1,memory=512,auto_balance \
+          -H:xen-pvm \
+          --net=0:mode=bridged,link=xen-br0 \
+          --name=xen.i1
+       [...]
+       gnt-backup import xen.i1
+       [...]
+       gnt-instance list
+
+Export options
+--------------
+Export options include choice of disk formats to convert the disk image
+(``--format``) and compression of the disk into gzip format
+(``--compress``). User has also the choice of allowing to skip the
+Ganeti-specific part of the OVF document (``--external``).
+
+By default, exported OVF package will not be contained in the OVA
+package, but this may be changed by adding ``--ova`` option.
+
+Please note that in order to create an OVF package, it is first
+required that you export your VM using ``gnt-backup export``.
+
+Example::
+
+       gnt-backup export -n node1.xen xen.i1
+       [...]
+       ovfconverter export --format=vmdk --ova --external \
+         --output-dir=~/xen.i1 \
+         /srv/ganeti/export/xen.i1.node1.xen/config.ini
 
 Implementation details
 ======================
 
-TODO
+Disk conversion
+---------------
+
+Disk conversion for both import and export is done using external tool
+called ``qemu-img``. The same tool is used to determine the type of
+disk, as well as its virtual size.
+
+
+Import
+------
+
+Import functionality is implemented using two classes - OVFReader and
+OVFImporter.
+
+OVFReader class is used to read the contents of the ``.ovf`` file. Every
+action that requires ``.ovf`` file access is done through that class.
+It also performs validation of manifest, if one is present.
+
+The result of reading some part of file is typically a dictionary or a
+string, containing options which correspond to the ones in
+``config.ini`` file. Only in case of disks, the resulting value is
+different - it is then a list of disk names. The reason for that is the
+need for conversion.
+
+OVFImporter class performs all the command-line-like tasks, such as
+unpacking OVA package, removing temporary directory, converting disk
+file to raw format or saving the configuration file on disk.
+It also contains a set of functions that read the options provided in
+the command line.
+
+
+Typical workflow for the import is very simple:
+
+- read the ``.ovf`` file into memory
+- verify manifest
+- parse each element of the configuration file: name, disk template,
+  hypervisor, operating system, backend parameters, network and disks
+
+    - check if option for the element can be read from command line
+      options
+
+               - if yes: parse options from command line
+
+               - otherwise: read the appropriate portion of ``.ovf`` file
+
+- save gathered information in ``config.ini`` file
+
+Export
+------
+
+Similar to import, export functionality also uses two classes -
+OVFWriter and OVFExporter.
+
+OVFWriter class produces XML output based on the information given. Its
+sole role is to separate the creation of ``.ovf`` file content.
+
+OVFExporter class gathers information from ``config.ini`` file or
+command line and performs necessary operations like disk conversion, disk
+compression, manifest creation and OVA package creation.
+
+Typical workflow for the export is even simpler, than for the import:
+
+- read the ``config.ini`` file into memory
+- gather information about certain parts of the instance, convert and
+  compress disks if desired
+- save each of these elements as a fragment of XML tree
+- save the XML tree as ``.ovf`` file
+- create manifest file and fill it with appropriate checksums
+- if ``--ova`` option was chosen, pack the results into ``.ova`` tarfile
+
+
+Work in progress
+----------------
+
+- conversion to/from raw disk should be quicker
+- add graphic card memory to export information (12 MB of memory)
+- space requirements for conversion + compression + ova are currently
+  enormous
+- add support for disks in chunks
+- add support for certificates
+- investigate why VMWare's ovftool does not work with ovfconverter's
+  compression and ova packaging -- maybe noteworty: if OVA archive does
+  not have a disk (i.e. in OVA package there is only .ovf ad .mf file),
+  then the ovftool works
+- investigate why new versions of VirtualBox have problems with OVF
+  created by ovfconverter (everything works fine with 3.16 version, but
+  not with 4.0)
+
 
 .. vim: set textwidth=72 :
 .. Local Variables:
diff --git a/doc/design-query-splitting.rst b/doc/design-query-splitting.rst
new file mode 100644 (file)
index 0000000..2992f74
--- /dev/null
@@ -0,0 +1,156 @@
+===========================================
+Splitting the query and job execution paths
+===========================================
+
+
+Introduction
+============
+
+Currently, the master daemon does two main roles:
+
+- execute jobs that change the cluster state
+- respond to queries
+
+Due to the technical details of the implementation, the job execution
+and query paths interact with each other, and for example the "masterd
+hang" issue that we had late in the 2.5 release cycle was due to the
+interaction between job queries and job execution.
+
+Furthermore, also because technical implementations (Python lacking
+read-only variables being one example), we can't share internal data
+structures for jobs; instead, in the query path, we read them from
+disk in order to not block job execution due to locks.
+
+All these point to the fact that the integration of both queries and
+job execution in the same process (multi-threaded) creates more
+problems than advantages, and hence we should look into separating
+them.
+
+
+Proposed design
+===============
+
+In Ganeti 2.7, we will introduce a separate, optional daemon to handle
+queries (note: whether this is an actual "new" daemon, or its
+functionality is folded into confd, remains to be seen).
+
+This daemon will expose exactly the same Luxi interface as masterd,
+except that job submission will be disabled. If so configured (at
+build time), clients will be changed to:
+
+- keep sending REQ_SUBMIT_JOB, REQ_SUBMIT_MANY_JOBS, and all requests
+  except REQ_QUERY_* to the masterd socket (but also QR_LOCK)
+- redirect all REQ_QUERY_* requests to the new Luxi socket of the new
+  daemon (except generic query with QR_LOCK)
+
+This new daemon will serve both pure configuration queries (which
+confd can already serve), and run-time queries (which currently only
+masterd can serve). Since the RPC can be done from any node to any
+node, the new daemon can run on all master candidates, not only on the
+master node. This means that all gnt-* list options can be now run on
+other nodes than the master node. If we implement this as a separate
+daemon that talks to confd, then we could actually run this on all
+nodes of the cluster (to be decided).
+
+During the 2.7 release, masterd will still respond to queries itself,
+but it will log all such queries for identification of "misbehaving"
+clients.
+
+Advantages
+----------
+
+As far as I can see, this will bring some significant advantages.
+
+First, we remove any interaction between the job execution and cluster
+query state. This means that bugs in the locking code (job execution)
+will not impact the query of the cluster state, nor the query of the
+job execution itself. Furthermore, we will be able to have different
+tuning parameters between job execution (e.g. 25 threads for job
+execution) versus query (since these are transient, we could
+practically have unlimited numbers of query threads).
+
+As a result of the above split, we move from the current model, where
+shutdown of the master daemon practically "breaks" the entire Ganeti
+functionality (no job execution nor queries, not even connecting to
+the instance console), to a split model:
+
+- if just masterd is stopped, then other cluster functionality remains
+  available: listing instances, connecting to the console of an
+  instance, etc.
+- if just "queryd" is stopped, masterd can still process jobs, and one
+  can furthermore run queries from other nodes (MCs)
+- only if both are stopped, we end up with the previous state
+
+This will help, for example, in the case where the master node has
+crashed and we haven't failed it over yet: querying and investigating
+the cluster state will still be possible from other master candidates
+(on small clusters, this will mean from all nodes).
+
+A last advantage is that we finally will be able to reduce the
+footprint of masterd; instead of previous discussion of splitting
+individual jobs, which requires duplication of all the base
+functionality, this will just split the queries, a more trivial piece
+of code than job execution. This should be a reasonable work effort,
+with a much smaller impact in case of failure (we can still run
+masterd as before).
+
+Disadvantages
+-------------
+
+We might get increased inconsistency during queries, as there will be
+a delay between masterd saving an updated configuration and
+confd/query loading and parsing it. However, this could be compensated
+by the fact that queries will only look at "snapshots" of the
+configuration, whereas before it could also look at "in-progress"
+modifications (due to the non-atomic updates). I think these will
+cancel each other out, we will have to see in practice how it works.
+
+Another disadvantage *might* be that we have a more complex setup, due
+to the introduction of a new daemon. However, the query path will be
+much simpler, and when we remove the query functionality from masterd
+we should have a more robust system.
+
+Finally, we have QR_LOCK, which is an internal query related to the
+master daemon, using the same infrastructure as the other queries
+(related to cluster state). This is unfortunate, and will require
+untangling in order to keep code duplication low.
+
+Long-term plans
+===============
+
+If this works well, the plan would be (tentatively) to disable the
+query functionality in masterd completely in Ganeti 2.8, in order to
+remove the duplication. This might change based on how/if we split the
+configuration/locking daemon out, or not.
+
+Once we split this out, there is not technical reason why we can't
+execute any query from any node; except maybe practical reasons
+(network topology, remote nodes, etc.) or security reasons (if/whether
+we want to change the cluster security model). In any case, it should
+be possible to do this in a reliable way from all master candidates.
+
+Some implementation details
+---------------------------
+
+We will fold this in confd, at least initially, to reduce the
+proliferation of daemons. Haskell will limit (if used properly) any too
+deep integration between the old "confd" functionality and the new query
+one. As advantages, we'll have a single daemons that handles
+configuration queries.
+
+The redirection of Luxi requests can be easily done based on the
+request type, if we have both sockets open, or if we open on demand.
+
+We don't want the masterd to talk to the queryd itself (hidden
+redirection), since we want to be able to run queries while masterd is
+down.
+
+During the 2.7 release cycle, we can test all queries against both
+masterd and queryd in QA, so we know we have exactly the same
+interface and it is consistent.
+
+.. vim: set textwidth=72 :
+.. Local Variables:
+.. mode: rst
+.. fill-column: 72
+.. End:
diff --git a/doc/design-resource-model.rst b/doc/design-resource-model.rst
new file mode 100644 (file)
index 0000000..a4d63c6
--- /dev/null
@@ -0,0 +1,962 @@
+========================
+ Resource model changes
+========================
+
+
+Introduction
+============
+
+In order to manage virtual machines across the cluster, Ganeti needs to
+understand the resources present on the nodes, the hardware and software
+limitations of the nodes, and how much can be allocated safely on each
+node. Some of these decisions are delegated to IAllocator plugins, for
+easier site-level customisation.
+
+Similarly, the HTools suite has an internal model that simulates the
+hardware resource changes in response to Ganeti operations, in order to
+provide both an iallocator plugin and for balancing the
+cluster.
+
+While currently the HTools model is much more advanced than Ganeti's,
+neither one is flexible enough and both are heavily geared toward a
+specific Xen model; they fail to work well with (e.g.) KVM or LXC, or
+with Xen when :term:`tmem` is enabled. Furthermore, the set of metrics
+contained in the models is limited to historic requirements and fails to
+account for (e.g.)  heterogeneity in the I/O performance of the nodes.
+
+Current situation
+=================
+
+Ganeti
+------
+
+At this moment, Ganeti itself doesn't do any static modelling of the
+cluster resources. It only does some runtime checks:
+
+- when creating instances, for the (current) free disk space
+- when starting instances, for the (current) free memory
+- during cluster verify, for enough N+1 memory on the secondaries, based
+  on the (current) free memory
+
+Basically this model is a pure :term:`SoW` one, and it works well when
+there are other instances/LVs on the nodes, as it allows Ganeti to deal
+with ‘orphan’ resource usage, but on the other hand it has many issues,
+described below.
+
+HTools
+------
+
+Since HTools does an pure in-memory modelling of the cluster changes as
+it executes the balancing or allocation steps, it had to introduce a
+static (:term:`SoR`) cluster model.
+
+The model is constructed based on the received node properties from
+Ganeti (hence it basically is constructed on what Ganeti can export).
+
+Disk
+~~~~
+
+For disk it consists of just the total (``tdsk``) and the free disk
+space (``fdsk``); we don't directly track the used disk space. On top of
+this, we compute and warn if the sum of disk sizes used by instance does
+not match with ``tdsk - fdsk``, but otherwise we do not track this
+separately.
+
+Memory
+~~~~~~
+
+For memory, the model is more complex and tracks some variables that
+Ganeti itself doesn't compute. We start from the total (``tmem``), free
+(``fmem``) and node memory (``nmem``) as supplied by Ganeti, and
+additionally we track:
+
+instance memory (``imem``)
+    the total memory used by primary instances on the node, computed
+    as the sum of instance memory
+
+reserved memory (``rmem``)
+    the memory reserved by peer nodes for N+1 redundancy; this memory is
+    tracked per peer-node, and the maximum value out of the peer memory
+    lists is the node's ``rmem``; when not using DRBD, this will be
+    equal to zero
+
+unaccounted memory (``xmem``)
+    memory that cannot be unaccounted for via the Ganeti model; this is
+    computed at startup as::
+
+        tmem - imem - nmem - fmem
+
+    and is presumed to remain constant irrespective of any instance
+    moves
+
+available memory (``amem``)
+    this is simply ``fmem - rmem``, so unless we use DRBD, this will be
+    equal to ``fmem``
+
+``tmem``, ``nmem`` and ``xmem`` are presumed constant during the
+instance moves, whereas the ``fmem``, ``imem``, ``rmem`` and ``amem``
+values are updated according to the executed moves.
+
+CPU
+~~~
+
+The CPU model is different than the disk/memory models, since it's the
+only one where:
+
+#. we do oversubscribe physical CPUs
+#. and there is no natural limit for the number of VCPUs we can allocate
+
+We therefore track the total number of VCPUs used on the node and the
+number of physical CPUs, and we cap the vcpu-to-cpu ratio in order to
+make this somewhat more similar to the other resources which are
+limited.
+
+Dynamic load
+~~~~~~~~~~~~
+
+There is also a model that deals with *dynamic load* values in
+htools. As far as we know, it is not currently used actually with load
+values, but it is active by default with unitary values for all
+instances; it currently tracks these metrics:
+
+- disk load
+- memory load
+- cpu load
+- network load
+
+Even though we do not assign real values to these load values, the fact
+that we at least sum them means that the algorithm tries to equalise
+these loads, and especially the network load, which is otherwise not
+tracked at all. The practical result (due to a combination of these four
+metrics) is that the number of secondaries will be balanced.
+
+Limitations
+-----------
+
+
+There are unfortunately many limitations to the current model.
+
+Memory
+~~~~~~
+
+The memory model doesn't work well in case of KVM. For Xen, the memory
+for the node (i.e. ``dom0``) can be static or dynamic; we don't support
+the latter case, but for the former case, the static value is configured
+in Xen/kernel command line, and can be queried from Xen
+itself. Therefore, Ganeti can query the hypervisor for the memory used
+for the node; the same model was adopted for the chroot/KVM/LXC
+hypervisors, but in these cases there's no natural value for the memory
+used by the base OS/kernel, and we currently try to compute a value for
+the node memory based on current consumption. This, being variable,
+breaks the assumptions in both Ganeti and HTools.
+
+This problem also shows for the free memory: if the free memory on the
+node is not constant (Xen with :term:`tmem` auto-ballooning enabled), or
+if the node and instance memory are pooled together (Linux-based
+hypervisors like KVM and LXC), the current value of the free memory is
+meaningless and cannot be used for instance checks.
+
+A separate issue related to the free memory tracking is that since we
+don't track memory use but rather memory availability, an instance that
+is temporary down changes Ganeti's understanding of the memory status of
+the node. This can lead to problems such as:
+
+.. digraph:: "free-mem-issue"
+
+  node  [shape=box];
+  inst1 [label="instance1"];
+  inst2 [label="instance2"];
+
+  node  [shape=note];
+  nodeA [label="fmem=0"];
+  nodeB [label="fmem=1"];
+  nodeC [label="fmem=0"];
+
+  node  [shape=ellipse, style=filled, fillcolor=green]
+
+  {rank=same; inst1 inst2}
+
+  stop    [label="crash!", fillcolor=orange];
+  migrate [label="migrate/ok"];
+  start   [style=filled, fillcolor=red, label="start/fail"];
+  inst1   -> stop -> start;
+  stop    -> migrate -> start [style=invis, weight=0];
+  inst2   -> migrate;
+
+  {rank=same; inst1 inst2 nodeA}
+  {rank=same; stop nodeB}
+  {rank=same; migrate nodeC}
+
+  nodeA -> nodeB -> nodeC [style=invis, weight=1];
+
+The behaviour here is wrong; the migration of *instance2* to the node in
+question will succeed or fail depending on whether *instance1* is
+running or not. And for *instance1*, it can lead to cases where it if
+crashes, it cannot restart anymore.
+
+Finally, not a problem but rather a missing important feature is support
+for memory over-subscription: both Xen and KVM support memory
+ballooning, even automatic memory ballooning, for a while now. The
+entire memory model is based on a fixed memory size for instances, and
+if memory ballooning is enabled, it will “break” the HTools
+algorithm. Even the fact that KVM instances do not use all memory from
+the start creates problems (although not as high, since it will grow and
+stabilise in the end).
+
+Disks
+~~~~~
+
+Because we only track disk space currently, this means if we have a
+cluster of ``N`` otherwise identical nodes but half of them have 10
+drives of size ``X`` and the other half 2 drives of size ``5X``, HTools
+will consider them exactly the same. However, in the case of mechanical
+drives at least, the I/O performance will differ significantly based on
+spindle count, and a “fair” load distribution should take this into
+account (a similar comment can be made about processor/memory/network
+speed).
+
+Another problem related to the spindle count is the LVM allocation
+algorithm. Currently, the algorithm always creates (or tries to create)
+striped volumes, with the stripe count being hard-coded to the
+``./configure`` parameter ``--with-lvm-stripecount``. This creates
+problems like:
+
+- when installing from a distribution package, all clusters will be
+  either limited or overloaded due to this fixed value
+- it is not possible to mix heterogeneous nodes (even in different node
+  groups) and have optimal settings for all nodes
+- the striping value applies both to LVM/DRBD data volumes (which are on
+  the order of gigabytes to hundreds of gigabytes) and to DRBD metadata
+  volumes (whose size is always fixed at 128MB); when stripping such
+  small volumes over many PVs, their size will increase needlessly (and
+  this can confuse HTools' disk computation algorithm)
+
+Moreover, the allocation currently allocates based on a ‘most free
+space’ algorithm. This balances the free space usage on disks, but on
+the other hand it tends to mix rather badly the data and metadata
+volumes of different instances. For example, it cannot do the following:
+
+- keep DRBD data and metadata volumes on the same drives, in order to
+  reduce exposure to drive failure in a many-drives system
+- keep DRBD data and metadata volumes on different drives, to reduce
+  performance impact of metadata writes
+
+Additionally, while Ganeti supports setting the volume separately for
+data and metadata volumes at instance creation, there are no defaults
+for this setting.
+
+Similar to the above stripe count problem (which is about not good
+enough customisation of Ganeti's behaviour), we have limited
+pass-through customisation of the various options of our storage
+backends; while LVM has a system-wide configuration file that can be
+used to tweak some of its behaviours, for DRBD we don't use the
+:command:`drbdadmin` tool, and instead we call :command:`drbdsetup`
+directly, with a fixed/restricted set of options; so for example one
+cannot tweak the buffer sizes.
+
+Another current problem is that the support for shared storage in HTools
+is still limited, but this problem is outside of this design document.
+
+Locking
+~~~~~~~
+
+A further problem generated by the “current free” model is that during a
+long operation which affects resource usage (e.g. disk replaces,
+instance creations) we have to keep the respective objects locked
+(sometimes even in exclusive mode), since we don't want any concurrent
+modifications to the *free* values.
+
+A classic example of the locking problem is the following:
+
+.. digraph:: "iallocator-lock-issues"
+
+  rankdir=TB;
+
+  start [style=invis];
+  node  [shape=box,width=2];
+  job1  [label="add instance\niallocator run\nchoose A,B"];
+  job1e [label="finish add"];
+  job2  [label="add instance\niallocator run\nwait locks"];
+  job2s [label="acquire locks\nchoose C,D"];
+  job2e [label="finish add"];
+
+  job1  -> job1e;
+  job2  -> job2s -> job2e;
+  edge [style=invis,weight=0];
+  start -> {job1; job2}
+  job1  -> job2;
+  job2  -> job1e;
+  job1e -> job2s [style=dotted,label="release locks"];
+
+In the above example, the second IAllocator run will wait for locks for
+nodes ``A`` and ``B``, even though in the end the second instance will
+be placed on another set of nodes (``C`` and ``D``). This wait shouldn't
+be needed, since right after the first IAllocator run has finished,
+:command:`hail` knows the status of the cluster after the allocation,
+and it could answer the question for the second run too; however, Ganeti
+doesn't have such visibility into the cluster state and thus it is
+forced to wait with the second job.
+
+Similar examples can be made about replace disks (another long-running
+opcode).
+
+.. _label-policies:
+
+Policies
+~~~~~~~~
+
+For most of the resources, we have metrics defined by policy: e.g. the
+over-subscription ratio for CPUs, the amount of space to reserve,
+etc. Furthermore, although there are no such definitions in Ganeti such
+as minimum/maximum instance size, a real deployment will need to have
+them, especially in a fully-automated workflow where end-users can
+request instances via an automated interface (that talks to the cluster
+via RAPI, LUXI or command line). However, such an automated interface
+will need to also take into account cluster capacity, and if the
+:command:`hspace` tool is used for the capacity computation, it needs to
+be told the maximum instance size, however it has a built-in minimum
+instance size which is not customisable.
+
+It is clear that this situation leads to duplicate definition of
+resource policies which makes it hard to easily change per-cluster (or
+globally) the respective policies, and furthermore it creates
+inconsistencies if such policies are not enforced at the source (i.e. in
+Ganeti).
+
+Balancing algorithm
+~~~~~~~~~~~~~~~~~~~
+
+The balancing algorithm, as documented in the HTools ``README`` file,
+tries to minimise the cluster score; this score is based on a set of
+metrics that describe both exceptional conditions and how spread the
+instances are across the nodes. In order to achieve this goal, it moves
+the instances around, with a series of moves of various types:
+
+- disk replaces (for DRBD-based instances)
+- instance failover/migrations (for all types)
+
+However, the algorithm only looks at the cluster score, and not at the
+*“cost”* of the moves. In other words, the following can and will happen
+on a cluster:
+
+.. digraph:: "balancing-cost-issues"
+
+  rankdir=LR;
+  ranksep=1;
+
+  start     [label="score α", shape=hexagon];
+
+  node      [shape=box, width=2];
+  replace1  [label="replace_disks 500G\nscore α-3ε\ncost 3"];
+  replace2a [label="replace_disks 20G\nscore α-2ε\ncost 2"];
+  migrate1  [label="migrate\nscore α-ε\ncost 1"];
+
+  choose    [shape=ellipse,label="choose min(score)=α-3ε\ncost 3"];
+
+  start -> {replace1; replace2a; migrate1} -> choose;
+
+Even though a migration is much, much cheaper than a disk replace (in
+terms of network and disk traffic on the cluster), if the disk replace
+results in a score infinitesimally smaller, then it will be
+chosen. Similarly, between two disk replaces, one moving e.g. ``500GiB``
+and one moving ``20GiB``, the first one will be chosen if it results in
+a score smaller than the second one. Furthermore, even if the resulting
+scores are equal, the first computed solution will be kept, whichever it
+is.
+
+Fixing this algorithmic problem is doable, but currently Ganeti doesn't
+export enough information about nodes to make an informed decision; in
+the above example, if the ``500GiB`` move is between nodes having fast
+I/O (both disks and network), it makes sense to execute it over a disk
+replace of ``100GiB`` between nodes with slow I/O, so simply relating to
+the properties of the move itself is not enough; we need more node
+information for cost computation.
+
+Allocation algorithm
+~~~~~~~~~~~~~~~~~~~~
+
+.. note:: This design document will not address this limitation, but it
+  is worth mentioning as it directly related to the resource model.
+
+The current allocation/capacity algorithm works as follows (per
+node-group)::
+
+    repeat:
+        allocate instance without failing N+1
+
+This simple algorithm, and its use of ``N+1`` criterion, has a built-in
+limit of 1 machine failure in case of DRBD. This means the algorithm
+guarantees that, if using DRBD storage, there are enough resources to
+(re)start all affected instances in case of one machine failure. This
+relates mostly to memory; there is no account for CPU over-subscription
+(i.e. in case of failure, make sure we can failover while still not
+going over CPU limits), or for any other resource.
+
+In case of shared storage, there's not even the memory guarantee, as the
+N+1 protection doesn't work for shared storage.
+
+If a given cluster administrator wants to survive up to two machine
+failures, or wants to ensure CPU limits too for DRBD, there is no
+possibility to configure this in HTools (neither in :command:`hail` nor
+in :command:`hspace`). Current workaround employ for example deducting a
+certain number of instances from the size computed by :command:`hspace`,
+but this is a very crude method, and requires that instance creations
+are limited before Ganeti (otherwise :command:`hail` would allocate
+until the cluster is full).
+
+Proposed architecture
+=====================
+
+
+There are two main changes proposed:
+
+- changing the resource model from a pure :term:`SoW` to a hybrid
+  :term:`SoR`/:term:`SoW` one, where the :term:`SoR` component is
+  heavily emphasised
+- extending the resource model to cover additional properties,
+  completing the “holes” in the current coverage
+
+The second change is rather straightforward, but will add more
+complexity in the modelling of the cluster. The first change, however,
+represents a significant shift from the current model, which Ganeti had
+from its beginnings.
+
+Lock-improved resource model
+----------------------------
+
+Hybrid SoR/SoW model
+~~~~~~~~~~~~~~~~~~~~
+
+The resources of a node can be characterised in two broad classes:
+
+- mostly static resources
+- dynamically changing resources
+
+In the first category, we have things such as total core count, total
+memory size, total disk size, number of network interfaces etc. In the
+second category we have things such as free disk space, free memory, CPU
+load, etc. Note that nowadays we don't have (anymore) fully-static
+resources: features like CPU and memory hot-plug, online disk replace,
+etc. mean that theoretically all resources can change (there are some
+practical limitations, of course).
+
+Even though the rate of change of the two resource types is wildly
+different, right now Ganeti handles both the same. Given that the
+interval of change of the semi-static ones is much bigger than most
+Ganeti operations, even more than lengthy sequences of Ganeti jobs, it
+makes sense to treat them separately.
+
+The proposal is then to move the following resources into the
+configuration and treat the configuration as the authoritative source
+for them (a :term:`SoR` model):
+
+- CPU resources:
+    - total core count
+    - node core usage (*new*)
+- memory resources:
+    - total memory size
+    - node memory size
+    - hypervisor overhead (*new*)
+- disk resources:
+    - total disk size
+    - disk overhead (*new*)
+
+Since these resources can though change at run-time, we will need
+functionality to update the recorded values.
+
+Pre-computing dynamic resource values
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Remember that the resource model used by HTools models the clusters as
+obeying the following equations:
+
+  disk\ :sub:`free` = disk\ :sub:`total` - ∑ disk\ :sub:`instances`
+
+  mem\ :sub:`free` = mem\ :sub:`total` - ∑ mem\ :sub:`instances` - mem\
+  :sub:`node` - mem\ :sub:`overhead`
+
+As this model worked fine for HTools, we can consider it valid and adopt
+it in Ganeti. Furthermore, note that all values in the right-hand side
+come now from the configuration:
+
+- the per-instance usage values were already stored in the configuration
+- the other values will are moved to the configuration per the previous
+  section
+
+This means that we can now compute the free values without having to
+actually live-query the nodes, which brings a significant advantage.
+
+There are a couple of caveats to this model though. First, as the
+run-time state of the instance is no longer taken into consideration, it
+means that we have to introduce a new *offline* state for an instance
+(similar to the node one). In this state, the instance's runtime
+resources (memory and VCPUs) are no longer reserved for it, and can be
+reused by other instances. Static resources like disk and MAC addresses
+are still reserved though. Transitioning into and out of this reserved
+state will be more involved than simply stopping/starting the instance
+(e.g. de-offlining can fail due to missing resources). This complexity
+is compensated by the increased consistency of what guarantees we have
+in the stopped state (we always guarantee resource reservation), and the
+potential for management tools to restrict which users can transition
+into/out of this state separate from which users can stop/start the
+instance.
+
+Separating per-node resource locks
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Many of the current node locks in Ganeti exist in order to guarantee
+correct resource state computation, whereas others are designed to
+guarantee reasonable run-time performance of nodes (e.g. by not
+overloading the I/O subsystem). This is an unfortunate coupling, since
+it means for example that the following two operations conflict in
+practice even though they are orthogonal:
+
+- replacing a instance's disk on a node
+- computing node disk/memory free for an IAllocator run
+
+This conflict increases significantly the lock contention on a big/busy
+cluster and at odds with the goal of increasing the cluster size.
+
+The proposal is therefore to add a new level of locking that is only
+used to prevent concurrent modification to the resource states (either
+node properties or instance properties) and not for long-term
+operations:
+
+- instance creation needs to acquire and keep this lock until adding the
+  instance to the configuration
+- instance modification needs to acquire and keep this lock until
+  updating the instance
+- node property changes will need to acquire this lock for the
+  modification
+
+The new lock level will sit before the instance level (right after BGL)
+and could either be single-valued (like the “Big Ganeti Lock”), in which
+case we won't be able to modify two nodes at the same time, or per-node,
+in which case the list of locks at this level needs to be synchronised
+with the node lock level. To be determined.
+
+Lock contention reduction
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Based on the above, the locking contention will be reduced as follows:
+IAllocator calls will no longer need the ``LEVEL_NODE: ALL_SET`` lock,
+only the resource lock (in exclusive mode). Hence allocating/computing
+evacuation targets will no longer conflict for longer than the time to
+compute the allocation solution.
+
+The remaining long-running locks will be the DRBD replace-disks ones
+(exclusive mode). These can also be removed, or changed into shared
+locks, but that is a separate design change.
+
+.. admonition:: FIXME
+
+  Need to rework instance replace disks. I don't think we need exclusive
+  locks for replacing disks: it is safe to stop/start the instance while
+  it's doing a replace disks. Only modify would need exclusive, and only
+  for transitioning into/out of offline state.
+
+Instance memory model
+---------------------
+
+In order to support ballooning, the instance memory model needs to be
+changed from a “memory size” one to a “min/max memory size”. This
+interacts with the new static resource model, however, and thus we need
+to declare a-priori the expected oversubscription ratio on the cluster.
+
+The new minimum memory size parameter will be similar to the current
+memory size; the cluster will guarantee that in all circumstances, all
+instances will have available their minimum memory size. The maximum
+memory size will permit burst usage of more memory by instances, with
+the restriction that the sum of maximum memory usage will not be more
+than the free memory times the oversubscription factor:
+
+    ∑ memory\ :sub:`min` ≤ memory\ :sub:`available`
+
+    ∑ memory\ :sub:`max` ≤ memory\ :sub:`free` * oversubscription_ratio
+
+The hypervisor will have the possibility of adjusting the instance's
+memory size dynamically between these two boundaries.
+
+Note that the minimum memory is related to the available memory on the
+node, whereas the maximum memory is related to the free memory. On
+DRBD-enabled clusters, this will have the advantage of using the
+reserved memory for N+1 failover for burst usage, instead of having it
+completely idle.
+
+.. admonition:: FIXME
+
+  Need to document how Ganeti forces minimum size at runtime, overriding
+  the hypervisor, in cases of failover/lack of resources.
+
+New parameters
+--------------
+
+Unfortunately the design will add a significant number of new
+parameters, and change the meaning of some of the current ones.
+
+Instance size limits
+~~~~~~~~~~~~~~~~~~~~
+
+As described in :ref:`label-policies`, we currently lack a clear
+definition of the support instance sizes (minimum, maximum and
+standard). As such, we will add the following structure to the cluster
+parameters:
+
+- ``min_ispec``, ``max_ispec``: minimum and maximum acceptable instance
+  specs
+- ``std_ispec``: standard instance size, which will be used for capacity
+  computations and for default parameters on the instance creation
+  request
+
+Ganeti will by default reject non-standard instance sizes (lower than
+``min_ispec`` or greater than ``max_ispec``), but as usual a
+``--ignore-ipolicy`` option on the command line or in the RAPI request
+will override these constraints. The ``std_spec`` structure will be used
+to fill in missing instance specifications on create.
+
+Each of the ispec structures will be a dictionary, since the contents
+can change over time. Initially, we will define the following variables
+in these structures:
+
++---------------+----------------------------------+--------------+
+|Name           |Description                       |Type          |
++===============+==================================+==============+
+|mem_size       |Allowed memory size               |int           |
++---------------+----------------------------------+--------------+
+|cpu_count      |Allowed vCPU count                |int           |
++---------------+----------------------------------+--------------+
+|disk_count     |Allowed disk count                |int           |
++---------------+----------------------------------+--------------+
+|disk_size      |Allowed disk size                 |int           |
++---------------+----------------------------------+--------------+
+|nic_count      |Alowed NIC count                  |int           |
++---------------+----------------------------------+--------------+
+
+Inheritance
++++++++++++
+
+In a single-group cluster, the above structure is sufficient. However,
+on a multi-group cluster, it could be that the hardware specifications
+differ across node groups, and thus the following problem appears: how
+can Ganeti present unified specifications over RAPI?
+
+Since the set of instance specs is only partially ordered (as opposed to
+the sets of values of individual variable in the spec, which are totally
+ordered), it follows that we can't present unified specs. As such, the
+proposed approach is to allow the ``min_ispec`` and ``max_ispec`` to be
+customised per node-group (and export them as a list of specifications),
+and a single ``std_spec`` at cluster level (exported as a single value).
+
+
+Allocation parameters
+~~~~~~~~~~~~~~~~~~~~~
+
+Beside the limits of min/max instance sizes, there are other parameters
+related to capacity and allocation limits. These are mostly related to
+the problems related to over allocation.
+
++-----------------+----------+---------------------------+----------+------+
+| Name            |Level(s)  |Description                |Current   |Type  |
+|                 |          |                           |value     |      |
++=================+==========+===========================+==========+======+
+|vcpu_ratio       |cluster,  |Maximum ratio of virtual to|64 (only  |float |
+|                 |node group|physical CPUs              |in htools)|      |
++-----------------+----------+---------------------------+----------+------+
+|spindle_ratio    |cluster,  |Maximum ratio of instances |none      |float |
+|                 |node group|to spindles; when the I/O  |          |      |
+|                 |          |model doesn't map directly |          |      |
+|                 |          |to spindles, another       |          |      |
+|                 |          |measure of I/O should be   |          |      |
+|                 |          |used instead               |          |      |
++-----------------+----------+---------------------------+----------+------+
+|max_node_failures|cluster,  |Cap allocation/capacity so |1         |int   |
+|                 |node group|that the cluster can       |(hardcoded|      |
+|                 |          |survive this many node     |in htools)|      |
+|                 |          |failures                   |          |      |
++-----------------+----------+---------------------------+----------+------+
+
+Since these are used mostly internally (in htools), they will be
+exported as-is from Ganeti, without explicit handling of node-groups
+grouping.
+
+Regarding ``spindle_ratio``, in this context spindles do not necessarily
+have to mean actual mechanical hard-drivers; it's rather a measure of
+I/O performance for internal storage.
+
+Disk parameters
+~~~~~~~~~~~~~~~
+
+The proposed model for the new disk parameters is a simple free-form one
+based on dictionaries, indexed per disk template and parameter name.
+Only the disk template parameters are visible to the user, and those are
+internally translated to logical disk level parameters.
+
+This is a simplification, because each parameter is applied to a whole
+nested structure and there is no way of fine-tuning each level's
+parameters, but it is good enough for the current parameter set. This
+model could need to be expanded, e.g., if support for three-nodes stacked
+DRBD setups is added to Ganeti.
+
+At JSON level, since the object key has to be a string, the keys can be
+encoded via a separator (e.g. slash), or by having two dict levels.
+
+When needed, the unit of measurement is expressed inside square
+brackets.
+
++--------+--------------+-------------------------+---------------------+------+
+|Disk    |Name          |Description              |Current status       |Type  |
+|template|              |                         |                     |      |
++========+==============+=========================+=====================+======+
+|plain   |stripes       |How many stripes to use  |Configured at        |int   |
+|        |              |for newly created (plain)|./configure time, not|      |
+|        |              |logical voumes           |overridable at       |      |
+|        |              |                         |runtime              |      |
++--------+--------------+-------------------------+---------------------+------+
+|drbd    |data-stripes  |How many stripes to use  |Same as for          |int   |
+|        |              |for data volumes         |plain/stripes        |      |
++--------+--------------+-------------------------+---------------------+------+
+|drbd    |metavg        |Default volume group for |Same as the main     |string|
+|        |              |the metadata LVs         |volume group,        |      |
+|        |              |                         |overridable via      |      |
+|        |              |                         |'metavg' key         |      |
++--------+--------------+-------------------------+---------------------+------+
+|drbd    |meta-stripes  |How many stripes to use  |Same as for lvm      |int   |
+|        |              |for meta volumes         |'stripes', suboptimal|      |
+|        |              |                         |as the meta LVs are  |      |
+|        |              |                         |small                |      |
++--------+--------------+-------------------------+---------------------+------+
+|drbd    |disk-barriers |What kind of barriers to |Either all enabled or|string|
+|        |              |*disable* for disks;     |all disabled, per    |      |
+|        |              |either "n" or a string   |./configure time     |      |
+|        |              |containing a subset of   |option               |      |
+|        |              |"bfd"                    |                     |      |
++--------+--------------+-------------------------+---------------------+------+
+|drbd    |meta-barriers |Whether to disable or not|Handled together with|bool  |
+|        |              |the barriers for the meta|disk-barriers        |      |
+|        |              |volume                   |                     |      |
++--------+--------------+-------------------------+---------------------+------+
+|drbd    |resync-rate   |The (static) resync rate |Hardcoded in         |int   |
+|        |              |for drbd, when using the |constants.py, not    |      |
+|        |              |static syncer, in KiB/s  |changeable via Ganeti|      |
++--------+--------------+-------------------------+---------------------+------+
+|drbd    |dynamic-resync|Whether to use the       |Not supported.       |bool  |
+|        |              |dynamic resync speed     |                     |      |
+|        |              |controller or not. If    |                     |      |
+|        |              |enabled, c-plan-ahead    |                     |      |
+|        |              |must be non-zero and all |                     |      |
+|        |              |the c-* parameters will  |                     |      |
+|        |              |be used by DRBD.         |                     |      |
+|        |              |Otherwise, the value of  |                     |      |
+|        |              |resync-rate will be used |                     |      |
+|        |              |as a static resync speed.|                     |      |
++--------+--------------+-------------------------+---------------------+------+
+|drbd    |c-plan-ahead  |Agility factor of the    |Not supported.       |int   |
+|        |              |dynamic resync speed     |                     |      |
+|        |              |controller. (the higher, |                     |      |
+|        |              |the slower the algorithm |                     |      |
+|        |              |will adapt the resync    |                     |      |
+|        |              |speed). A value of 0     |                     |      |
+|        |              |(that is the default)    |                     |      |
+|        |              |disables the controller  |                     |      |
+|        |              |[ds]                     |                     |      |
++--------+--------------+-------------------------+---------------------+------+
+|drbd    |c-fill-target |Maximum amount of        |Not supported.       |int   |
+|        |              |in-flight resync data    |                     |      |
+|        |              |for the dynamic resync   |                     |      |
+|        |              |speed controller         |                     |      |
+|        |              |[sectors]                |                     |      |
++--------+--------------+-------------------------+---------------------+------+
+|drbd    |c-delay-target|Maximum estimated peer   |Not supported.       |int   |
+|        |              |response latency for the |                     |      |
+|        |              |dynamic resync speed     |                     |      |
+|        |              |controller [ds]          |                     |      |
++--------+--------------+-------------------------+---------------------+------+
+|drbd    |c-max-rate    |Upper bound on resync    |Not supported.       |int   |
+|        |              |speed for the dynamic    |                     |      |
+|        |              |resync speed controller  |                     |      |
+|        |              |[KiB/s]                  |                     |      |
++--------+--------------+-------------------------+---------------------+------+
+|drbd    |c-min-rate    |Minimum resync speed for |Not supported.       |int   |
+|        |              |the dynamic resync speed |                     |      |
+|        |              |controller [KiB/s]       |                     |      |
++--------+--------------+-------------------------+---------------------+------+
+|drbd    |disk-custom   |Free-form string that    |Not supported        |string|
+|        |              |will be appended to the  |                     |      |
+|        |              |drbdsetup disk command   |                     |      |
+|        |              |line, for custom options |                     |      |
+|        |              |not supported by Ganeti  |                     |      |
+|        |              |itself                   |                     |      |
++--------+--------------+-------------------------+---------------------+------+
+|drbd    |net-custom    |Free-form string for     |Not supported        |string|
+|        |              |custom net setup options |                     |      |
++--------+--------------+-------------------------+---------------------+------+
+
+Currently Ganeti supports only DRBD 8.0.x, 8.2.x, 8.3.x.  It will refuse
+to work with DRBD 8.4 since the :command:`drbdsetup` syntax has changed
+significantly.
+
+The barriers-related parameters have been introduced in different DRBD
+versions; please make sure that your version supports all the barrier
+parameters that you pass to Ganeti. Any version later than 8.3.0
+implements all of them.
+
+The minimum DRBD version for using the dynamic resync speed controller
+is 8.3.9, since previous versions implement different parameters.
+
+A more detailed discussion of the dynamic resync speed controller
+parameters is outside the scope of the present document. Please refer to
+the ``drbdsetup`` man page
+(`8.3 <http://www.drbd.org/users-guide-8.3/re-drbdsetup.html>`_ and 
+`8.4 <http://www.drbd.org/users-guide/re-drbdsetup.html>`_). An
+interesting discussion about them can also be found in a
+`drbd-user mailing list post
+<http://lists.linbit.com/pipermail/drbd-user/2011-August/016739.html>`_.
+
+All the above parameters are at cluster and node group level; as in
+other parts of the code, the intention is that all nodes in a node group
+should be equal. It will later be decided to which node group give
+precedence in case of instances split over node groups.
+
+.. admonition:: FIXME
+
+   Add details about when each parameter change takes effect (device
+   creation vs. activation)
+
+Node parameters
+~~~~~~~~~~~~~~~
+
+For the new memory model, we'll add the following parameters, in a
+dictionary indexed by the hypervisor name (node attribute
+``hv_state``). The rationale is that, even though multi-hypervisor
+clusters are rare, they make sense sometimes, and thus we need to
+support multipe node states (one per hypervisor).
+
+Since usually only one of the multiple hypervisors is the 'main' one
+(and the others used sparringly), capacity computation will still only
+use the first hypervisor, and not all of them. Thus we avoid possible
+inconsistencies.
+
++----------+-----------------------------------+---------------+-------+
+|Name      |Description                        |Current state  |Type   |
+|          |                                   |               |       |
++==========+===================================+===============+=======+
+|mem_total |Total node memory, as discovered by|Queried at     |int    |
+|          |this hypervisor                    |runtime        |       |
++----------+-----------------------------------+---------------+-------+
+|mem_node  |Memory used by, or reserved for,   |Queried at     |int    |
+|          |the node itself; not that some     |runtime        |       |
+|          |hypervisors can report this in an  |               |       |
+|          |authoritative way, other not       |               |       |
++----------+-----------------------------------+---------------+-------+
+|mem_hv    |Memory used either by the          |Not used,      |int    |
+|          |hypervisor itself or lost due to   |htools computes|       |
+|          |instance allocation rounding;      |it internally  |       |
+|          |usually this cannot be precisely   |               |       |
+|          |computed, but only roughly         |               |       |
+|          |estimated                          |               |       |
++----------+-----------------------------------+---------------+-------+
+|cpu_total |Total node cpu (core) count;       |Queried at     |int    |
+|          |usually this can be discovered     |runtime        |       |
+|          |automatically                      |               |       |
+|          |                                   |               |       |
+|          |                                   |               |       |
+|          |                                   |               |       |
++----------+-----------------------------------+---------------+-------+
+|cpu_node  |Number of cores reserved for the   |Not used at all|int    |
+|          |node itself; this can either be    |               |       |
+|          |discovered or set manually. Only   |               |       |
+|          |used for estimating how many VCPUs |               |       |
+|          |are left for instances             |               |       |
+|          |                                   |               |       |
++----------+-----------------------------------+---------------+-------+
+
+Of the above parameters, only ``_total`` ones are straight-forward. The
+others have sometimes strange semantics:
+
+- Xen can report ``mem_node``, if configured statically (as we
+  recommend); but Linux-based hypervisors (KVM, chroot, LXC) do not, and
+  this needs to be configured statically for these values
+- ``mem_hv``, representing unaccounted for memory, is not directly
+  computable; on Xen, it can be seen that on a N GB machine, with 1 GB
+  for dom0 and N-2 GB for instances, there's just a few MB left, instead
+  fo a full 1 GB of RAM; however, the exact value varies with the total
+  memory size (at least)
+- ``cpu_node`` only makes sense on Xen (currently), in the case when we
+  restrict dom0; for Linux-based hypervisors, the node itself cannot be
+  easily restricted, so it should be set as an estimate of how "heavy"
+  the node loads will be
+
+Since these two values cannot be auto-computed from the node, we need to
+be able to declare a default at cluster level (debatable how useful they
+are at node group level); the proposal is to do this via a cluster-level
+``hv_state`` dict (per hypervisor).
+
+Beside the per-hypervisor attributes, we also have disk attributes,
+which are queried directly on the node (without hypervisor
+involvment). The are stored in a separate attribute (``disk_state``),
+which is indexed per storage type and name; currently this will be just
+``LD_LV`` and the volume name as key.
+
++-------------+-------------------------+--------------------+--------+
+|Name         |Description              |Current state       |Type    |
+|             |                         |                    |        |
++=============+=========================+====================+========+
+|disk_total   |Total disk size          |Queried at runtime  |int     |
+|             |                         |                    |        |
++-------------+-------------------------+--------------------+--------+
+|disk_reserved|Reserved disk size; this |None used in Ganeti;|int     |
+|             |is a lower limit on the  |htools has a        |        |
+|             |free space, if such a    |parameter for this  |        |
+|             |limit is desired         |                    |        |
++-------------+-------------------------+--------------------+--------+
+|disk_overhead|Disk that is expected to |None used in Ganeti;|int     |
+|             |be used by other volumes |htools detects this |        |
+|             |(set via                 |at runtime          |        |
+|             |``reserved_lvs``);       |                    |        |
+|             |usually should be zero   |                    |        |
++-------------+-------------------------+--------------------+--------+
+
+
+Instance parameters
+~~~~~~~~~~~~~~~~~~~
+
+New instance parameters, needed especially for supporting the new memory
+model:
+
++--------------+----------------------------------+-----------------+------+
+|Name          |Description                       |Current status   |Type  |
+|              |                                  |                 |      |
++==============+==================================+=================+======+
+|offline       |Whether the instance is in        |Not supported    |bool  |
+|              |“permanent” offline mode; this is |                 |      |
+|              |stronger than the "admin_down”    |                 |      |
+|              |state, and is similar to the node |                 |      |
+|              |offline attribute                 |                 |      |
++--------------+----------------------------------+-----------------+------+
+|be/max_memory |The maximum memory the instance is|Not existent, but|int   |
+|              |allowed                           |virtually        |      |
+|              |                                  |identical to     |      |
+|              |                                  |memory           |      |
++--------------+----------------------------------+-----------------+------+
+
+HTools changes
+--------------
+
+All the new parameters (node, instance, cluster, not so much disk) will
+need to be taken into account by HTools, both in balancing and in
+capacity computation.
+
+Since the Ganeti's cluster model is much enhanced, Ganeti can also
+export its own reserved/overhead variables, and as such HTools can make
+less “guesses” as to the difference in values.
+
+.. admonition:: FIXME
+
+   Need to detail more the htools changes; the model is clear to me, but
+   need to write it down.
+
+.. vim: set textwidth=72 :
+.. Local Variables:
+.. mode: rst
+.. fill-column: 72
+.. End:
index 9f3a0ea..c175476 100644 (file)
@@ -47,7 +47,7 @@ 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.
+  least 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.
diff --git a/doc/design-virtual-clusters.rst b/doc/design-virtual-clusters.rst
new file mode 100644 (file)
index 0000000..2877c0e
--- /dev/null
@@ -0,0 +1,245 @@
+==========================
+ Virtual clusters support
+==========================
+
+
+Introduction
+============
+
+Currently there are two ways to test the Ganeti (including HTools) code
+base:
+
+- unittests, which run using mocks as normal user and test small bits of
+  the code
+- QA/burnin/live-test, which require actual hardware (either physical or
+  virtual) and will build an actual cluster, with one machine to one
+  node correspondence
+
+The difference in time between these two is significant:
+
+- the unittests run in about 1-2 minutes
+- a so-called ‘quick’ QA (without burnin) runs in about an hour, and a
+  full QA could be double that time
+
+On one hand, the unittests have a clear advantage: quick to run, not
+requiring many machines, but on the other hand QA is actually able to
+run end-to-end tests (including HTools, for example).
+
+Ideally, we would have an intermediate step between these two extremes:
+be able to test most, if not all, of Ganeti's functionality but without
+requiring actual hardware, full machine ownership or root access.
+
+
+Current situation
+=================
+
+Ganeti
+------
+
+It is possible, given a manually built ``config.data`` and
+``_autoconf.py``, to run the masterd under the current user as a
+single-node cluster master. However, the node daemon and related
+functionality (cluster initialisation, master failover, etc.) are not
+directly runnable in this model.
+
+Also, masterd only works as a master of a single node cluster, due to
+our current “hostname” method of identifying nodes, which results in a
+limit of maximum one node daemon per machine, unless we use multiple
+name and IP aliases.
+
+HTools
+------
+
+In HTools the situation is better, since it doesn't have to deal with
+actual machine management: all tools can use a custom LUXI path, and can
+even load RAPI data from the filesystem (so the RAPI backend can be
+tested), and both the ‘text’ backend for hbal/hspace and the input files
+for hail are text-based, loaded from the file-system.
+
+Proposed changes
+================
+
+The end-goal is to have full support for “virtual clusters”, i.e. be
+able to run a “big” (hundreds of virtual nodes and towards thousands of
+virtual instances) on a reasonably powerful, but single machine, under a
+single user account and without any special privileges.
+
+This would have significant advantages:
+
+- being able to test end-to-end certain changes, without requiring a
+  complicated setup
+- better able to estimate Ganeti's behaviour and performance as the
+  cluster size grows; this is something that we haven't been able to
+  test reliably yet, and as such we still have not yet diagnosed
+  scaling problems
+- easier integration with external tools (and even with HTools)
+
+``masterd``
+-----------
+
+As described above, ``masterd`` already works reasonably well in a
+virtual setup, as it won't execute external programs and it shouldn't
+directly read files from the local filesystem (or at least not
+virtualisation-related, as the master node can be a non-vm_capable
+node).
+
+``noded``
+---------
+
+The node daemon executes many privileged operations, but they can be
+split in a few general categories:
+
++---------------+-----------------------+------------------------------------+
+|Category       |Description            |Solution                            |
++===============+=======================+====================================+
+|disk operations|Disk creation and      |Use only diskless or file-based     |
+|               |removal                |instances                           |
++---------------+-----------------------+------------------------------------+
+|disk query     |Node disk total/free,  |Not supported currently, could use  |
+|               |used in node listing   |file-based                          |
+|               |and htools             |                                    |
++---------------+-----------------------+------------------------------------+
+|hypervisor     |Instance start, stop   |Use the *fake* hypervisor           |
+|operations     |and query              |                                    |
++---------------+-----------------------+------------------------------------+
+|instance       |Bridge existence query |Unprivileged operation, can be used |
+|networking     |                       |with an existing bridge at system   |
+|               |                       |level or use NIC-less instances     |
++---------------+-----------------------+------------------------------------+
+|instance OS    |OS add, OS rename,     |Only used with non diskless         |
+|operations     |export and import      |instances; could work with custom OS|
+|               |                       |scripts (that just ``dd`` without   |
+|               |                       |mounting filesystems                |
++---------------+-----------------------+------------------------------------+
+|node networking|IP address management  |Not supported; Ganeti will need to  |
+|               |(master ip), IP query, |work without a master IP. For the IP|
+|               |etc.                   |query operations, the test machine  |
+|               |                       |would need externally-configured IPs|
++---------------+-----------------------+------------------------------------+
+|node setup     |ssh, /etc/hosts, so on |Can already be disabled from the    |
+|               |                       |cluster config                      |
++---------------+-----------------------+------------------------------------+
+|master failover|start/stop the master  |Doable (as long as we use a single  |
+|               |daemon                 |user), might get tricky w.r.t. paths|
+|               |                       |to executables                      |
++---------------+-----------------------+------------------------------------+
+|file upload    |Uploading of system    |The only issue could be with system |
+|               |files, job queue files |files, which are not owned by the   |
+|               |and ganeti config      |current user; internal ganeti files |
+|               |                       |should be working fine              |
++---------------+-----------------------+------------------------------------+
+|node oob       |Out-of-band commands   |Since these are user-defined, we can|
+|               |                       |mock them easily                    |
++---------------+-----------------------+------------------------------------+
+|node OS        |List the existing OSes |No special privileges needed, so    |
+|discovery      |and their properties   |works fine as-is                    |
++---------------+-----------------------+------------------------------------+
+|hooks          |Running hooks for given|No special privileges needed        |
+|               |operations             |                                    |
++---------------+-----------------------+------------------------------------+
+|iallocator     |Calling an iallocator  |No special privileges needed        |
+|               |script                 |                                    |
++---------------+-----------------------+------------------------------------+
+|export/import  |Exporting and importing|When exporting/importing file-based |
+|               |instances              |instances, this should work, as the |
+|               |                       |listening ports are dynamically     |
+|               |                       |chosen                              |
++---------------+-----------------------+------------------------------------+
+|hypervisor     |The validation of      |As long as the hypervisors don't    |
+|validation     |hypervisor parameters  |call to privileged commands, it     |
+|               |                       |should work                         |
++---------------+-----------------------+------------------------------------+
+|node powercycle|The ability to power   |Privileged, so not supported, but   |
+|               |cycle a node remotely  |anyway not very interesting for     |
+|               |                       |testing                             |
++---------------+-----------------------+------------------------------------+
+
+It seems that much of the functionality works as is, or could work with
+small adjustments, even in a non-privileged setup. The bigger problem is
+the actual use of multiple node daemons per machine.
+
+Multiple ``noded`` per machine
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Currently Ganeti identifies node simply by their hostname. Since
+changing this method would imply significant changes to tracking the
+nodes, the proposal is to simply have as many IPs per the (single)
+machine that is used for tests as nodes, and have each IP correspond to
+a different name, and thus no changes are needed to the core RPC
+library. Unfortunately this has the downside of requiring root rights
+for setting up the extra IPs and hostnames.
+
+An alternative option is to implement per-node IP/port support in Ganeti
+(especially in the RPC layer), which would eliminate the root rights. We
+expect that this will get implemented as a second step of this design.
+
+The only remaining problem is with sharing the ``localstatedir``
+structure (lib, run, log) amongst the daemons, for which we propose to
+add a command line parameter which can override this path (via injection
+into ``_autoconf.py``). The rationale for this is two-fold:
+
+- having two or more node daemons writing to the same directory might
+  introduce artificial scenarios not existent in real life; currently
+  noded either owns the entire ``/var/lib/ganeti`` directory or shares
+  it with masterd, but never with another noded
+- having separate directories allows cluster verify to check correctly
+  consistency of file upload operations; otherwise, as long as one node
+  daemon wrote a file successfully, the results from all others are
+  “lost”
+
+
+``rapi``
+--------
+
+The RAPI daemon is not privileged and furthermore we only need one per
+cluster, so it presents no issues.
+
+``confd``
+---------
+
+``confd`` has somewhat the same issues as the node daemon regarding
+multiple daemons per machine, but the per-address binding still works.
+
+``ganeti-watcher``
+------------------
+
+Since the startup of daemons will be customised with per-IP binds, the
+watcher either has to be modified to not activate the daemons, or the
+start-stop tool has to take this into account. Due to watcher's use of
+the hostname, it's recommended that the master node is set to the
+machine hostname (also a requirement for the master daemon).
+
+CLI scripts
+-----------
+
+As long as the master node is set to the machine hostname, these should
+work fine.
+
+Cluster initialisation
+----------------------
+
+It could be possible that the cluster initialisation procedure is a bit
+more involved (this was not tried yet). In any case, we can build a
+``config.data`` file manually, without having to actually run
+``gnt-cluster init``.
+
+Needed tools
+============
+
+With the above investigation results in mind, the only thing we need
+are:
+
+- a tool to setup per-virtual node tree structure of ``localstatedir``
+  and setup correctly the extra IP/hostnames
+- changes to the startup daemon tools to launch correctly the daemons
+  per virtual node
+- changes to ``noded`` to override the ``localstatedir`` path
+- documentation for running such a virtual cluster
+- and eventual small fixes to the node daemon backend functionality, to
+  better separate privileged and non-privileged code
+
+.. vim: set textwidth=72 :
+.. Local Variables:
+.. mode: rst
+.. fill-column: 72
+.. End:
index f9646fb..48544fb 100644 (file)
@@ -1,6 +1,8 @@
 Developer notes
 ===============
 
+.. highlight:: shell-example
+
 Build dependencies
 ------------------
 
@@ -19,7 +21,7 @@ Most dependencies from :doc:`install-quick`, plus (for Python):
 - `pep8 <https://github.com/jcrocholl/pep8/>`_
 
 Note that for pylint, at the current moment the following versions
-need to be used::
+must be used::
 
     $ pylint --version
     pylint 0.21.1,
@@ -36,28 +38,65 @@ document, plus:
 - `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)
+  linter (equivalent to pylint for Python), recommended version 1.8 or
+  above (tested with 1.8.15)
 - the `QuickCheck <http://hackage.haskell.org/package/QuickCheck>`_
   library, version 2.x
 - ``hpc``, which comes with the compiler, so you should already have
   it
+- `shelltestrunner <http://joyful.com/shelltestrunner>`_, used for
+  running unit-tests
+
+Under Debian Wheezy or later, these can be installed (on top of the
+required ones from the quick install document) via::
 
-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
 
-  apt-get install libghc-quickcheck2-dev hscolour hlint
+Or alternatively via ``cabal``::
+
+  $ cabal install quickcheck hscolour hlint shelltestrunner
 
 
 Configuring for development
 ---------------------------
 
-.. highlight:: sh
-
 Run the following command (only use ``PYTHON=...`` if you need to use a
 different python version)::
 
-  ./autogen.sh && \
-  ./configure --prefix=/usr/local --sysconfdir=/etc --localstatedir=/var
+  $ ./autogen.sh && \
+    ./configure --prefix=/usr/local --sysconfdir=/etc --localstatedir=/var
+
+Haskell development notes
+-------------------------
+
+There are a few things which can help writing or debugging the Haskell
+code.
+
+You can run the Haskell linter :command:`hlint` via::
+
+  $ make hlint
+
+This is not enabled by default (as the htools component is
+optional). The above command will generate both output on the terminal
+and, if any warnings are found, also an HTML report at
+``doc/hs-lint.html``.
+
+When writing or debugging TemplateHaskell code, it's useful to see
+what the splices are converted to. This can be done via::
+
+  $ make HEXTRA="-ddump-splices"
+
+Due to the way TemplateHaskell works, it's not straightforward to
+build profiling code. The recommended way is to run ``make hs-prof``,
+or alternatively the manual sequence is::
+
+  $ make clean
+  $ make htools/htools HEXTRA="-osuf .o"
+  $ rm htools/htools
+  $ make htools/htools HEXTRA="-osuf .prof_o -prof -auto-all"
+
+This will build the binary twice, per the TemplateHaskell
+documentation, the second one with profiling enabled.
 
 
 Packaging notes
index cd137bd..f16d35b 100644 (file)
@@ -1,6 +1,8 @@
 #!/bin/sh
-# ganeti node daemon starter script
-# based on skeleton from Debian GNU/Linux
+# ganeti daemons init script
+#
+# chkconfig: 2345 99 01
+# description: Ganeti Cluster Manager
 ### BEGIN INIT INFO
 # Provides:          ganeti
 # Required-Start:    $syslog $remote_fs
@@ -20,7 +22,14 @@ SCRIPTNAME="@SYSCONFDIR@/init.d/ganeti"
 
 test -f "$DAEMON_UTIL" || exit 0
 
-. /lib/lsb/init-functions
+if [ -r /lib/lsb/init-functions ]; then
+  . /lib/lsb/init-functions
+elif [ -r /etc/rc.d/init.d/functions ]; then
+  . /etc/rc.d/init.d/functions
+else
+  echo "Unable to find init functions"
+  exit 1
+fi
 
 check_exitcode() {
     RC=$1
@@ -75,6 +84,30 @@ stop_all() {
     done
 }
 
+status_all() {
+    local daemons="$1" status ret
+
+    if [ -z "$daemons" ]; then
+      daemons=$($DAEMON_UTIL list-start-daemons)
+    fi
+
+    status=0
+
+    for i in $daemons; do
+      if status_of_proc $($DAEMON_UTIL daemon-executable $i) $i; then
+          ret=0
+      else
+          ret=$?
+          # Use exit code from first failed call
+          if [ "$status" -eq 0 ]; then
+              status=$ret
+          fi
+      fi
+    done
+
+    exit $status
+}
+
 if [ -n "$2" ] && ! errmsg=$($DAEMON_UTIL is-daemon-name "$2" 2>&1); then
     log_failure_msg "$errmsg"
     exit 1
@@ -94,6 +127,9 @@ case "$1" in
         stop_all "$2"
         start_all "$2"
         ;;
+    status)
+        status_all "$2"
+        ;;
     *)
         log_success_msg "Usage: $SCRIPTNAME {start|stop|force-reload|restart}"
         exit 1
diff --git a/doc/examples/rapi_testutils.py b/doc/examples/rapi_testutils.py
new file mode 100755 (executable)
index 0000000..8a4ceab
--- /dev/null
@@ -0,0 +1,70 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2012 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.
+
+
+"""Example for using L{ganeti.rapi.testutils}"""
+
+import logging
+
+from ganeti import rapi
+
+import ganeti.rapi.testutils
+
+
+def main():
+  # Disable log output
+  logging.getLogger("").setLevel(logging.CRITICAL)
+
+  cl = rapi.testutils.InputTestClient()
+
+  print "Testing features ..."
+  assert isinstance(cl.GetFeatures(), list)
+
+  print "Testing node evacuation ..."
+  result = cl.EvacuateNode("inst1.example.com",
+                           mode=rapi.client.NODE_EVAC_PRI)
+  assert result is NotImplemented
+
+  print "Testing listing instances ..."
+  for bulk in [False, True]:
+    result = cl.GetInstances(bulk=bulk)
+    assert result is NotImplemented
+
+  print "Testing renaming instance ..."
+  result = cl.RenameInstance("inst1.example.com", "inst2.example.com")
+  assert result is NotImplemented
+
+  print "Testing renaming instance with error ..."
+  try:
+    # This test deliberately uses an invalid value for the boolean parameter
+    # "ip_check"
+    result = cl.RenameInstance("inst1.example.com", "inst2.example.com",
+                               ip_check=["non-boolean", "value"])
+  except rapi.testutils.VerificationError:
+    # Verification failed as expected
+    pass
+  else:
+    raise Exception("This test should have failed")
+
+  print "Success!"
+
+
+if __name__ == "__main__":
+  main()
index a7f905d..f1c255e 100644 (file)
@@ -5,37 +5,86 @@ Glossary
 .. if you add new entries, keep the alphabetical sorting!
 
 .. glossary::
+  :sorted:
 
-  BE Parameter
-    BE stands for Backend. BE parameters are hypervisor-independent
+  ballooning
+    A term describing runtime, dynamic changes to an instance's memory,
+    without having to reboot the instance. Depending on the hypervisor
+    and configuration, the changes need to be initiated manually, or
+    they can be automatically initiated by the hypervisor based on the
+    node and instances memory usage.
+
+  BE parameter
+    BE stands for *backend*. BE parameters are hypervisor-independent
     instance parameters such as the amount of RAM/virtual CPUs it has
     been allocated.
 
+  DRBD
+    A block device driver that can be used to build RAID1 across the
+    network or even shared storage, while using only locally-attached
+    storage.
+
+  HV parameter
+    HV stands for *hypervisor*. HV parameters are the ones that describe
+    the virtualization-specific aspects of the instance; for example,
+    what kernel to use to boot the instance (if any), or what emulation
+    model to use for the emulated hard drives.
+
   HVM
-    Hardware virtualization mode, where the virtual machine is
-    oblivious to the fact that's being virtualized and all the
-    hardware is emulated.
+    Hardware virtualization mode, where the virtual machine is oblivious
+    to the fact that's being virtualized and all the hardware is
+    emulated.
 
   LogicalUnit
-    The code associated with an OpCode, e.g. the code that implements
-    the startup of an instance.
+    The code associated with an :term:`OpCode`, e.g. the code that
+    implements the startup of an instance.
 
   LUXI
-     Local UniX Interface. The IPC method over unix sockets used between
-     the cli tools and the master daemon.
+     Local UniX Interface. The IPC method over :manpage:`unix(7)`
+     sockets used between the CLI tools/RAPI daemon and the master
+     daemon.
+
+  OOB
+    *Out of Band*. This term describes methods of accessing a machine
+    (or parts of a machine) not via the usual network connection. For
+    example, accessing a remote server via a physical serial console or
+    via a virtual one IPMI counts as out of band access.
 
   OpCode
     A data structure encapsulating a basic cluster operation; for
     example, start instance, add instance, etc.
 
   PVM
-    Para-virtualization mode, where the virtual machine knows it's being
-    virtualized and as such there is no need for hardware emulation.
+    (Xen) Para-virtualization mode, where the virtual machine knows it's
+    being virtualized and as such there is no need for hardware
+    emulation or virtualization.
+
+  SoR
+    *State of Record*. Refers to values/properties that come from an
+    authoritative configuration source. For example, the maximum VCPU
+    over-subscription ratio is a *SoR* value, but the current
+    over-subscription ration (based on how many instances live on the
+    node) is a :term:`SoW` value.
+
+  SoW
+    *State of the World*. Refers to values that describe directly the
+    world, as opposed to values that come from the
+    configuration. Contrast with :term:`SoR`.
+
+  tmem
+    Xen Transcendent Memory
+    (http://en.wikipedia.org/wiki/Transcendent_memory). It is a
+    mechanism used by Xen to provide memory over-subscription.
 
   watcher
-    ``ganeti-watcher`` is a tool that should be run regularly from cron
-    and takes care of restarting failed instances, restarting secondary
-    DRBD devices, etc. For more details, see the man page
+    :command:`ganeti-watcher` is a tool that should be run regularly
+    from cron and takes care of restarting failed instances, restarting
+    secondary DRBD devices, etc. For more details, see the man page
     :manpage:`ganeti-watcher(8)`.
 
+
 .. vim: set textwidth=72 :
+.. Local Variables:
+.. mode: rst
+.. fill-column: 72
+.. End:
index 3cf8bc7..be4c730 100644 (file)
@@ -1,17 +1,18 @@
 Ganeti customisation using hooks
 ================================
 
-Documents Ganeti version 2.5
+Documents Ganeti version 2.6
 
 .. contents::
 
 Introduction
 ------------
 
-
-In order to allow customisation of operations, ganeti runs scripts
-under ``/etc/ganeti/hooks`` based on certain rules.
-
+In order to allow customisation of operations, Ganeti runs scripts in
+sub-directories of ``@SYSCONFDIR@/ganeti/hooks``. These sub-directories
+are named ``$hook-$phase.d``, where ``$phase`` is either ``pre`` or
+``post`` and ``$hook`` matches the directory name given for a hook (e.g.
+``cluster-verify-post.d`` or ``node-add-pre.d``).
 
 This is similar to the ``/etc/network/`` structure present in Debian
 for network interface handling.
@@ -147,16 +148,6 @@ Changes a node's parameters.
 :pre-execution: master node, the target node
 :post-execution: master node, the target node
 
-OP_NODE_EVACUATE
-++++++++++++++++
-
-Relocate secondary instances from a node.
-
-:directory: node-evacuate
-:env. vars: NEW_SECONDARY, NODE_NAME
-:pre-execution: master node, target node
-:post-execution: master node, target node
-
 OP_NODE_MIGRATE
 ++++++++++++++++
 
@@ -304,7 +295,7 @@ OP_INSTANCE_SET_PARAMS
 Modifies the instance parameters.
 
 :directory: instance-modify
-:env. vars: NEW_DISK_TEMPLATE
+:env. vars: NEW_DISK_TEMPLATE, RUNTIME_MEMORY
 :pre-execution: master node, primary and secondary nodes
 :post-execution: master node, primary and secondary nodes
 
@@ -343,16 +334,6 @@ Remove an instance.
 :pre-execution: master node
 :post-execution: master node, primary and secondary nodes
 
-OP_INSTANCE_REPLACE_DISKS
-+++++++++++++++++++++++++
-
-Replace an instance's disks.
-
-:directory: mirror-replace
-:env. vars: MODE, NEW_SECONDARY, OLD_SECONDARY
-:pre-execution: master node, primary and secondary nodes
-:post-execution: master node, primary and secondary nodes
-
 OP_INSTANCE_GROW_DISK
 +++++++++++++++++++++
 
@@ -472,6 +453,28 @@ Modifies the cluster parameters.
 :pre-execution: master node
 :post-execution: master node
 
+Virtual operation :pyeval:`constants.FAKE_OP_MASTER_TURNUP`
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+This doesn't correspond to an actual op-code, but it is called when the
+master IP is activated.
+
+:directory: master-ip-turnup
+:env. vars: MASTER_NETDEV, MASTER_IP, MASTER_NETMASK, CLUSTER_IP_VERSION
+:pre-execution: master node
+:post-execution: master node
+
+Virtual operation :pyeval:`constants.FAKE_OP_MASTER_TURNDOWN`
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+This doesn't correspond to an actual op-code, but it is called when the
+master IP is deactivated.
+
+:directory: master-ip-turndown
+:env. vars: MASTER_NETDEV, MASTER_IP, MASTER_NETMASK, CLUSTER_IP_VERSION
+:pre-execution: master node
+:post-execution: master node
+
 
 Obsolete operations
 ~~~~~~~~~~~~~~~~~~~
@@ -529,6 +532,9 @@ Specialised variables
 This is the list of variables which are specific to one or more
 operations.
 
+CLUSTER_IP_VERSION
+  IP version of the master IP (4 or 6)
+
 INSTANCE_NAME
   The name of the instance which is the target of the operation.
 
@@ -601,6 +607,15 @@ MASTER_CAPABLE
 VM_CAPABLE
   Whether the node can host instances.
 
+MASTER_NETDEV
+  Network device of the master IP
+
+MASTER_IP
+  The master IP
+
+MASTER_NETMASK
+  Netmask of the master IP
+
 INSTANCE_TAGS
   A space-delimited list of the instance's tags.
 
index 26cc808..99cc5e4 100644 (file)
@@ -1,7 +1,7 @@
 Ganeti automatic instance allocation
 ====================================
 
-Documents Ganeti version 2.5
+Documents Ganeti version 2.6
 
 .. contents::
 
@@ -41,7 +41,7 @@ using the first one whose filename matches the one given by the user.
 Command line interface changes
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-The node selection options in instanece add and instance replace disks
+The node selection options in instance add and instance replace disks
 can be replace by the new ``--iallocator=NAME`` option (shortened to
 ``-I``), which will cause the auto-assignement of nodes with the
 passed iallocator. The selected node(s) will be show as part of the
@@ -90,6 +90,10 @@ cluster_tags
 enabled_hypervisors
   the list of enabled hypervisors
 
+ipolicy
+  the cluster-wide instance policy (for information; the per-node group
+  values take precedence and should be used instead)
+
 request
   a dictionary containing the details of the request; the keys vary
   depending on the type of operation that's being requested, as
@@ -105,13 +109,15 @@ nodegroups
   alloc_policy
     the allocation policy of the node group (consult the semantics of
     this attribute in the :manpage:`gnt-group(8)` manpage)
+  ipolicy
+    the instance policy of the node group
 
 instances
   a dictionary with the data for the current existing instance on the
   cluster, indexed by instance name; the contents are similar to the
   instance definitions for the allocate mode, with the addition of:
 
-  admin_up
+  admin_state
     if this instance is set to run (but not the actual status of the
     instance)
 
@@ -279,9 +285,11 @@ Allocation needs, in addition:
 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*
+     a list of nodes to move the instance away from; for DRBD-based
+     instances, this will contain a single node, the current secondary
+     of the instance, whereas for shared-storage instance, this will
+     contain also a single node, the current primary of the instance;
+     type *list of strings*
 
 As for ``node-evacuate``, it needs the following request arguments:
 
index b361957..95a8e12 100644 (file)
@@ -29,6 +29,7 @@ Contents:
    iallocator.rst
    rapi.rst
    move-instance.rst
+   ovfconverter.rst
    devnotes.rst
    news.rst
    glossary.rst
index 54a76ab..8abbfb5 100644 (file)
@@ -5,7 +5,7 @@ Documents Ganeti version |version|
 
 .. contents::
 
-.. highlight:: text
+.. highlight:: shell-example
 
 Introduction
 ------------
@@ -23,7 +23,7 @@ section of the :doc:`admin`. Please refer to that document if you are
 uncertain about the terms we are using.
 
 Ganeti has been developed for Linux and should be distribution-agnostic.
-This documentation will use Debian Lenny as an example system but the
+This documentation will use Debian Squeeze as an example system but the
 examples can be translated to any other distribution. You are expected
 to be familiar with your distribution, its package management system,
 and Xen or KVM before trying to use Ganeti.
@@ -51,8 +51,9 @@ are better as they can support more memory.
 Any disk drive recognized by Linux (``IDE``/``SCSI``/``SATA``/etc.) is
 supported in Ganeti. Note that no shared storage (e.g. ``SAN``) is
 needed to get high-availability features (but of course, one can be used
-to store the images). It is highly recommended to use more than one disk
-drive to improve speed. But Ganeti also works with one disk per machine.
+to store the images). Whilte it is highly recommended to use more than
+one disk drive in order to improve speed, Ganeti also works with one
+disk per machine.
 
 Installing the base system
 ++++++++++++++++++++++++++
@@ -69,6 +70,10 @@ all Ganeti features. The volume group name Ganeti uses (by default) is
 You can also use file-based storage only, without LVM, but this setup is
 not detailed in this document.
 
+If you choose to use RBD-based instances, there's no need for LVM
+provisioning. However, this feature is experimental, and is not yet
+recommended for production clusters.
+
 While you can use an existing system, please note that the Ganeti
 installation is intrusive in terms of changes to the system
 configuration, and it's best to use a newly-installed system without
@@ -88,9 +93,9 @@ and not just *node1*.
 
 .. admonition:: Debian
 
-   Debian Lenny and Etch configures the hostname differently than you
-   need it for Ganeti. For example, this is what Etch puts in
-   ``/etc/hosts`` in certain situations::
+   Debian usually configures the hostname differently than you need it
+   for Ganeti. For example, this is what it puts in ``/etc/hosts`` in
+   certain situations::
 
      127.0.0.1       localhost
      127.0.1.1       node1.example.com node1
@@ -98,7 +103,7 @@ and not just *node1*.
    but for Ganeti you need to have::
 
      127.0.0.1       localhost
-     192.0.2.1     node1.example.com node1
+     192.0.2.1       node1.example.com node1
 
    replacing ``192.0.2.1`` with your node's address. Also, the file
    ``/etc/hostname`` which configures the hostname of the system
@@ -132,8 +137,9 @@ Installing The Hypervisor
 
 While Ganeti is developed with the ability to modularly run on different
 virtualization environments in mind the only two currently useable on a
-live system are Xen and KVM. Supported Xen versions are: 3.0.3, 3.0.4
-and 3.1.  Supported KVM version are 72 and above.
+live system are Xen and KVM. Supported Xen versions are: 3.0.3 and later
+3.x versions, and 4.x (tested up to 4.1).  Supported KVM versions are 72
+and above.
 
 Please follow your distribution's recommended way to install and set up
 Xen, or install Xen from the upstream source, if you wish, following
@@ -147,9 +153,9 @@ kernels. For KVM no reboot should be necessary.
 
 .. admonition:: Xen on Debian
 
-   Under Lenny or Etch you can install the relevant ``xen-linux-system``
+   Under Debian you can install the relevant ``xen-linux-system``
    package, which will pull in both the hypervisor and the relevant
-   kernel. Also, if you are installing a 32-bit Lenny/Etch, you should
+   kernel. Also, if you are installing a 32-bit system, you should
    install the ``libc6-xen`` package (run ``apt-get install
    libc6-xen``).
 
@@ -165,7 +171,7 @@ the file ``/etc/xen/xend-config.sxp`` by setting the value
 
 For optimum performance when running both CPU and I/O intensive
 instances, it's also recommended that the dom0 is restricted to one CPU
-only, for example by booting with the kernel parameter ``nosmp``.
+only, for example by booting with the kernel parameter ``maxcpus=1``.
 
 It is recommended that you disable xen's automatic save of virtual
 machines at system shutdown and subsequent restore of them at reboot.
@@ -174,7 +180,9 @@ To obtain this make sure the variable ``XENDOMAINS_SAVE`` in the file
 
 If you want to use live migration make sure you have, in the xen config
 file, something that allows the nodes to migrate instances between each
-other. For example::
+other. For example:
+
+.. code-block:: text
 
   (xend-relocation-server yes)
   (xend-relocation-port 8002)
@@ -192,39 +200,46 @@ line assumes that all your nodes have secondary IPs in the
    Besides the ballooning change which you need to set in
    ``/etc/xen/xend-config.sxp``, you need to set the memory and nosmp
    parameters in the file ``/boot/grub/menu.lst``. You need to modify
-   the variable ``xenhopt`` to add ``dom0_mem=1024M`` like this::
+   the variable ``xenhopt`` to add ``dom0_mem=1024M`` like this:
+
+   .. code-block:: text
 
      ## Xen hypervisor options to use with the default Xen boot option
      # xenhopt=dom0_mem=1024M
 
-   and the ``xenkopt`` needs to include the ``nosmp`` option like this::
+   and the ``xenkopt`` needs to include the ``maxcpus`` option like
+   this:
+
+   .. code-block:: text
 
      ## Xen Linux kernel options to use with the default Xen boot option
-     # xenkopt=nosmp
+     # xenkopt=maxcpus=1
 
    Any existing parameters can be left in place: it's ok to have
-   ``xenkopt=console=tty0 nosmp``, for example. After modifying the
+   ``xenkopt=console=tty0 maxcpus=1``, for example. After modifying the
    files, you need to run::
 
-     /sbin/update-grub
+     $ /sbin/update-grub
 
 If you want to run HVM instances too with Ganeti and want VNC access to
 the console of your instances, set the following two entries in
-``/etc/xen/xend-config.sxp``::
+``/etc/xen/xend-config.sxp``:
+
+.. code-block:: text
 
   (vnc-listen '0.0.0.0') (vncpasswd '')
 
 You need to restart the Xen daemon for these settings to take effect::
 
-  /etc/init.d/xend restart
+  $ /etc/init.d/xend restart
 
 Selecting the instance kernel
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 After you have installed Xen, you need to tell Ganeti exactly what
 kernel to use for the instances it will create. This is done by creating
-a symlink from your actual kernel to ``/boot/vmlinuz-2.6-xenU``, and one
-from your initrd to ``/boot/initrd-2.6-xenU`` [#defkernel]_. Note that
+a symlink from your actual kernel to ``/boot/vmlinuz-3-xenU``, and one
+from your initrd to ``/boot/initrd-3-xenU`` [#defkernel]_. Note that
 if you don't use an initrd for the domU kernel, you don't need to create
 the initrd symlink.
 
@@ -233,9 +248,16 @@ the initrd symlink.
    After installation of the ``xen-linux-system`` package, you need to
    run (replace the exact version number with the one you have)::
 
-     cd /boot
-     ln -s vmlinuz-2.6.26-1-xen-amd64 vmlinuz-2.6-xenU
-     ln -s initrd.img-2.6.26-1-xen-amd64 initrd-2.6-xenU
+     $ cd /boot
+     $ ln -s vmlinuz-%2.6.26-1%-xen-amd64 vmlinuz-3-xenU
+     $ ln -s initrd.img-%2.6.26-1%-xen-amd64 initrd-3-xenU
+
+   By default, the initrd doesn't contain the Xen block drivers needed
+   to mount the root device, so it is recommended to update the initrd
+   by following these two steps:
+
+   - edit ``/etc/initramfs-tools/modules`` and add ``xen_blkfront``
+   - run ``update-initramfs -u``
 
 Installing DRBD
 +++++++++++++++
@@ -243,14 +265,14 @@ Installing DRBD
 Recommended on all nodes: DRBD_ is required if you want to use the high
 availability (HA) features of Ganeti, but optional if you don't require
 them or only run Ganeti on single-node clusters. You can upgrade a
-non-HA cluster to an HA one later, but you might need to export and
-re-import all your instances to take advantage of the new features.
+non-HA cluster to an HA one later, but you might need to convert all
+your instances to DRBD to take advantage of the new features.
 
 .. _DRBD: http://www.drbd.org/
 
-Supported DRBD versions: 8.0+. It's recommended to have at least version
-8.0.12. Note that for version 8.2 and newer it is needed to pass the
-``usermode_helper=/bin/true`` parameter to the module, either by
+Supported DRBD versions: 8.0-8.3. It's recommended to have at least
+version 8.0.12. Note that for version 8.2 and newer it is needed to pass
+the ``usermode_helper=/bin/true`` parameter to the module, either by
 configuring ``/etc/modules`` or when inserting it manually.
 
 Now the bad news: unless your distribution already provides it
@@ -276,17 +298,19 @@ instances on a node.
    following commands, making sure you are running the target (Xen or
    KVM) kernel::
 
-     apt-get install drbd8-source drbd8-utils
-     m-a update
-     m-a a-i drbd8
-     echo drbd minor_count=128 usermode_helper=/bin/true >> /etc/modules
-     depmod -a
-     modprobe drbd minor_count=128 usermode_helper=/bin/true
+     $ apt-get install drbd8-source drbd8-utils
+     $ m-a update
+     $ m-a a-i drbd8
+     $ echo drbd minor_count=128 usermode_helper=/bin/true >> /etc/modules
+     $ depmod -a
+     $ modprobe drbd minor_count=128 usermode_helper=/bin/true
 
    It is also recommended that you comment out the default resources in
    the ``/etc/drbd.conf`` file, so that the init script doesn't try to
    configure any drbd devices. You can do this by prefixing all
-   *resource* lines in the file with the keyword *skip*, like this::
+   *resource* lines in the file with the keyword *skip*, like this:
+
+   .. code-block:: text
 
      skip {
        resource r0 {
@@ -300,6 +324,90 @@ instances on a node.
        }
      }
 
+Installing RBD
+++++++++++++++
+
+Recommended on all nodes: RBD_ is required if you want to create
+instances with RBD disks residing inside a RADOS cluster (make use of
+the rbd disk template). RBD-based instances can failover or migrate to
+any other node in the ganeti cluster, enabling you to exploit of all
+Ganeti's high availabilily (HA) features.
+
+.. attention::
+   Be careful though: rbd is still experimental! For now it is
+   recommended only for testing purposes.  No sensitive data should be
+   stored there.
+
+.. _RBD: http://ceph.newdream.net/
+
+You will need the ``rbd`` and ``libceph`` kernel modules, the RBD/Ceph
+userspace utils (ceph-common Debian package) and an appropriate
+Ceph/RADOS configuration file on every VM-capable node.
+
+You will also need a working RADOS Cluster accessible by the above
+nodes.
+
+RADOS Cluster
+~~~~~~~~~~~~~
+
+You will need a working RADOS Cluster accesible by all VM-capable nodes
+to use the RBD template. For more information on setting up a RADOS
+Cluster, refer to the `official docs <http://ceph.newdream.net/>`_.
+
+If you want to use a pool for storing RBD disk images other than the
+default (``rbd``), you should first create the pool in the RADOS
+Cluster, and then set the corresponding rbd disk parameter named
+``pool``.
+
+Kernel Modules
+~~~~~~~~~~~~~~
+
+Unless your distribution already provides it, you might need to compile
+the ``rbd`` and ``libceph`` modules from source. You will need Linux
+Kernel 3.2 or above for the kernel modules. Alternatively you will have
+to build them as external modules (from Linux Kernel source 3.2 or
+above), if you want to run a less recent kernel, or your kernel doesn't
+include them.
+
+Userspace Utils
+~~~~~~~~~~~~~~~
+
+The RBD template has been tested with ``ceph-common`` v0.38 and
+above. We recommend using the latest version of ``ceph-common``.
+
+.. admonition:: Debian
+
+   On Debian, you can just install the RBD/Ceph userspace utils with
+   the following command::
+
+      $ apt-get install ceph-common
+
+Configuration file
+~~~~~~~~~~~~~~~~~~
+
+You should also provide an appropriate configuration file
+(``ceph.conf``) in ``/etc/ceph``. For the rbd userspace utils, you'll
+only need to specify the IP addresses of the RADOS Cluster monitors.
+
+.. admonition:: ceph.conf
+
+   Sample configuration file:
+
+   .. code-block:: text
+
+    [mon.a]
+           host = example_monitor_host1
+           mon addr = 1.2.3.4:6789
+    [mon.b]
+           host = example_monitor_host2
+           mon addr = 1.2.3.5:6789
+    [mon.c]
+           host = example_monitor_host3
+           mon addr = 1.2.3.6:6789
+
+For more information, please see the `Ceph Docs
+<http://ceph.newdream.net/docs/latest/>`_
+
 Other required software
 +++++++++++++++++++++++
 
@@ -313,8 +421,8 @@ Configuring the network
 
 **Mandatory** on all nodes.
 
-You can run Ganeti either in "bridge mode" or in "routed mode". In
-bridge mode, the default, the instances network interfaces will be
+You can run Ganeti either in "bridged mode" or in "routed mode". In
+bridged mode, the default, the instances network interfaces will be
 attached to a software bridge running in dom0. Xen by default creates
 such a bridge at startup, but your distribution might have a different
 way to do things, and you'll definitely need to manually set it up under
@@ -332,7 +440,7 @@ interfaces correctly.
 
 By default, under KVM, the "link" parameter you specify per-nic will
 represent, if non-empty, a different routing table name or number to use
-for your instances. This allows insulation between different instance
+for your instances. This allows isolation between different instance
 groups, and different routing policies between node traffic and instance
 traffic.
 
@@ -369,33 +477,33 @@ KVM, and in the main table under Xen).
 
      auto xen-br0
      iface xen-br0 inet static
-        address YOUR_IP_ADDRESS
-        netmask YOUR_NETMASK
-        network YOUR_NETWORK
-        broadcast YOUR_BROADCAST_ADDRESS
-        gateway YOUR_GATEWAY
+        address %YOUR_IP_ADDRESS%
+        netmask %YOUR_NETMASK%
+        network %YOUR_NETWORK%
+        broadcast %YOUR_BROADCAST_ADDRESS%
+        gateway %YOUR_GATEWAY%
         bridge_ports eth0
         bridge_stp off
         bridge_fd 0
         # example for setting manually the bridge address to the eth0 NIC
         up ip link set addr $(cat /sys/class/net/eth0/address) dev $IFACE
 
-The following commands need to be executed on the local console:
+The following commands need to be executed on the local console::
 
-  ifdown eth0
-  ifup xen-br0
+  $ ifdown eth0
+  $ ifup xen-br0
 
 To check if the bridge is setup, use the ``ip`` and ``brctl show``
 commands::
 
-  # ip a show xen-br0
+  $ ip a show xen-br0
   9: xen-br0: <BROADCAST,MULTICAST,UP,10000> mtu 1500 qdisc noqueue
       link/ether 00:20:fc:1e:d5:5d brd ff:ff:ff:ff:ff:ff
       inet 10.1.1.200/24 brd 10.1.1.255 scope global xen-br0
       inet6 fe80::220:fcff:fe1e:d55d/64 scope link
          valid_lft forever preferred_lft forever
 
-  # brctl show xen-br0
+  $ brctl show xen-br0
   bridge name     bridge id               STP enabled     interfaces
   xen-br0         8000.0020fc1ed55d       no              eth0
 
@@ -413,25 +521,27 @@ to do it before trying to initialize the Ganeti cluster. This is done by
 formatting the devices/partitions you want to use for it and then adding
 them to the relevant volume group::
 
-  pvcreate /dev/sda3
-  vgcreate xenvg /dev/sda3
+  $ pvcreate /dev/%sda3%
+  $ vgcreate xenvg /dev/%sda3%
 
 or::
 
-  pvcreate /dev/sdb1
-  pvcreate /dev/sdc1
-  vgcreate xenvg /dev/sdb1 /dev/sdc1
+  $ pvcreate /dev/%sdb1%
+  $ pvcreate /dev/%sdc1%
+  $ vgcreate xenvg /dev/%sdb1% /dev/%sdc1%
 
 If you want to add a device later you can do so with the *vgextend*
 command::
 
-  pvcreate /dev/sdd1
-  vgextend xenvg /dev/sdd1
+  $ pvcreate /dev/%sdd1%
+  $ vgextend xenvg /dev/%sdd1%
 
 Optional: it is recommended to configure LVM not to scan the DRBD
 devices for physical volumes. This can be accomplished by editing
 ``/etc/lvm/lvm.conf`` and adding the ``/dev/drbd[0-9]+`` regular
-expression to the ``filter`` variable, like this::
+expression to the ``filter`` variable, like this:
+
+.. code-block:: text
 
   filter = ["r|/dev/cdrom|", "r|/dev/drbd[0-9]+|" ]
 
@@ -447,20 +557,20 @@ Installing Ganeti
 
 It's now time to install the Ganeti software itself.  Download the
 source from the project page at `<http://code.google.com/p/ganeti/>`_,
-and install it (replace 2.0.0 with the latest version)::
+and install it (replace 2.6.0 with the latest version)::
 
-  tar xvzf ganeti-2.0.0.tar.gz
-  cd ganeti-2.0.0
-  ./configure --localstatedir=/var --sysconfdir=/etc
-  make
-  make install
-  mkdir /srv/ganeti/ /srv/ganeti/os /srv/ganeti/export
+  $ tar xvzf ganeti-%2.6.0%.tar.gz
+  $ cd ganeti-%2.6.0%
+  $ ./configure --localstatedir=/var --sysconfdir=/etc
+  $ make
+  $ make install
+  $ mkdir /srv/ganeti/ /srv/ganeti/os /srv/ganeti/export
 
 You also need to copy the file ``doc/examples/ganeti.initd`` from the
 source archive to ``/etc/init.d/ganeti`` and register it with your
 distribution's startup scripts, for example in Debian::
 
-  update-rc.d ganeti defaults 20 80
+  $ update-rc.d ganeti defaults 20 80
 
 In order to automatically restart failed instances, you need to setup a
 cron job run the *ganeti-watcher* command. A sample cron file is
@@ -477,6 +587,8 @@ distribution mechanisms, will install on the system:
   the python version this can be located in either
   ``lib/python-$ver/site-packages`` or various other locations)
 - a set of programs under ``/usr/local/sbin`` or ``/usr/sbin``
+- if the htools component was enabled, a set of programs unde
+  ``/usr/local/bin`` or ``/usr/bin/``
 - man pages for the above programs
 - a set of tools under the ``lib/ganeti/tools`` directory
 - an example iallocator script (see the admin guide for details) under
@@ -499,13 +611,13 @@ site.  Download it from the project page and follow the instructions in
 the ``README`` file.  Here is the installation procedure (replace 0.9
 with the latest version that is compatible with your ganeti version)::
 
-  cd /usr/local/src/
-  wget http://ganeti.googlecode.com/files/ganeti-instance-debootstrap-0.9.tar.gz
-  tar xzf ganeti-instance-debootstrap-0.9.tar.gz
-  cd ganeti-instance-debootstrap-0.9
-  ./configure
-  make
-  make install
+  $ cd /usr/local/src/
+  $ wget http://ganeti.googlecode.com/files/ganeti-instance-debootstrap-%0.9%.tar.gz
+  $ tar xzf ganeti-instance-debootstrap-%0.9%.tar.gz
+  $ cd ganeti-instance-debootstrap-%0.9%
+  $ ./configure
+  $ make
+  $ make install
 
 In order to use this OS definition, you need to have internet access
 from your nodes and have the *debootstrap*, *dump* and *restore*
@@ -518,21 +630,25 @@ installed.
 
    Use this command on all nodes to install the required packages::
 
-     apt-get install debootstrap dump kpartx
+     $ apt-get install debootstrap dump kpartx
+
+   Or alternatively install the OS definition from the Debian package::
+
+     $ apt-get install ganeti-instance-debootstrap
 
 .. admonition:: KVM
 
    In order for debootstrap instances to be able to shutdown cleanly
-   they must install have basic acpi support inside the instance. Which
-   packages are needed depend on the exact flavor of debian or ubuntu
+   they must install have basic ACPI support inside the instance. Which
+   packages are needed depend on the exact flavor of Debian or Ubuntu
    which you're installing, but the example defaults file has a
-   commented out configuration line that works for debian lenny and
-   squeeze::
+   commented out configuration line that works for Debian Lenny and
+   Squeeze::
 
      EXTRA_PKGS="acpi-support-base,console-tools,udev"
 
-   kbd can be used instead of console-tools, and more packages can be
-   added, of course, if needed.
+   ``kbd`` can be used instead of ``console-tools``, and more packages
+   can be added, of course, if needed.
 
 Alternatively, you can create your own OS definitions. See the manpage
 :manpage:`ganeti-os-interface`.
@@ -546,7 +662,7 @@ The last step is to initialize the cluster. After you have repeated the
 above process on all of your nodes, choose one as the master, and
 execute::
 
-  gnt-cluster init <CLUSTERNAME>
+  $ gnt-cluster init %CLUSTERNAME%
 
 The *CLUSTERNAME* is a hostname, which must be resolvable (e.g. it must
 exist in DNS or in ``/etc/hosts``) by all the nodes in the cluster. You
@@ -560,7 +676,7 @@ hostname used for this must resolve to an IP address reserved
 
 If you want to use a bridge which is not ``xen-br0``, or no bridge at
 all, change it with the ``--nic-parameters`` option. For example to
-bridge on br0 you can say::
+bridge on br0 you can add::
 
   --nic-parameters link=br0
 
@@ -568,8 +684,8 @@ Or to not bridge at all, and use a separate routing table::
 
   --nic-parameters mode=routed,link=100
 
-If you don't have a xen-br0 interface you also have to specify a
-different network interface which will get the cluster ip, on the master
+If you don't have a ``xen-br0`` interface you also have to specify a
+different network interface which will get the cluster IP, on the master
 node, by using the ``--master-netdev <device>`` option.
 
 You can use a different name than ``xenvg`` for the volume group (but
@@ -595,13 +711,14 @@ Hypervisor/Network/Cluster parameters
 
 Please note that the default hypervisor/network/cluster parameters may
 not be the correct one for your environment. Carefully check them, and
-change them at cluster init time, or later with ``gnt-cluster modify``.
+change them either at cluster init time, or later with ``gnt-cluster
+modify``.
 
 Your instance types, networking environment, hypervisor type and version
 may all affect what kind of parameters should be used on your cluster.
 
 For example kvm instances are by default configured to use a host
-kernel, and to be reached via serial console, which works nice for linux
+kernel, and to be reached via serial console, which works nice for Linux
 paravirtualized instances. If you want fully virtualized instances you
 may want to handle their kernel inside the instance, and to use VNC.
 
@@ -614,7 +731,7 @@ After you have initialized your cluster you need to join the other nodes
 to it. You can do so by executing the following command on the master
 node::
 
-  gnt-node add <NODENAME>
+  $ gnt-node add %NODENAME%
 
 Separate replication network
 ++++++++++++++++++++++++++++
@@ -635,7 +752,7 @@ Testing the setup
 
 Execute the ``gnt-node list`` command to see all nodes in the cluster::
 
-  # gnt-node list
+  $ gnt-node list
   Node              DTotal  DFree MTotal MNode MFree Pinst Sinst
   node1.example.com 197404 197404   2047  1896   125     0     0
 
diff --git a/doc/ovfconverter.rst b/doc/ovfconverter.rst
new file mode 100644 (file)
index 0000000..774cf81
--- /dev/null
@@ -0,0 +1,212 @@
+=============
+OVF converter
+=============
+
+Using ``ovfconverter`` from the ``tools`` directory, one can easily
+convert previously exported Ganeti instance into OVF package, supported
+by VMWare, VirtualBox and some other virtualization software. It is
+also possible to use instance exported from such a tool and convert it
+to Ganeti config file, used by ``gnt-backup import`` command.
+
+For the internal design of the converter and more detailed description,
+including listing of available command line options, please refer to
+:doc:`design-ovf-support`
+
+As the amount of Ganeti-specific details, that need to be provided in
+order to import an external instance, is rather large, we will present
+here some examples of importing instances from different sources.
+It is also worth noting that there are some limitations regarding
+support for different hardware.
+
+Limitations on import
+=====================
+
+Network
+-------
+Available modes for the network include ``bridged`` and ``routed``.
+There is no ``NIC`` mode, which is typically used e.g. by VirtualBox.
+For most usecases this should not be of any effect, since if
+``NetworkSection`` contains any networks which are not discovered as
+``bridged`` or ``routed``, the network mode is assigned automatically,
+using Ganeti's cluster defaults.
+
+Backend
+-------
+The only values that are taken into account regarding Virtual Hardware
+(described in ``VirtualHardwareSection`` of the ``.ovf`` file) are:
+
+- number of virtual CPUs
+- RAM memory
+- hard disks
+- networks
+
+Neither USB nor CD-ROM drive are used in Ganeti. We decided to simply
+ignore unused elements of this section, so their presence won't raise
+any warnings.
+
+Operating System
+----------------
+List of operating systems available on a cluster is viewable using
+``gnt-os list`` command. When importing from external source, providing
+OS type in a command line (``--os-type=...``) is **required**. This is
+because even if the type is given in OVF description, it is not detailed
+enough for Ganeti to know which os-specific scripts to use.
+Please note, that instance containing disks may only be imported using
+OS script that supports raw disk images.
+
+References
+----------
+Files listed in ``ovf:References`` section cannot be hyperlinks.
+
+
+Limitations on export
+=====================
+
+Disk content
+------------
+Most Ganeti instances do not contain grub. This results in some
+problems when importing to virtualization software that does expect it.
+Examples of such software include VirtualBox and VMWare.
+
+To avoid trouble, please install grub inside the instance before
+exporting it.
+
+
+Import to VirtualBox
+--------------------
+``format`` option should be set to ``vmdk`` in order for instance to be
+importable by VirtualBox.
+
+Tests using existing versions of VirtualBox (3.16) suggest, that
+VirtualBox does not support disk compression or OVA packaging. In future
+versions this might change.
+
+
+Import to VMWare
+----------------
+Importing Ganeti instance to VMWare was tested using ``ovftool``.
+
+``format`` option should be set to ``vmdk`` in order for instance to be
+importable by VMWare.
+
+Presence of Ganeti section does seem to cause some problems and
+therefore it is recommended to use ``--external`` option on export.
+
+Import of compressed disks generated by ovfconverter was impossible in
+current version of ``ovftool`` (2.1.0). This seems to be related to old
+``vmdk`` version. Since the conversion to ``vmdk`` format is done using
+``qemu-img``, it is possible and in fact expected, that future versions
+of the latter tool will resolve this problem.
+
+
+Import examples
+===============
+
+Ganeti's OVF
+------------
+If you are importing instance created using ``ovfconverter export`` --
+you most probably will not have to provide any additional information.
+In that case, the following is all you need (unless you wish to change
+some configuration options)::
+
+       ovfconverter import ganeti.ovf
+       [...]
+       gnt-instance import -n <node> <instance name>
+
+
+Virtualbox, VMWare and other external sources
+---------------------------------------------
+In case of importing from external source, you will most likely have to
+provide the following details:
+
+- ``os-type`` can be any operating system listed on ``gnt-os list``
+- ``name`` that has to be resolvable, as it will be used as instance
+  name (even if your external instance has a name, it most probably is
+  not resolvable to an IP address)
+
+These are not the only options, but the recommended ones. For the
+complete list of available options please refer to
+`Command Line description <design-ovf-support.rst>`
+
+Minimalistic but complete example of importing Virtualbox's OVF
+instance may look like::
+
+    ovfconverter virtualbox.ovf --os-type=lenny-image \
+      --name=xen.test.i1 --disk-template=diskless
+    [...]
+    gnt-instance import -n node1.xen xen.test.i1
+
+
+
+Export example
+==============
+
+Exporting instance into ``.ovf`` format is pretty streightforward and
+requires little - if any - explanation. The only compulsory detail is
+the required disk format, provided using the ``--format`` option.
+
+Export to another Ganeti instance
+---------------------------------
+If for some reason it is convenient for you to use ``ovfconverter`` to
+move instance between clusters (e.g. because of the disk compression),
+the complete example of export may look like this::
+
+    gnt-backup export -n node1.xen xen.test.i1
+    [...]
+    ovfconverter export --format=vmdk --ova \
+      /srv/ganeti/export/xen.i1.node1.xen/config.ini
+    [...]
+
+The result is then in
+``/srv/ganeti/export/xen.i1.node1.xen/xen.test.i1.ova``
+
+Export to Virtualbox/VMWare/other external tool
+-----------------------------------------------
+Typically, when exporting to external tool we do not want
+Ganeti-specific configuration to be saved. In that case, simply use the
+``--external`` option::
+
+    gnt-backup export -n node1.xen xen.test.i1
+    [...]
+    ovfconverter export --external --output-dir ~/ganeti-instance/ \
+      /srv/ganeti/export/xen.i1.node1.xen/config.ini
+
+
+Known issues
+============
+
+Conversion errors
+-----------------
+If you are encountering trouble when converting the disk, please ensure
+that you have newest ``qemu-img`` version.
+
+OVA and compression
+-------------------
+The compressed disks and OVA packaging do not work correctly in either
+VirtualBox (old version) or VMWare.
+
+VirtualBox (3.16 OSE) does not seem to support those two, so there is
+very little we can do about this.
+
+As for VMWare, the reason behind it not accepting compressed or packed
+instances created by ovfconverter seems to be related to the old vmdk
+version.
+
+Problems on newest VirtualBox
+-----------------------------
+In Oracle VM Virtualbox 4.0+ there seems to be a problem when importing
+any OVF instance created by ovfconverter. Reasons are again unknown,
+this will be investigated.
+
+Disk space
+----------
+The disk space requirements for both import and export are at the moment
+very large - we require free space up to about 3-4 times the size of
+disks. This will most likely be changed in future versions.
+
+
+.. vim: set textwidth=72 :
+.. Local Variables:
+.. mode: rst
+.. fill-column: 72
+.. End:
index 2135795..ac20323 100644 (file)
@@ -166,6 +166,69 @@ likely to succeed or at least start executing.
 Force operation to continue even if it will cause the cluster to become
 inconsistent (e.g. because there are not enough master candidates).
 
+Parameter details
+-----------------
+
+Some parameters are not straight forward, so we describe them in details
+here.
+
+.. _rapi-ipolicy:
+
+``ipolicy``
++++++++++++
+
+The instance policy specification is a dict with the following fields:
+
+.. pyassert::
+
+  constants.IPOLICY_ALL_KEYS == set([constants.ISPECS_MIN,
+                                     constants.ISPECS_MAX,
+                                     constants.ISPECS_STD,
+                                     constants.IPOLICY_DTS,
+                                     constants.IPOLICY_VCPU_RATIO,
+                                     constants.IPOLICY_SPINDLE_RATIO])
+
+
+.. pyassert::
+
+  (set(constants.ISPECS_PARAMETER_TYPES.keys()) ==
+   set([constants.ISPEC_MEM_SIZE,
+        constants.ISPEC_DISK_SIZE,
+        constants.ISPEC_DISK_COUNT,
+        constants.ISPEC_CPU_COUNT,
+        constants.ISPEC_NIC_COUNT,
+        constants.ISPEC_SPINDLE_USE]))
+
+.. |ispec-min| replace:: :pyeval:`constants.ISPECS_MIN`
+.. |ispec-max| replace:: :pyeval:`constants.ISPECS_MAX`
+.. |ispec-std| replace:: :pyeval:`constants.ISPECS_STD`
+
+
+|ispec-min|, |ispec-max|, |ispec-std|
+  A sub- `dict` with the following fields, which sets the limit and standard
+  values of the instances:
+
+  :pyeval:`constants.ISPEC_MEM_SIZE`
+    The size in MiB of the memory used
+  :pyeval:`constants.ISPEC_DISK_SIZE`
+    The size in MiB of the disk used
+  :pyeval:`constants.ISPEC_DISK_COUNT`
+    The numbers of disks used
+  :pyeval:`constants.ISPEC_CPU_COUNT`
+    The numbers of cpus used
+  :pyeval:`constants.ISPEC_NIC_COUNT`
+    The numbers of nics used
+  :pyeval:`constants.ISPEC_SPINDLE_USE`
+    The numbers of virtual disk spindles used by this instance. They are
+    not real in the sense of actual HDD spindles, but useful for
+    accounting the spindle usage on the residing node
+:pyeval:`constants.IPOLICY_DTS`
+  A `list` of disk templates allowed for instances using this policy
+:pyeval:`constants.IPOLICY_VCPU_RATIO`
+  Maximum ratio of virtual to physical CPUs (`float`)
+:pyeval:`constants.IPOLICY_SPINDLE_RATIO`
+  Maximum ratio of instances to their node's ``spindle_count`` (`float`)
+
 Usage examples
 --------------
 
@@ -239,30 +302,13 @@ Resources
 ``/``
 +++++
 
-The root resource.
-
-It supports the following commands: ``GET``.
-
-``GET``
-~~~~~~~
-
-Shows the list of mapped resources.
-
-Returns: a dictionary with 'name' and 'uri' keys for each of them.
+The root resource. Has no function, but for legacy reasons the ``GET``
+method is supported.
 
 ``/2``
 ++++++
 
-The ``/2`` resource, the root of the version 2 API.
-
-It supports the following commands: ``GET``.
-
-``GET``
-~~~~~~~
-
-Show the list of mapped resources.
-
-Returns: a dictionary with ``name`` and ``uri`` keys for each of them.
+Has no function, but for legacy reasons the ``GET`` method is supported.
 
 ``/2/info``
 +++++++++++
@@ -320,6 +366,10 @@ It supports the following commands: ``PUT``.
 
 Redistribute configuration to all nodes. The result will be a job id.
 
+Job result:
+
+.. opcode_result:: OP_CLUSTER_REDIST_CONF
+
 
 ``/2/features``
 +++++++++++++++
@@ -338,12 +388,12 @@ features:
                              rlib2._NODE_EVAC_RES1])
 
 :pyeval:`rlib2._INST_CREATE_REQV1`
-  Instance creation request data version 1 supported.
+  Instance creation request data version 1 supported
 :pyeval:`rlib2._INST_REINSTALL_REQV1`
-  Instance reinstall supports body parameters.
+  Instance reinstall supports body parameters
 :pyeval:`rlib2._NODE_MIGRATE_REQV1`
   Whether migrating a node (``/2/nodes/[node_name]/migrate``) supports
-  request body parameters.
+  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)
@@ -365,6 +415,10 @@ Body parameters:
 
 .. opcode_params:: OP_CLUSTER_SET_PARAMS
 
+Job result:
+
+.. opcode_result:: OP_CLUSTER_SET_PARAMS
+
 
 ``/2/groups``
 +++++++++++++
@@ -395,7 +449,7 @@ If the optional bool *bulk* argument is provided and set to a true value
 (i.e ``?bulk=1``), the output contains detailed information about node
 groups as a list.
 
-Returned fields: :pyeval:`utils.CommaJoin(sorted(rlib2.G_FIELDS))`
+Returned fields: :pyeval:`utils.CommaJoin(sorted(rlib2.G_FIELDS))`.
 
 Example::
 
@@ -436,6 +490,10 @@ Body parameters:
 Earlier versions used a parameter named ``name`` which, while still
 supported, has been renamed to ``group_name``.
 
+Job result:
+
+.. opcode_result:: OP_GROUP_ADD
+
 
 ``/2/groups/[group_name]``
 ++++++++++++++++++++++++++
@@ -450,7 +508,7 @@ It supports the following commands: ``GET``, ``DELETE``.
 Returns information about a node group, similar to the bulk output from
 the node group list.
 
-Returned fields: :pyeval:`utils.CommaJoin(sorted(rlib2.G_FIELDS))`
+Returned fields: :pyeval:`utils.CommaJoin(sorted(rlib2.G_FIELDS))`.
 
 ``DELETE``
 ~~~~~~~~~~
@@ -459,6 +517,10 @@ Deletes a node group.
 
 It supports the ``dry-run`` argument.
 
+Job result:
+
+.. opcode_result:: OP_GROUP_REMOVE
+
 
 ``/2/groups/[group_name]/modify``
 +++++++++++++++++++++++++++++++++
@@ -521,6 +583,10 @@ Body parameters:
 .. opcode_params:: OP_GROUP_ASSIGN_NODES
    :exclude: group_name, force, dry_run
 
+Job result:
+
+.. opcode_result:: OP_GROUP_ASSIGN_NODES
+
 
 ``/2/groups/[group_name]/tags``
 +++++++++++++++++++++++++++++++
@@ -591,7 +657,7 @@ If the optional bool *bulk* argument is provided and set to a true value
 (i.e ``?bulk=1``), the output contains detailed information about
 instances as a list.
 
-Returned fields: :pyeval:`utils.CommaJoin(sorted(rlib2.I_FIELDS))`
+Returned fields: :pyeval:`utils.CommaJoin(sorted(rlib2.I_FIELDS))`.
 
 Example::
 
@@ -666,7 +732,7 @@ It supports the following commands: ``GET``, ``DELETE``.
 Returns information about an instance, similar to the bulk output from
 the instance list.
 
-Returned fields: :pyeval:`utils.CommaJoin(sorted(rlib2.I_FIELDS))`
+Returned fields: :pyeval:`utils.CommaJoin(sorted(rlib2.I_FIELDS))`.
 
 ``DELETE``
 ~~~~~~~~~~
@@ -675,6 +741,10 @@ Deletes an instance.
 
 It supports the ``dry-run`` argument.
 
+Job result:
+
+.. opcode_result:: OP_INSTANCE_REMOVE
+
 
 ``/2/instances/[instance_name]/info``
 +++++++++++++++++++++++++++++++++++++++
@@ -689,6 +759,10 @@ Requests detailed information about the instance. An optional parameter,
 configuration without querying the instance's nodes. The result will be
 a job id.
 
+Job result:
+
+.. opcode_result:: OP_INSTANCE_QUERY_DATA
+
 
 ``/2/instances/[instance_name]/reboot``
 +++++++++++++++++++++++++++++++++++++++
@@ -717,6 +791,10 @@ instance even if secondary disks are failing.
 
 It supports the ``dry-run`` argument.
 
+Job result:
+
+.. opcode_result:: OP_INSTANCE_REBOOT
+
 
 ``/2/instances/[instance_name]/shutdown``
 +++++++++++++++++++++++++++++++++++++++++
@@ -735,6 +813,10 @@ It supports the ``dry-run`` argument.
 .. opcode_params:: OP_INSTANCE_SHUTDOWN
    :exclude: instance_name, dry_run
 
+Job result:
+
+.. opcode_result:: OP_INSTANCE_SHUTDOWN
+
 
 ``/2/instances/[instance_name]/startup``
 ++++++++++++++++++++++++++++++++++++++++
@@ -753,6 +835,11 @@ instance even if secondary disks are failing.
 
 It supports the ``dry-run`` argument.
 
+Job result:
+
+.. opcode_result:: OP_INSTANCE_STARTUP
+
+
 ``/2/instances/[instance_name]/reinstall``
 ++++++++++++++++++++++++++++++++++++++++++++++
 
@@ -799,6 +886,10 @@ Body parameters:
 Ganeti 2.4 and below used query parameters. Those are deprecated and
 should no longer be used.
 
+Job result:
+
+.. opcode_result:: OP_INSTANCE_REPLACE_DISKS
+
 
 ``/2/instances/[instance_name]/activate-disks``
 +++++++++++++++++++++++++++++++++++++++++++++++
@@ -813,6 +904,10 @@ It supports the following commands: ``PUT``.
 Takes the bool parameter ``ignore_size``. When set ignore the recorded
 size (useful for forcing activation when recorded size is wrong).
 
+Job result:
+
+.. opcode_result:: OP_INSTANCE_ACTIVATE_DISKS
+
 
 ``/2/instances/[instance_name]/deactivate-disks``
 +++++++++++++++++++++++++++++++++++++++++++++++++
@@ -826,6 +921,31 @@ It supports the following commands: ``PUT``.
 
 Takes no parameters.
 
+Job result:
+
+.. opcode_result:: OP_INSTANCE_DEACTIVATE_DISKS
+
+
+``/2/instances/[instance_name]/recreate-disks``
++++++++++++++++++++++++++++++++++++++++++++++++++
+
+Recreate disks of an instance. Supports the following commands:
+``POST``.
+
+``POST``
+~~~~~~~~
+
+Returns a job ID.
+
+Body parameters:
+
+.. opcode_params:: OP_INSTANCE_RECREATE_DISKS
+   :exclude: instance_name
+
+Job result:
+
+.. opcode_result:: OP_INSTANCE_RECREATE_DISKS
+
 
 ``/2/instances/[instance_name]/disk/[disk_index]/grow``
 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
@@ -844,6 +964,10 @@ Body parameters:
 .. opcode_params:: OP_INSTANCE_GROW_DISK
    :exclude: instance_name, disk
 
+Job result:
+
+.. opcode_result:: OP_INSTANCE_GROW_DISK
+
 
 ``/2/instances/[instance_name]/prepare-export``
 +++++++++++++++++++++++++++++++++++++++++++++++++
@@ -857,6 +981,10 @@ It supports the following commands: ``PUT``.
 
 Takes one parameter, ``mode``, for the export mode. Returns a job ID.
 
+Job result:
+
+.. opcode_result:: OP_BACKUP_PREPARE
+
 
 ``/2/instances/[instance_name]/export``
 +++++++++++++++++++++++++++++++++++++++++++++++++
@@ -876,6 +1004,10 @@ Body parameters:
    :exclude: instance_name
    :alias: target_node=destination
 
+Job result:
+
+.. opcode_result:: OP_BACKUP_EXPORT
+
 
 ``/2/instances/[instance_name]/migrate``
 ++++++++++++++++++++++++++++++++++++++++
@@ -894,6 +1026,10 @@ Body parameters:
 .. opcode_params:: OP_INSTANCE_MIGRATE
    :exclude: instance_name, live
 
+Job result:
+
+.. opcode_result:: OP_INSTANCE_MIGRATE
+
 
 ``/2/instances/[instance_name]/failover``
 +++++++++++++++++++++++++++++++++++++++++
@@ -912,6 +1048,10 @@ Body parameters:
 .. opcode_params:: OP_INSTANCE_FAILOVER
    :exclude: instance_name
 
+Job result:
+
+.. opcode_result:: OP_INSTANCE_FAILOVER
+
 
 ``/2/instances/[instance_name]/rename``
 ++++++++++++++++++++++++++++++++++++++++
@@ -976,26 +1116,29 @@ console. Contained keys:
      constants.CONS_MESSAGE,
      constants.CONS_SSH,
      constants.CONS_VNC,
+     constants.CONS_SPICE,
      ])
 
 ``instance``
-  Instance name.
+  Instance name
 ``kind``
   Console type, one of :pyeval:`constants.CONS_SSH`,
-  :pyeval:`constants.CONS_VNC` or :pyeval:`constants.CONS_MESSAGE`.
+  :pyeval:`constants.CONS_VNC`, :pyeval:`constants.CONS_SPICE`
+  or :pyeval:`constants.CONS_MESSAGE`
 ``message``
-  Message to display (:pyeval:`constants.CONS_MESSAGE` type only).
+  Message to display (:pyeval:`constants.CONS_MESSAGE` type only)
 ``host``
-  Host to connect to (:pyeval:`constants.CONS_SSH` and
-  :pyeval:`constants.CONS_VNC` only).
+  Host to connect to (:pyeval:`constants.CONS_SSH`,
+  :pyeval:`constants.CONS_VNC` or :pyeval:`constants.CONS_SPICE` only)
 ``port``
-  TCP port to connect to (:pyeval:`constants.CONS_VNC` only).
+  TCP port to connect to (:pyeval:`constants.CONS_VNC` or
+  :pyeval:`constants.CONS_SPICE` only)
 ``user``
-  Username to use (:pyeval:`constants.CONS_SSH` only).
+  Username to use (:pyeval:`constants.CONS_SSH` only)
 ``command``
   Command to execute on machine (:pyeval:`constants.CONS_SSH` only)
 ``display``
-  VNC display number (:pyeval:`constants.CONS_VNC` only).
+  VNC display number (:pyeval:`constants.CONS_VNC` only)
 
 
 ``/2/instances/[instance_name]/tags``
@@ -1058,7 +1201,7 @@ as a list.
 
 Returned fields for bulk requests (unlike other bulk requests, these
 fields are not the same as for per-job requests):
-:pyeval:`utils.CommaJoin(sorted(rlib2.J_FIELDS_BULK))`
+:pyeval:`utils.CommaJoin(sorted(rlib2.J_FIELDS_BULK))`.
 
 ``/2/jobs/[job_id]``
 ++++++++++++++++++++
@@ -1166,14 +1309,14 @@ Waits for changes on a job. Takes the following body parameters in a
 dict:
 
 ``fields``
-  The job fields on which to watch for changes.
+  The job fields on which to watch for changes
 
 ``previous_job_info``
-  Previously received field values or None if not yet available.
+  Previously received field values or None if not yet available
 
 ``previous_log_serial``
   Highest log serial number received so far or None if not yet
-  available.
+  available
 
 Returns None if no changes have been detected and a dict with two keys,
 ``job_info`` and ``log_entries`` otherwise.
@@ -1208,7 +1351,7 @@ If the optional bool *bulk* argument is provided and set to a true value
 (i.e ``?bulk=1``), the output contains detailed information about nodes
 as a list.
 
-Returned fields: :pyeval:`utils.CommaJoin(sorted(rlib2.N_FIELDS))`
+Returned fields: :pyeval:`utils.CommaJoin(sorted(rlib2.N_FIELDS))`.
 
 Example::
 
@@ -1235,7 +1378,22 @@ Returns information about a node.
 
 It supports the following commands: ``GET``.
 
-Returned fields: :pyeval:`utils.CommaJoin(sorted(rlib2.N_FIELDS))`
+Returned fields: :pyeval:`utils.CommaJoin(sorted(rlib2.N_FIELDS))`.
+
+``/2/nodes/[node_name]/powercycle``
++++++++++++++++++++++++++++++++++++
+
+Powercycles a node. Supports the following commands: ``POST``.
+
+``POST``
+~~~~~~~~
+
+Returns a job ID.
+
+Job result:
+
+.. opcode_result:: OP_NODE_POWERCYCLE
+
 
 ``/2/nodes/[node_name]/evacuate``
 +++++++++++++++++++++++++++++++++
@@ -1326,6 +1484,32 @@ be a job id.
 
 It supports the bool ``force`` argument.
 
+Job result:
+
+.. opcode_result:: OP_NODE_SET_PARAMS
+
+
+``/2/nodes/[node_name]/modify``
++++++++++++++++++++++++++++++++
+
+Modifies the parameters of a node. Supports the following commands:
+``POST``.
+
+``POST``
+~~~~~~~~
+
+Returns a job ID.
+
+Body parameters:
+
+.. opcode_params:: OP_NODE_SET_PARAMS
+   :exclude: node_name
+
+Job result:
+
+.. opcode_result:: OP_NODE_SET_PARAMS
+
+
 ``/2/nodes/[node_name]/storage``
 ++++++++++++++++++++++++++++++++
 
@@ -1361,6 +1545,11 @@ and ``name`` (name of the storage unit).  Parameters can be passed
 additionally. Currently only :pyeval:`constants.SF_ALLOCATABLE` (bool)
 is supported. The result will be a job id.
 
+Job result:
+
+.. opcode_result:: OP_NODE_MODIFY_STORAGE
+
+
 ``/2/nodes/[node_name]/storage/repair``
 +++++++++++++++++++++++++++++++++++++++
 
@@ -1380,6 +1569,11 @@ Repairs a storage unit on the node. Requires the parameters
 repaired) and ``name`` (name of the storage unit). The result will be a
 job id.
 
+Job result:
+
+.. opcode_result:: OP_REPAIR_NODE_STORAGE
+
+
 ``/2/nodes/[node_name]/tags``
 +++++++++++++++++++++++++++++
 
index a173357..6bbe9c5 100644 (file)
@@ -5,7 +5,7 @@ Documents Ganeti version |version|
 
 .. contents::
 
-.. highlight:: text
+.. highlight:: shell-example
 
 Introduction
 ------------
@@ -32,16 +32,16 @@ Cluster creation
 Follow the :doc:`install` document and prepare the nodes. Then it's time
 to initialise the cluster::
 
-  node1# gnt-cluster init -s 192.0.2.1 --enabled-hypervisors=xen-pvm example-cluster
-  node1#
+  $ gnt-cluster init -s %192.0.2.1% --enabled-hypervisors=xen-pvm %example-cluster%
+  $
 
 The creation was fine. Let's check that one node we have is functioning
 correctly::
 
-  node1# gnt-node list
+  $ gnt-node list
   Node  DTotal DFree MTotal MNode MFree Pinst Sinst
   node1   1.3T  1.3T  32.0G  1.0G 30.5G     0     0
-  node1# gnt-cluster verify
+  $ gnt-cluster verify
   Mon Oct 26 02:08:51 2009 * Verifying global settings
   Mon Oct 26 02:08:51 2009 * Gathering data (1 nodes)
   Mon Oct 26 02:08:52 2009 * Verifying node status
@@ -51,41 +51,39 @@ correctly::
   Mon Oct 26 02:08:52 2009 * Verifying N+1 Memory redundancy
   Mon Oct 26 02:08:52 2009 * Other Notes
   Mon Oct 26 02:08:52 2009 * Hooks Results
-  node1#
+  $
 
 Since this proceeded correctly, let's add the other two nodes::
 
-  node1# gnt-node add -s 192.0.2.2 node2
+  $ gnt-node add -s %192.0.2.2% %node2%
   -- WARNING --
   Performing this operation is going to replace the ssh daemon keypair
   on the target machine (node2) with the ones of the current one
   and grant full intra-cluster ssh root access to/from it
 
-  The authenticity of host 'node2 (192.0.2.2)' can't be established.
-  RSA key fingerprint is 9f:…
-  Are you sure you want to continue connecting (yes/no)? yes
-  root@node2's password:
+  Unable to verify hostkey of host xen-devi-5.fra.corp.google.com:
+  f7:…. Do you want to accept it?
+  y/[n]/?: %y%
+  Mon Oct 26 02:11:53 2009  Authentication to node2 via public key failed, trying password
+  root password:
   Mon Oct 26 02:11:54 2009  - INFO: Node will be a master candidate
-  node1# gnt-node add -s 192.0.2.3 node3
+  $ gnt-node add -s %192.0.2.3% %node3%
   -- WARNING --
   Performing this operation is going to replace the ssh daemon keypair
-  on the target machine (node2) with the ones of the current one
+  on the target machine (node3) with the ones of the current one
   and grant full intra-cluster ssh root access to/from it
 
-  The authenticity of host 'node3 (192.0.2.3)' can't be established.
-  RSA key fingerprint is 9f:…
-  Are you sure you want to continue connecting (yes/no)? yes
-  root@node2's password:
-  Mon Oct 26 02:11:54 2009  - INFO: Node will be a master candidate
+  …
+  Mon Oct 26 02:12:43 2009  - INFO: Node will be a master candidate
 
 Checking the cluster status again::
 
-  node1# gnt-node list
+  $ gnt-node list
   Node  DTotal DFree MTotal MNode MFree Pinst Sinst
   node1   1.3T  1.3T  32.0G  1.0G 30.5G     0     0
   node2   1.3T  1.3T  32.0G  1.0G 30.5G     0     0
   node3   1.3T  1.3T  32.0G  1.0G 30.5G     0     0
-  node1# gnt-cluster verify
+  $ gnt-cluster verify
   Mon Oct 26 02:15:14 2009 * Verifying global settings
   Mon Oct 26 02:15:14 2009 * Gathering data (3 nodes)
   Mon Oct 26 02:15:16 2009 * Verifying node status
@@ -95,24 +93,24 @@ Checking the cluster status again::
   Mon Oct 26 02:15:16 2009 * Verifying N+1 Memory redundancy
   Mon Oct 26 02:15:16 2009 * Other Notes
   Mon Oct 26 02:15:16 2009 * Hooks Results
-  node1#
+  $
 
 And let's check that we have a valid OS::
 
-  node1# gnt-os list
+  $ gnt-os list
   Name
   debootstrap
   node1#
 
-Running a burnin
-----------------
+Running a burn-in
+-----------------
 
 Now that the cluster is created, it is time to check that the hardware
 works correctly, that the hypervisor can actually create instances,
 etc. This is done via the debootstrap tool as described in the admin
 guide. Similar output lines are replaced with ``…`` in the below log::
 
-  node1# /usr/lib/ganeti/tools/burnin -o debootstrap -p instance{1..5}
+  $ /usr/lib/ganeti/tools/burnin -o debootstrap -p instance{1..5}
   - Testing global parameters
   - Creating instances
     * instance instance1
@@ -261,10 +259,10 @@ guide. Similar output lines are replaced with ``…`` in the below log::
     * Submitted job ID(s) 235, 236, 237, 238, 239
       waiting for job 235 for instance1
       …
-  node1#
+  $
 
-You can see in the above what operations the burnin does. Ideally, the
-burnin log would proceed successfully through all the steps and end
+You can see in the above what operations the burn-in does. Ideally, the
+burn-in log would proceed successfully through all the steps and end
 cleanly, without throwing errors.
 
 Instance operations
@@ -276,36 +274,36 @@ Creation
 At this point, Ganeti and the hardware seems to be functioning
 correctly, so we'll follow up with creating the instances manually::
 
-  node1# gnt-instance add -t drbd -o debootstrap -s 256m -n node1:node2 instance3
+  $ gnt-instance add -t drbd -o debootstrap -s %256m% -n %node1%:%node2% %instance3%
   Mon Oct 26 04:06:52 2009  - INFO: Selected nodes for instance instance1 via iallocator hail: node2, node3
   Mon Oct 26 04:06:53 2009 * creating instance disks...
   Mon Oct 26 04:06:57 2009 adding instance instance1 to cluster config
   Mon Oct 26 04:06:57 2009  - INFO: Waiting for instance instance1 to sync disks.
-  Mon Oct 26 04:06:57 2009  - INFO: - device disk/0: 20.00% done, 4 estimated seconds remaining
+  Mon Oct 26 04:06:57 2009  - INFO: - device disk/0: 20.00\% done, 4 estimated seconds remaining
   Mon Oct 26 04:07:01 2009  - INFO: Instance instance1's disks are in sync.
   Mon Oct 26 04:07:01 2009 creating os for instance instance1 on node node2
   Mon Oct 26 04:07:01 2009 * running the instance OS create scripts...
   Mon Oct 26 04:07:14 2009 * starting instance...
-  node1# gnt-instance add -t drbd -o debootstrap -s 256m -n node1:node2 instanc<drbd -o debootstrap -s 256m -n node1:node2 instance2
+  $ gnt-instance add -t drbd -o debootstrap -s %256m% -n %node1%:%node2% %instance2%
   Mon Oct 26 04:11:37 2009 * creating instance disks...
   Mon Oct 26 04:11:40 2009 adding instance instance2 to cluster config
   Mon Oct 26 04:11:41 2009  - INFO: Waiting for instance instance2 to sync disks.
-  Mon Oct 26 04:11:41 2009  - INFO: - device disk/0: 35.40% done, 1 estimated seconds remaining
-  Mon Oct 26 04:11:42 2009  - INFO: - device disk/0: 58.50% done, 1 estimated seconds remaining
-  Mon Oct 26 04:11:43 2009  - INFO: - device disk/0: 86.20% done, 0 estimated seconds remaining
-  Mon Oct 26 04:11:44 2009  - INFO: - device disk/0: 92.40% done, 0 estimated seconds remaining
-  Mon Oct 26 04:11:44 2009  - INFO: - device disk/0: 97.00% done, 0 estimated seconds remaining
+  Mon Oct 26 04:11:41 2009  - INFO: - device disk/0: 35.40\% done, 1 estimated seconds remaining
+  Mon Oct 26 04:11:42 2009  - INFO: - device disk/0: 58.50\% done, 1 estimated seconds remaining
+  Mon Oct 26 04:11:43 2009  - INFO: - device disk/0: 86.20\% done, 0 estimated seconds remaining
+  Mon Oct 26 04:11:44 2009  - INFO: - device disk/0: 92.40\% done, 0 estimated seconds remaining
+  Mon Oct 26 04:11:44 2009  - INFO: - device disk/0: 97.00\% done, 0 estimated seconds remaining
   Mon Oct 26 04:11:44 2009  - INFO: Instance instance2's disks are in sync.
   Mon Oct 26 04:11:44 2009 creating os for instance instance2 on node node1
   Mon Oct 26 04:11:44 2009 * running the instance OS create scripts...
   Mon Oct 26 04:11:57 2009 * starting instance...
-  node1#
+  $
 
 The above shows one instance created via an iallocator script, and one
 being created with manual node assignment. The other three instances
 were also created and now it's time to check them::
 
-  node1# gnt-instance list
+  $ gnt-instance list
   Instance  Hypervisor OS          Primary_node Status  Memory
   instance1 xen-pvm    debootstrap node2        running   128M
   instance2 xen-pvm    debootstrap node1        running   128M
@@ -318,7 +316,7 @@ Accessing instances
 
 Accessing an instance's console is easy::
 
-  node1# gnt-instance console instance2
+  $ gnt-instance console %instance2%
   [    0.000000] Bootdata ok (command line is root=/dev/sda1 ro)
   [    0.000000] Linux version 2.6…
   [    0.000000] BIOS-provided physical RAM map:
@@ -347,24 +345,24 @@ At this moment you can login to the instance and, after configuring the
 network (and doing this on all instances), we can check their
 connectivity::
 
-  node1# fping instance{1..5}
+  $ fping %instance{1..5}%
   instance1 is alive
   instance2 is alive
   instance3 is alive
   instance4 is alive
   instance5 is alive
-  node1#
+  $
 
 Removal
 +++++++
 
 Removing unwanted instances is also easy::
 
-  node1# gnt-instance remove instance5
+  $ gnt-instance remove %instance5%
   This will remove the volumes of the instance instance5 (including
   mirrors), thus removing all the data of the instance. Continue?
-  y/[n]/?: y
-  node1#
+  y/[n]/?: %y%
+  $
 
 
 Recovering from hardware failures
@@ -376,7 +374,7 @@ Recovering from node failure
 We are now left with four instances. Assume that at this point, node3,
 which has one primary and one secondary instance, crashes::
 
-  node1# gnt-node info node3
+  $ gnt-node info %node3%
   Node name: node3
     primary ip: 198.51.100.1
     secondary ip: 192.0.2.3
@@ -387,47 +385,47 @@ which has one primary and one secondary instance, crashes::
       - instance4
     secondary for instances:
       - instance1
-  node1# fping node3
+  $ fping %node3%
   node3 is unreachable
 
 At this point, the primary instance of that node (instance4) is down,
 but the secondary instance (instance1) is not affected except it has
 lost disk redundancy::
 
-  node1# fping instance{1,4}
+  $ fping %instance{1,4}%
   instance1 is alive
   instance4 is unreachable
-  node1#
+  $
 
 If we try to check the status of instance4 via the instance info
 command, it fails because it tries to contact node3 which is down::
 
-  node1# gnt-instance info instance4
+  $ gnt-instance info %instance4%
   Failure: command execution error:
   Error checking node node3: Connection failed (113: No route to host)
-  node1#
+  $
 
 So we need to mark node3 as being *offline*, and thus Ganeti won't talk
 to it anymore::
 
-  node1# gnt-node modify -O yes -f node3
+  $ gnt-node modify -O yes -f %node3%
   Mon Oct 26 04:34:12 2009  - WARNING: Not enough master candidates (desired 10, new value will be 2)
   Mon Oct 26 04:34:15 2009  - WARNING: Communication failure to node node3: Connection failed (113: No route to host)
   Modified node node3
    - offline -> True
    - master_candidate -> auto-demotion due to offline
-  node1#
+  $
 
 And now we can failover the instance::
 
-  node1# gnt-instance failover --ignore-consistency instance4
+  $ gnt-instance failover --ignore-consistency %instance4%
   Failover will happen to image instance4. This requires a shutdown of
   the instance. Continue?
-  y/[n]/?: y
+  y/[n]/?: %y%
   Mon Oct 26 04:35:34 2009 * checking disk consistency between source and target
   Failure: command execution error:
   Disk disk/0 is degraded on target node, aborting failover.
-  node1# gnt-instance failover --ignore-consistency instance4
+  $ gnt-instance failover --ignore-consistency %instance4%
   Failover will happen to image instance4. This requires a shutdown of
   the instance. Continue?
   y/[n]/?: y
@@ -439,24 +437,24 @@ And now we can failover the instance::
   Mon Oct 26 04:35:47 2009 * activating the instance's disks on target node
   Mon Oct 26 04:35:47 2009  - WARNING: Could not prepare block device disk/0 on node node3 (is_primary=False, pass=1): Node is marked offline
   Mon Oct 26 04:35:48 2009 * starting the instance on the target node
-  node1#
+  $
 
 Note in our first attempt, Ganeti refused to do the failover since it
 wasn't sure what is the status of the instance's disks. We pass the
 ``--ignore-consistency`` flag and then we can failover::
 
-  node1# gnt-instance list
+  $ gnt-instance list
   Instance  Hypervisor OS          Primary_node Status  Memory
   instance1 xen-pvm    debootstrap node2        running   128M
   instance2 xen-pvm    debootstrap node1        running   128M
   instance3 xen-pvm    debootstrap node1        running   128M
   instance4 xen-pvm    debootstrap node1        running   128M
-  node1#
+  $
 
 But at this point, both instance1 and instance4 are without disk
 redundancy::
 
-  node1# gnt-instance info instance1
+  $ gnt-instance info %instance1%
   Instance name: instance1
   UUID: 45173e82-d1fa-417c-8758-7d582ab7eef4
   Serial number: 2
@@ -478,7 +476,8 @@ redundancy::
       - initrd_path: default ()
     Hardware:
       - VCPUs: 1
-      - memory: 128MiB
+      - maxmem: 256MiB
+      - minmem: 512MiB
       - NICs:
         - nic/0: MAC: aa:00:00:78:da:63, IP: None, mode: bridged, link: xen-br0
     Disks:
@@ -502,10 +501,10 @@ to run the node evacuate command which will change from the current
 secondary node to a new one (in this case, we only have two working
 nodes, so all instances will be end on nodes one and two)::
 
-  node1# gnt-node evacuate -I hail node3
+  $ gnt-node evacuate -I hail %node3%
   Relocate instance(s) 'instance1','instance4' from node
    node3 using iallocator hail?
-  y/[n]/?: y
+  y/[n]/?: %y%
   Mon Oct 26 05:05:39 2009  - INFO: Selected new secondary for instance 'instance1': node1
   Mon Oct 26 05:05:40 2009  - INFO: Selected new secondary for instance 'instance4': node2
   Mon Oct 26 05:05:40 2009 Replacing disk(s) 0 for instance1
@@ -526,7 +525,7 @@ nodes, so all instances will be end on nodes one and two)::
   Mon Oct 26 05:05:45 2009  - INFO: Attaching primary drbds to new secondary (standalone => connected)
   Mon Oct 26 05:05:46 2009 STEP 5/6 Sync devices
   Mon Oct 26 05:05:46 2009  - INFO: Waiting for instance instance1 to sync disks.
-  Mon Oct 26 05:05:46 2009  - INFO: - device disk/0: 13.90% done, 7 estimated seconds remaining
+  Mon Oct 26 05:05:46 2009  - INFO: - device disk/0: 13.90\% done, 7 estimated seconds remaining
   Mon Oct 26 05:05:53 2009  - INFO: Instance instance1's disks are in sync.
   Mon Oct 26 05:05:53 2009 STEP 6/6 Removing old storage
   Mon Oct 26 05:05:53 2009  - INFO: Remove logical volumes for 0
@@ -552,7 +551,7 @@ nodes, so all instances will be end on nodes one and two)::
   Mon Oct 26 05:05:55 2009  - INFO: Attaching primary drbds to new secondary (standalone => connected)
   Mon Oct 26 05:05:56 2009 STEP 5/6 Sync devices
   Mon Oct 26 05:05:56 2009  - INFO: Waiting for instance instance4 to sync disks.
-  Mon Oct 26 05:05:56 2009  - INFO: - device disk/0: 12.40% done, 8 estimated seconds remaining
+  Mon Oct 26 05:05:56 2009  - INFO: - device disk/0: 12.40\% done, 8 estimated seconds remaining
   Mon Oct 26 05:06:04 2009  - INFO: Instance instance4's disks are in sync.
   Mon Oct 26 05:06:04 2009 STEP 6/6 Removing old storage
   Mon Oct 26 05:06:04 2009  - INFO: Remove logical volumes for 0
@@ -560,11 +559,11 @@ nodes, so all instances will be end on nodes one and two)::
   Mon Oct 26 05:06:04 2009       Hint: remove unused LVs manually
   Mon Oct 26 05:06:04 2009  - WARNING: Can't remove old LV: Node is marked offline
   Mon Oct 26 05:06:04 2009       Hint: remove unused LVs manually
-  node1#
+  $
 
 And now node3 is completely free of instances and can be repaired::
 
-  node1# gnt-node list
+  $ gnt-node list
   Node  DTotal DFree MTotal MNode MFree Pinst Sinst
   node1   1.3T  1.3T  32.0G  1.0G 30.2G     3     1
   node2   1.3T  1.3T  32.0G  1.0G 30.4G     1     3
@@ -573,26 +572,25 @@ And now node3 is completely free of instances and can be repaired::
 Re-adding a node to the cluster
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-
 Let's say node3 has been repaired and is now ready to be
 reused. Re-adding it is simple::
 
-  node1# gnt-node add --readd node3
+  $ gnt-node add --readd %node3%
   The authenticity of host 'node3 (198.51.100.1)' can't be established.
   RSA key fingerprint is 9f:2e:5a:2e:e0:bd:00:09:e4:5c:32:f2:27:57:7a:f4.
   Are you sure you want to continue connecting (yes/no)? yes
   Mon Oct 26 05:27:39 2009  - INFO: Readding a node, the offline/drained flags were reset
   Mon Oct 26 05:27:39 2009  - INFO: Node will be a master candidate
 
-And is now working again::
+And it is now working again::
 
-  node1# gnt-node list
+  $ gnt-node list
   Node  DTotal DFree MTotal MNode MFree Pinst Sinst
   node1   1.3T  1.3T  32.0G  1.0G 30.2G     3     1
   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 has been built with the htools
+.. note:: If Ganeti has been built with the htools
    component enabled, you can shuffle the instances around to have a
    better use of the nodes.
 
@@ -607,7 +605,7 @@ traffic.
 Let take the cluster status in the above listing, and check what volumes
 are in use::
 
-  node1# gnt-node volumes -o phys,instance node2
+  $ gnt-node volumes -o phys,instance %node2%
   PhysDev   Instance
   /dev/sdb1 instance4
   /dev/sdb1 instance4
@@ -617,14 +615,15 @@ are in use::
   /dev/sdb1 instance3
   /dev/sdb1 instance2
   /dev/sdb1 instance2
-  node1#
+  $
 
 You can see that all instances on node2 have logical volumes on
 ``/dev/sdb1``. Let's simulate a disk failure on that disk::
 
-  node1# ssh node2
-  node2# echo offline > /sys/block/sdb/device/state
-  node2# vgs
+  $ ssh node2
+  # on node2
+  $ echo offline > /sys/block/sdb/device/state
+  $ vgs
     /dev/sdb1: read failed after 0 of 4096 at 0: Input/output error
     /dev/sdb1: read failed after 0 of 4096 at 750153695232: Input/output error
     /dev/sdb1: read failed after 0 of 4096 at 0: Input/output error
@@ -635,12 +634,12 @@ You can see that all instances on node2 have logical volumes on
     Couldn't find device with uuid '954bJA-mNL0-7ydj-sdpW-nc2C-ZrCi-zFp91c'.
     Couldn't find all physical volumes for volume group xenvg.
     Volume group xenvg not found
-  node2#
+  $
 
 At this point, the node is broken and if we are to examine
 instance2 we get (simplified output shown)::
 
-  node1# gnt-instance info instance2
+  $ gnt-instance info %instance2%
   Instance name: instance2
   State: configured to be up, actual state is up
     Nodes:
@@ -654,7 +653,7 @@ instance2 we get (simplified output shown)::
 This instance has a secondary only on node2. Let's verify a primary
 instance of node2::
 
-  node1# gnt-instance info instance1
+  $ gnt-instance info %instance1%
   Instance name: instance1
   State: configured to be up, actual state is up
     Nodes:
@@ -664,7 +663,7 @@ instance of node2::
       - disk/0: drbd8, size 256M
         on primary:   /dev/drbd0 (147:0) in sync, status *DEGRADED* *MISSING DISK*
         on secondary: /dev/drbd3 (147:3) in sync, status ok
-  node1# gnt-instance console instance1
+  $ gnt-instance console %instance1%
 
   Debian GNU/Linux 5.0 instance1 tty1
 
@@ -696,18 +695,18 @@ involved instances.
 
 ::
 
-  node1# gnt-node repair-storage node2 lvm-vg xenvg
+  $ gnt-node repair-storage %node2% lvm-vg %xenvg%
   Mon Oct 26 18:14:03 2009 Repairing storage unit 'xenvg' on node2 ...
-  node1# ssh node2 vgs
-    VG    #PV #LV #SN Attr   VSize   VFree
-    xenvg   1   8   0 wz--n- 673.84G 673.84G
-  node1#
+  $ ssh %node2% vgs
+  VG    #PV #LV #SN Attr   VSize   VFree
+  xenvg   1   8   0 wz--n- 673.84G 673.84G
+  $
 
 This has removed the 'bad' disk from the volume group, which is now left
 with only one PV. We can now replace the disks for the involved
 instances::
 
-  node1# for i in instance{1..4}; do gnt-instance replace-disks -a $i; done
+  $ for i in %instance{1..4}%; do gnt-instance replace-disks -a $i; done
   Mon Oct 26 18:15:38 2009 Replacing disk(s) 0 for instance1
   Mon Oct 26 18:15:38 2009 STEP 1/6 Check device existence
   Mon Oct 26 18:15:38 2009  - INFO: Checking disk/0 on node1
@@ -724,7 +723,7 @@ instances::
   Mon Oct 26 18:15:40 2009  - INFO: Adding new mirror component on node2
   Mon Oct 26 18:15:41 2009 STEP 5/6 Sync devices
   Mon Oct 26 18:15:41 2009  - INFO: Waiting for instance instance1 to sync disks.
-  Mon Oct 26 18:15:41 2009  - INFO: - device disk/0: 12.40% done, 9 estimated seconds remaining
+  Mon Oct 26 18:15:41 2009  - INFO: - device disk/0: 12.40\% done, 9 estimated seconds remaining
   Mon Oct 26 18:15:50 2009  - INFO: Instance instance1's disks are in sync.
   Mon Oct 26 18:15:50 2009 STEP 6/6 Removing old storage
   Mon Oct 26 18:15:50 2009  - INFO: Remove logical volumes for disk/0
@@ -743,7 +742,7 @@ instances::
   …
   Mon Oct 26 18:16:18 2009 STEP 6/6 Removing old storage
   Mon Oct 26 18:16:18 2009  - INFO: Remove logical volumes for disk/0
-  node1#
+  $
 
 As this point, all instances should be healthy again.
 
@@ -752,9 +751,9 @@ As this point, all instances should be healthy again.
    with argument ``-p`` and once secondary instances with argument
    ``-s``, but otherwise the operations are similar::
 
-     node1# gnt-instance replace-disks -p instance1
+     $ gnt-instance replace-disks -p instance1
      …
-     node1# for i in instance{2..4}; do gnt-instance replace-disks -s $i; done
+     $ for i in %instance{2..4}%; do gnt-instance replace-disks -s $i; done
 
 Common cluster problems
 -----------------------
@@ -765,7 +764,7 @@ this exercise we will consider the case of node3, which was broken
 previously and re-added to the cluster without reinstallation. Running
 cluster verify on the cluster reports::
 
-  node1# gnt-cluster verify
+  $ gnt-cluster verify
   Mon Oct 26 18:30:08 2009 * Verifying global settings
   Mon Oct 26 18:30:08 2009 * Gathering data (3 nodes)
   Mon Oct 26 18:30:10 2009 * Verifying node status
@@ -782,7 +781,7 @@ cluster verify on the cluster reports::
   Mon Oct 26 18:30:10 2009 * Verifying N+1 Memory redundancy
   Mon Oct 26 18:30:10 2009 * Other Notes
   Mon Oct 26 18:30:10 2009 * Hooks Results
-  node1#
+  $
 
 Instance status
 +++++++++++++++
@@ -795,7 +794,7 @@ network environment and anyone who tries to use it.
 Ganeti doesn't directly handle this case. It is recommended to logon to
 node3 and run::
 
-  node3# xm destroy instance4
+  $ xm destroy %instance4%
 
 Unallocated DRBD minors
 +++++++++++++++++++++++
@@ -803,9 +802,11 @@ Unallocated DRBD minors
 There are still unallocated DRBD minors on node3. Again, these are not
 handled by Ganeti directly and need to be cleaned up via DRBD commands::
 
-  node3# drbdsetup /dev/drbd0 down
-  node3# drbdsetup /dev/drbd1 down
-  node3#
+  $ ssh %node3%
+  # on node 3
+  $ drbdsetup /dev/drbd%0% down
+  $ drbdsetup /dev/drbd%1% down
+  $
 
 Orphan volumes
 ++++++++++++++
@@ -815,20 +816,22 @@ At this point, the only remaining problem should be the so-called
 disk-replace, or similar situation where Ganeti was not able to recover
 automatically. Here you need to remove them manually via LVM commands::
 
-  node3# lvremove xenvg
-  Do you really want to remove active logical volume "22459cf8-117d-4bea-a1aa-791667d07800.disk0_data"? [y/n]: y
+  $ ssh %node3%
+  # on node3
+  $ lvremove %xenvg%
+  Do you really want to remove active logical volume "22459cf8-117d-4bea-a1aa-791667d07800.disk0_data"? [y/n]: %y%
     Logical volume "22459cf8-117d-4bea-a1aa-791667d07800.disk0_data" successfully removed
-  Do you really want to remove active logical volume "22459cf8-117d-4bea-a1aa-791667d07800.disk0_meta"? [y/n]: y
+  Do you really want to remove active logical volume "22459cf8-117d-4bea-a1aa-791667d07800.disk0_meta"? [y/n]: %y%
     Logical volume "22459cf8-117d-4bea-a1aa-791667d07800.disk0_meta" successfully removed
-  Do you really want to remove active logical volume "1aaf4716-e57f-4101-a8d6-03af5da9dc50.disk0_data"? [y/n]: y
+  Do you really want to remove active logical volume "1aaf4716-e57f-4101-a8d6-03af5da9dc50.disk0_data"? [y/n]: %y%
     Logical volume "1aaf4716-e57f-4101-a8d6-03af5da9dc50.disk0_data" successfully removed
-  Do you really want to remove active logical volume "1aaf4716-e57f-4101-a8d6-03af5da9dc50.disk0_meta"? [y/n]: y
+  Do you really want to remove active logical volume "1aaf4716-e57f-4101-a8d6-03af5da9dc50.disk0_meta"? [y/n]: %y%
     Logical volume "1aaf4716-e57f-4101-a8d6-03af5da9dc50.disk0_meta" successfully removed
   node3#
 
 At this point cluster verify shouldn't complain anymore::
 
-  node1# gnt-cluster verify
+  $ gnt-cluster verify
   Mon Oct 26 18:37:51 2009 * Verifying global settings
   Mon Oct 26 18:37:51 2009 * Gathering data (3 nodes)
   Mon Oct 26 18:37:53 2009 * Verifying node status
@@ -838,7 +841,7 @@ At this point cluster verify shouldn't complain anymore::
   Mon Oct 26 18:37:53 2009 * Verifying N+1 Memory redundancy
   Mon Oct 26 18:37:53 2009 * Other Notes
   Mon Oct 26 18:37:53 2009 * Hooks Results
-  node1#
+  $
 
 N+1 errors
 ++++++++++
@@ -855,18 +858,19 @@ increase the memory of the current instances to 4G, and add three new
 instances, two on node2:node3 with 8GB of RAM and one on node1:node2,
 with 12GB of RAM (numbers chosen so that we run out of memory)::
 
-  node1# gnt-instance modify -B memory=4G instance1
+  $ gnt-instance modify -B memory=%4G% %instance1%
   Modified instance instance1
-   - be/memory -> 4096
+   - be/maxmem -> 4096
+   - be/minmem -> 4096
   Please don't forget that these parameters take effect only at the next start of the instance.
-  node1# gnt-instance modify …
+  $ gnt-instance modify …
 
-  node1# gnt-instance add -t drbd -n node2:node3 -s 512m -B memory=8G -o debootstrap instance5
+  $ gnt-instance add -t drbd -n %node2%:%node3% -s %512m% -B memory=%8G% -o %debootstrap% %instance5%
   …
-  node1# gnt-instance add -t drbd -n node2:node3 -s 512m -B memory=8G -o debootstrap instance6
+  $ gnt-instance add -t drbd -n %node2%:%node3% -s %512m% -B memory=%8G% -o %debootstrap% %instance6%
   …
-  node1# gnt-instance add -t drbd -n node1:node2 -s 512m -B memory=8G -o debootstrap instance7
-  node1# gnt-instance reboot --all
+  $ gnt-instance add -t drbd -n %node1%:%node2% -s %512m% -B memory=%8G% -o %debootstrap% %instance7%
+  $ gnt-instance reboot --all
   The reboot will operate on 7 instances.
   Do you want to continue?
   Affected instances:
@@ -877,7 +881,7 @@ with 12GB of RAM (numbers chosen so that we run out of memory)::
     instance5
     instance6
     instance7
-  y/[n]/?: y
+  y/[n]/?: %y%
   Submitted jobs 677, 678, 679, 680, 681, 682, 683
   Waiting for job 677 for instance1...
   Waiting for job 678 for instance2...
@@ -886,17 +890,17 @@ with 12GB of RAM (numbers chosen so that we run out of memory)::
   Waiting for job 681 for instance5...
   Waiting for job 682 for instance6...
   Waiting for job 683 for instance7...
-  node1#
+  $
 
-We rebooted instances for the memory changes to have effect. Now the
+We rebooted the instances for the memory changes to have effect. Now the
 cluster looks like::
 
-  node1# gnt-node list
+  $ gnt-node list
   Node  DTotal DFree MTotal MNode MFree Pinst Sinst
   node1   1.3T  1.3T  32.0G  1.0G  6.5G     4     1
   node2   1.3T  1.3T  32.0G  1.0G 10.5G     3     4
   node3   1.3T  1.3T  32.0G  1.0G 30.5G     0     2
-  node1# gnt-cluster verify
+  $ gnt-cluster verify
   Mon Oct 26 18:59:36 2009 * Verifying global settings
   Mon Oct 26 18:59:36 2009 * Gathering data (3 nodes)
   Mon Oct 26 18:59:37 2009 * Verifying node status
@@ -907,7 +911,7 @@ cluster looks like::
   Mon Oct 26 18:59:37 2009   - ERROR: node node2: not enough memory to accommodate instance failovers should node node1 fail
   Mon Oct 26 18:59:37 2009 * Other Notes
   Mon Oct 26 18:59:37 2009 * Hooks Results
-  node1#
+  $
 
 The cluster verify error above shows that if node1 fails, node2 will not
 have enough memory to failover all primary instances on node1 to it. To
@@ -915,8 +919,15 @@ solve this, you have a number of options:
 
 - try to manually move instances around (but this can become complicated
   for any non-trivial cluster)
-- try to reduce memory of some instances to accommodate the available
-  node memory
+- try to reduce the minimum memory of some instances on the source node
+  of the N+1 failure (in the example above ``node1``): this will allow
+  it to start and be failed over/migrated with less than its maximum
+  memory
+- try to reduce the runtime/maximum memory of some instances on the
+  destination node of the N+1 failure (in the example above ``node2``)
+  to create additional available node memory (check the :doc:`admin`
+  guide for what Ganeti will and won't automatically do in regards to
+  instance runtime memory modification)
 - 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
@@ -928,7 +939,7 @@ In case a node has problems with the network (usually the secondary
 network, as problems with the primary network will render the node
 unusable for ganeti commands), it will show up in cluster verify as::
 
-  node1# gnt-cluster verify
+  $ gnt-cluster verify
   Mon Oct 26 19:07:19 2009 * Verifying global settings
   Mon Oct 26 19:07:19 2009 * Gathering data (3 nodes)
   Mon Oct 26 19:07:23 2009 * Verifying node status
@@ -943,7 +954,7 @@ unusable for ganeti commands), it will show up in cluster verify as::
   Mon Oct 26 19:07:23 2009 * Verifying N+1 Memory redundancy
   Mon Oct 26 19:07:23 2009 * Other Notes
   Mon Oct 26 19:07:23 2009 * Hooks Results
-  node1#
+  $
 
 This shows that both node1 and node2 have problems contacting node3 over
 the secondary network, and node3 has problems contacting them. From this
@@ -966,12 +977,12 @@ It is always safe to run this command as long as the instance has good
 data on its primary node (i.e. not showing as degraded). If so, you can
 simply run::
 
-  node1# gnt-instance migrate --cleanup instance1
+  $ gnt-instance migrate --cleanup %instance1%
   Instance instance1 will be recovered from a failed migration. Note
   that the migration procedure (including cleanup) is **experimental**
   in this version. This might impact the instance if anything goes
   wrong. Continue?
-  y/[n]/?: y
+  y/[n]/?: %y%
   Mon Oct 26 19:13:49 2009 Migrating instance instance1
   Mon Oct 26 19:13:49 2009 * checking where the instance actually runs (if this hangs, the hypervisor might be in a bad state)
   Mon Oct 26 19:13:49 2009 * instance confirmed to be running on its primary node (node2)
@@ -981,7 +992,7 @@ simply run::
   Mon Oct 26 19:13:50 2009 * changing disks into single-master mode
   Mon Oct 26 19:13:50 2009 * wait until resync is done
   Mon Oct 26 19:13:51 2009 * done
-  node1#
+  $
 
 In use disks at instance shutdown
 +++++++++++++++++++++++++++++++++
@@ -989,7 +1000,7 @@ In use disks at instance shutdown
 If you see something like the following when trying to shutdown or
 deactivate disks for an instance::
 
-  node1# gnt-instance shutdown instance1
+  $ gnt-instance shutdown %instance1%
   Mon Oct 26 19:16:23 2009  - WARNING: Could not shutdown block device disk/0 on node node2: drbd0: can't shutdown drbd device: /dev/drbd0: State change failed: (-12) Device is held open by someone\n
 
 It most likely means something is holding open the underlying DRBD
@@ -1009,20 +1020,20 @@ and pay attention to the hypervisor being used:
 
 For Xen, check if it's not using the disks itself::
 
-  node1# xenstore-ls /local/domain/0/backend/vbd|grep -e "domain =" -e physical-device
+  $ xenstore-ls /local/domain/%0%/backend/vbd|grep -e "domain =" -e physical-device
   domain = "instance2"
   physical-device = "93:0"
   domain = "instance3"
   physical-device = "93:1"
   domain = "instance4"
   physical-device = "93:2"
-  node1#
+  $
 
 You can see in the above output that the node exports three disks, to
 three instances. The ``physical-device`` key is in major:minor format in
-hexadecimal, and 0x93 represents DRBD's major number. Thus we can see
-from the above that instance2 has /dev/drbd0, instance3 /dev/drbd1, and
-instance4 /dev/drbd2.
+hexadecimal, and ``0x93`` represents DRBD's major number. Thus we can
+see from the above that instance2 has /dev/drbd0, instance3 /dev/drbd1,
+and instance4 /dev/drbd2.
 
 LUXI version mismatch
 +++++++++++++++++++++
@@ -1031,7 +1042,7 @@ LUXI is the protocol used for communication between clients and the
 master daemon. Starting in Ganeti 2.3, the peers exchange their version
 in each message. When they don't match, an error is raised::
 
-  $ gnt-node modify -O yes node3
+  $ gnt-node modify -O yes %node3%
   Unhandled Ganeti error: LUXI version mismatch, server 2020000, request 2030000
 
 Usually this means that server and client are from different Ganeti
diff --git a/htools/Ganeti/BasicTypes.hs b/htools/Ganeti/BasicTypes.hs
new file mode 100644 (file)
index 0000000..55bab28
--- /dev/null
@@ -0,0 +1,172 @@
+{-
+
+Copyright (C) 2009, 2010, 2011, 2012 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.BasicTypes
+  ( Result(..)
+  , isOk
+  , isBad
+  , eitherToResult
+  , annotateResult
+  , annotateIOError
+  , select
+  , LookupResult(..)
+  , MatchPriority(..)
+  , lookupName
+  , goodLookupResult
+  , goodMatchPriority
+  , prefixMatch
+  , compareNameComponent
+  ) where
+
+import Control.Monad
+import Data.Function
+import Data.List
+
+-- | This is similar to the JSON library Result type - /very/ similar,
+-- but we want to use it in multiple places, so we abstract it into a
+-- mini-library here.
+--
+-- The failure value for this monad is simply a string.
+data Result a
+    = Bad String
+    | Ok a
+    deriving (Show, Read, Eq)
+
+instance Monad Result where
+  (>>=) (Bad x) _ = Bad x
+  (>>=) (Ok x) fn = fn x
+  return = Ok
+  fail = Bad
+
+instance MonadPlus Result where
+  mzero = Bad "zero Result when used as MonadPlus"
+  -- for mplus, when we 'add' two Bad values, we concatenate their
+  -- error descriptions
+  (Bad x) `mplus` (Bad y) = Bad (x ++ "; " ++ y)
+  (Bad _) `mplus` x = x
+  x@(Ok _) `mplus` _ = x
+
+-- | Simple checker for whether a 'Result' is OK.
+isOk :: Result a -> Bool
+isOk (Ok _) = True
+isOk _ = False
+
+-- | Simple checker for whether a 'Result' is a failure.
+isBad :: Result a  -> Bool
+isBad = not . isOk
+
+-- | Converter from Either String to 'Result'.
+eitherToResult :: Either String a -> Result a
+eitherToResult (Left s) = Bad s
+eitherToResult (Right v) = Ok v
+
+-- | Annotate a Result with an ownership information.
+annotateResult :: String -> Result a -> Result a
+annotateResult owner (Bad s) = Bad $ owner ++ ": " ++ s
+annotateResult _ v = v
+
+-- | Annotates and transforms IOErrors into a Result type. This can be
+-- used in the error handler argument to 'catch', for example.
+annotateIOError :: String -> IOError -> IO (Result a)
+annotateIOError description exc =
+  return . Bad $ description ++ ": " ++ show exc
+
+-- * Misc functionality
+
+-- | Return the first result with a True condition, or the default otherwise.
+select :: a            -- ^ default result
+       -> [(Bool, a)]  -- ^ list of \"condition, result\"
+       -> a            -- ^ first result which has a True condition, or default
+select def = maybe def snd . find fst
+
+-- * Lookup of partial names functionality
+
+-- | The priority of a match in a lookup result.
+data MatchPriority = ExactMatch
+                   | MultipleMatch
+                   | PartialMatch
+                   | FailMatch
+                   deriving (Show, Read, Enum, Eq, Ord)
+
+-- | The result of a name lookup in a list.
+data LookupResult = LookupResult
+  { lrMatchPriority :: MatchPriority -- ^ The result type
+  -- | Matching value (for ExactMatch, PartialMatch), Lookup string otherwise
+  , lrContent :: String
+  } deriving (Show, Read)
+
+-- | Lookup results have an absolute preference ordering.
+instance Eq LookupResult where
+  (==) = (==) `on` lrMatchPriority
+
+instance Ord LookupResult where
+  compare = compare `on` lrMatchPriority
+
+-- | Check for prefix matches in names.
+-- Implemented in Ganeti core utils.text.MatchNameComponent
+-- as the regexp r"^%s(\..*)?$" % re.escape(key)
+prefixMatch :: String  -- ^ Lookup
+            -> String  -- ^ Full name
+            -> Bool    -- ^ Whether there is a prefix match
+prefixMatch = isPrefixOf . (++ ".")
+
+-- | Is the lookup priority a "good" one?
+goodMatchPriority :: MatchPriority -> Bool
+goodMatchPriority ExactMatch = True
+goodMatchPriority PartialMatch = True
+goodMatchPriority _ = False
+
+-- | Is the lookup result an actual match?
+goodLookupResult :: LookupResult -> Bool
+goodLookupResult = goodMatchPriority . lrMatchPriority
+
+-- | Compares a canonical name and a lookup string.
+compareNameComponent :: String        -- ^ Canonical (target) name
+                     -> String        -- ^ Partial (lookup) name
+                     -> LookupResult  -- ^ Result of the lookup
+compareNameComponent cnl lkp =
+  select (LookupResult FailMatch lkp)
+  [ (cnl == lkp          , LookupResult ExactMatch cnl)
+  , (prefixMatch lkp cnl , LookupResult PartialMatch cnl)
+  ]
+
+-- | Lookup a string and choose the best result.
+chooseLookupResult :: String       -- ^ Lookup key
+                   -> String       -- ^ String to compare to the lookup key
+                   -> LookupResult -- ^ Previous result
+                   -> LookupResult -- ^ New result
+chooseLookupResult lkp cstr old =
+  -- default: use class order to pick the minimum result
+  select (min new old)
+  -- special cases:
+  -- short circuit if the new result is an exact match
+  [ (lrMatchPriority new == ExactMatch, new)
+  -- if both are partial matches generate a multiple match
+  , (partial2, LookupResult MultipleMatch lkp)
+  ] where new = compareNameComponent cstr lkp
+          partial2 = all ((PartialMatch==) . lrMatchPriority) [old, new]
+
+-- | Find the canonical name for a lookup string in a list of names.
+lookupName :: [String]      -- ^ List of keys
+           -> String        -- ^ Lookup string
+           -> LookupResult  -- ^ Result of the lookup
+lookupName l s = foldr (chooseLookupResult s)
+                       (LookupResult FailMatch s) l
diff --git a/htools/Ganeti/Confd.hs b/htools/Ganeti/Confd.hs
new file mode 100644 (file)
index 0000000..8fdf12d
--- /dev/null
@@ -0,0 +1,167 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+{-| Implementation of the Ganeti confd types.
+
+-}
+
+{-
+
+Copyright (C) 2011, 2012 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.Confd
+  ( C.confdProtocolVersion
+  , C.confdMaxClockSkew
+  , C.confdConfigReloadTimeout
+  , C.confdConfigReloadRatelimit
+  , C.confdMagicFourcc
+  , C.confdDefaultReqCoverage
+  , C.confdClientExpireTimeout
+  , C.maxUdpDataSize
+  , ConfdRequestType(..)
+  , ConfdReqQ(..)
+  , ConfdReqField(..)
+  , ConfdReplyStatus(..)
+  , ConfdNodeRole(..)
+  , ConfdErrorType(..)
+  , ConfdRequest(..)
+  , ConfdReply(..)
+  , ConfdQuery(..)
+  , SignedMessage(..)
+  ) where
+
+import Text.JSON
+
+import qualified Ganeti.Constants as C
+import Ganeti.THH
+import Ganeti.HTools.JSON
+
+{-
+   Note that we re-export as is from Constants the following simple items:
+   - confdProtocolVersion
+   - confdMaxClockSkew
+   - confdConfigReloadTimeout
+   - confdConfigReloadRatelimit
+   - confdMagicFourcc
+   - confdDefaultReqCoverage
+   - confdClientExpireTimeout
+   - maxUdpDataSize
+
+-}
+
+$(declareIADT "ConfdRequestType"
+  [ ("ReqPing",             'C.confdReqPing )
+  , ("ReqNodeRoleByName",   'C.confdReqNodeRoleByname )
+  , ("ReqNodePipList",      'C.confdReqNodePipList )
+  , ("ReqNodePipByInstPip", 'C.confdReqNodePipByInstanceIp )
+  , ("ReqClusterMaster",    'C.confdReqClusterMaster )
+  , ("ReqMcPipList",        'C.confdReqMcPipList )
+  , ("ReqInstIpsList",      'C.confdReqInstancesIpsList )
+  , ("ReqNodeDrbd",         'C.confdReqNodeDrbd )
+  ])
+$(makeJSONInstance ''ConfdRequestType)
+
+$(declareSADT "ConfdReqField"
+  [ ("ReqFieldName",     'C.confdReqfieldName )
+  , ("ReqFieldIp",       'C.confdReqfieldIp )
+  , ("ReqFieldMNodePip", 'C.confdReqfieldMnodePip )
+  ])
+$(makeJSONInstance ''ConfdReqField)
+
+-- Confd request query fields. These are used to narrow down queries.
+-- These must be strings rather than integers, because json-encoding
+-- converts them to strings anyway, as they're used as dict-keys.
+
+$(buildObject "ConfdReqQ" "confdReqQ"
+  [ renameField "Ip" $
+                optionalField $ simpleField C.confdReqqIp [t| String   |]
+  , renameField "IpList" $
+                defaultField [| [] |] $
+                simpleField C.confdReqqIplist [t| [String] |]
+  , renameField "Link" $ optionalField $
+                simpleField C.confdReqqLink [t| String   |]
+  , renameField "Fields" $ defaultField [| [] |] $
+                simpleField C.confdReqqFields [t| [ConfdReqField] |]
+  ])
+
+-- | Confd query type. This is complex enough that we can't
+-- automatically derive it via THH.
+data ConfdQuery = EmptyQuery
+                | PlainQuery String
+                | DictQuery  ConfdReqQ
+                  deriving (Show, Read, Eq)
+
+instance JSON ConfdQuery where
+  readJSON o = case o of
+                 JSNull     -> return EmptyQuery
+                 JSString s -> return . PlainQuery . fromJSString $ s
+                 JSObject _ -> fmap DictQuery (readJSON o::Result ConfdReqQ)
+                 _ -> fail $ "Cannot deserialise into ConfdQuery\
+                             \ the value '" ++ show o ++ "'"
+  showJSON cq = case cq of
+                  EmptyQuery -> JSNull
+                  PlainQuery s -> showJSON s
+                  DictQuery drq -> showJSON drq
+
+$(declareIADT "ConfdReplyStatus"
+  [ ( "ReplyStatusOk",      'C.confdReplStatusOk )
+  , ( "ReplyStatusError",   'C.confdReplStatusError )
+  , ( "ReplyStatusNotImpl", 'C.confdReplStatusNotimplemented )
+  ])
+$(makeJSONInstance ''ConfdReplyStatus)
+
+$(declareIADT "ConfdNodeRole"
+  [ ( "NodeRoleMaster",    'C.confdNodeRoleMaster )
+  , ( "NodeRoleCandidate", 'C.confdNodeRoleCandidate )
+  , ( "NodeRoleOffline",   'C.confdNodeRoleOffline )
+  , ( "NodeRoleDrained",   'C.confdNodeRoleDrained )
+  , ( "NodeRoleRegular",   'C.confdNodeRoleRegular )
+  ])
+$(makeJSONInstance ''ConfdNodeRole)
+
+
+-- Note that the next item is not a frozenset in Python, but we make
+-- it a separate type for safety
+
+$(declareIADT "ConfdErrorType"
+  [ ( "ConfdErrorUnknownEntry", 'C.confdErrorUnknownEntry )
+  , ( "ConfdErrorInternal",     'C.confdErrorInternal )
+  , ( "ConfdErrorArgument",     'C.confdErrorArgument )
+  ])
+$(makeJSONInstance ''ConfdErrorType)
+
+$(buildObject "ConfdRequest" "confdRq" $
+  [ simpleField "protocol" [t| Int |]
+  , simpleField "type"     [t| ConfdRequestType |]
+  , defaultField [| EmptyQuery |] $ simpleField "query" [t| ConfdQuery |]
+  , simpleField "rsalt"    [t| String |]
+  ])
+
+$(buildObject "ConfdReply" "confdReply"
+  [ simpleField "protocol" [t| Int              |]
+  , simpleField "status"   [t| ConfdReplyStatus |]
+  , simpleField "answer"   [t| JSValue          |]
+  , simpleField "serial"   [t| Int              |]
+  ])
+
+$(buildObject "SignedMessage" "signedMsg"
+  [ simpleField "hmac" [t| String |]
+  , simpleField "msg"  [t| String |]
+  , simpleField "salt" [t| String |]
+  ])
diff --git a/htools/Ganeti/Confd/Server.hs b/htools/Ganeti/Confd/Server.hs
new file mode 100644 (file)
index 0000000..f0ef0f2
--- /dev/null
@@ -0,0 +1,536 @@
+{-# LANGUAGE BangPatterns #-}
+
+{-| Implementation of the Ganeti confd server functionality.
+
+-}
+
+{-
+
+Copyright (C) 2011, 2012 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.Confd.Server
+  ( main
+  ) where
+
+import Control.Concurrent
+import Control.Exception
+import Control.Monad (forever)
+import qualified Data.ByteString as B
+import Data.IORef
+import Data.List
+import qualified Data.Map as M
+import qualified Network.Socket as S
+import Prelude hiding (catch)
+import System.Posix.Files
+import System.Posix.Types
+import System.Time
+import qualified Text.JSON as J
+import System.INotify
+
+import Ganeti.Daemon
+import Ganeti.HTools.JSON
+import Ganeti.HTools.Types
+import Ganeti.HTools.Utils
+import Ganeti.Objects
+import Ganeti.Confd
+import Ganeti.Config
+import Ganeti.Hash
+import Ganeti.Logging
+import qualified Ganeti.Constants as C
+
+-- * Types and constants definitions
+
+-- | What we store as configuration.
+type CRef = IORef (Result (ConfigData, LinkIpMap))
+
+-- | File stat identifier.
+type FStat = (EpochTime, FileID, FileOffset)
+
+-- | Null 'FStat' value.
+nullFStat :: FStat
+nullFStat = (-1, -1, -1)
+
+-- | A small type alias for readability.
+type StatusAnswer = (ConfdReplyStatus, J.JSValue)
+
+-- | Reload model data type.
+data ReloadModel = ReloadNotify      -- ^ We are using notifications
+                 | ReloadPoll Int    -- ^ We are using polling
+                   deriving (Eq, Show)
+
+-- | Server state data type.
+data ServerState = ServerState
+  { reloadModel  :: ReloadModel
+  , reloadTime   :: Integer
+  , reloadFStat  :: FStat
+  }
+
+-- | Maximum no-reload poll rounds before reverting to inotify.
+maxIdlePollRounds :: Int
+maxIdlePollRounds = 2
+
+-- | Reload timeout in microseconds.
+configReloadTimeout :: Int
+configReloadTimeout = C.confdConfigReloadTimeout * 1000000
+
+-- | Ratelimit timeout in microseconds.
+configReloadRatelimit :: Int
+configReloadRatelimit = C.confdConfigReloadRatelimit * 1000000
+
+-- | Initial poll round.
+initialPoll :: ReloadModel
+initialPoll = ReloadPoll 0
+
+-- | Initial server state.
+initialState :: ServerState
+initialState = ServerState initialPoll 0 nullFStat
+
+-- | Reload status data type.
+data ConfigReload = ConfigToDate    -- ^ No need to reload
+                  | ConfigReloaded  -- ^ Configuration reloaded
+                  | ConfigIOError   -- ^ Error during configuration reload
+
+-- | Unknown entry standard response.
+queryUnknownEntry :: StatusAnswer
+queryUnknownEntry = (ReplyStatusError, J.showJSON ConfdErrorUnknownEntry)
+
+{- not used yet
+-- | Internal error standard response.
+queryInternalError :: StatusAnswer
+queryInternalError = (ReplyStatusError, J.showJSON ConfdErrorInternal)
+-}
+
+-- | Argument error standard response.
+queryArgumentError :: StatusAnswer
+queryArgumentError = (ReplyStatusError, J.showJSON ConfdErrorArgument)
+
+-- | Returns the current time.
+getCurrentTime :: IO Integer
+getCurrentTime = do
+  TOD ctime _ <- getClockTime
+  return ctime
+
+-- * Confd base functionality
+
+-- | Returns the HMAC key.
+getClusterHmac :: IO HashKey
+getClusterHmac = fmap B.unpack $ B.readFile C.confdHmacKey
+
+-- | Computes the node role.
+nodeRole :: ConfigData -> String -> Result ConfdNodeRole
+nodeRole cfg name =
+  let cmaster = clusterMasterNode . configCluster $ cfg
+      mnode = M.lookup name . configNodes $ cfg
+  in case mnode of
+       Nothing -> Bad "Node not found"
+       Just node | cmaster == name -> Ok NodeRoleMaster
+                 | nodeDrained node -> Ok NodeRoleDrained
+                 | nodeOffline node -> Ok NodeRoleOffline
+                 | nodeMasterCandidate node -> Ok NodeRoleCandidate
+       _ -> Ok NodeRoleRegular
+
+-- | Does an instance ip -> instance -> primary node -> primary ip
+-- transformation.
+getNodePipByInstanceIp :: ConfigData
+                       -> LinkIpMap
+                       -> String
+                       -> String
+                       -> StatusAnswer
+getNodePipByInstanceIp cfg linkipmap link instip =
+  case M.lookup instip (M.findWithDefault M.empty link linkipmap) of
+    Nothing -> queryUnknownEntry
+    Just instname ->
+      case getInstPrimaryNode cfg instname of
+        Bad _ -> queryUnknownEntry -- either instance or node not found
+        Ok node -> (ReplyStatusOk, J.showJSON (nodePrimaryIp node))
+
+-- | Builds the response to a given query.
+buildResponse :: (ConfigData, LinkIpMap) -> ConfdRequest -> Result StatusAnswer
+buildResponse (cfg, _) (ConfdRequest { confdRqType = ReqPing }) =
+  return (ReplyStatusOk, J.showJSON (configVersion cfg))
+
+buildResponse cdata req@(ConfdRequest { confdRqType = ReqClusterMaster }) =
+  case confdRqQuery req of
+    EmptyQuery -> return (ReplyStatusOk, J.showJSON master_name)
+    PlainQuery _ -> return queryArgumentError
+    DictQuery reqq -> do
+      mnode <- getNode cfg master_name
+      let fvals =map (\field -> case field of
+                                  ReqFieldName -> master_name
+                                  ReqFieldIp -> clusterMasterIp cluster
+                                  ReqFieldMNodePip -> nodePrimaryIp mnode
+                     ) (confdReqQFields reqq)
+      return (ReplyStatusOk, J.showJSON fvals)
+    where master_name = clusterMasterNode cluster
+          cluster = configCluster cfg
+          cfg = fst cdata
+
+buildResponse cdata req@(ConfdRequest { confdRqType = ReqNodeRoleByName }) = do
+  node_name <- case confdRqQuery req of
+                 PlainQuery str -> return str
+                 _ -> fail $ "Invalid query type " ++ show (confdRqQuery req)
+  role <- nodeRole (fst cdata) node_name
+  return (ReplyStatusOk, J.showJSON role)
+
+buildResponse cdata (ConfdRequest { confdRqType = ReqNodePipList }) =
+  -- note: we use foldlWithKey because that's present accross more
+  -- versions of the library
+  return (ReplyStatusOk, J.showJSON $
+          M.foldlWithKey (\accu _ n -> nodePrimaryIp n:accu) []
+          (configNodes (fst cdata)))
+
+buildResponse cdata (ConfdRequest { confdRqType = ReqMcPipList }) =
+  -- note: we use foldlWithKey because that's present accross more
+  -- versions of the library
+  return (ReplyStatusOk, J.showJSON $
+          M.foldlWithKey (\accu _ n -> if nodeMasterCandidate n
+                                         then nodePrimaryIp n:accu
+                                         else accu) []
+          (configNodes (fst cdata)))
+
+buildResponse (cfg, linkipmap)
+              req@(ConfdRequest { confdRqType = ReqInstIpsList }) = do
+  link <- case confdRqQuery req of
+            PlainQuery str -> return str
+            EmptyQuery -> return (getDefaultNicLink cfg)
+            _ -> fail "Invalid query type"
+  return (ReplyStatusOk, J.showJSON $ getInstancesIpByLink linkipmap link)
+
+buildResponse cdata (ConfdRequest { confdRqType = ReqNodePipByInstPip
+                                  , confdRqQuery = DictQuery query}) =
+  let (cfg, linkipmap) = cdata
+      link = maybe (getDefaultNicLink cfg) id (confdReqQLink query)
+  in case confdReqQIp query of
+       Just ip -> return $ getNodePipByInstanceIp cfg linkipmap link ip
+       Nothing -> return (ReplyStatusOk,
+                          J.showJSON $
+                           map (getNodePipByInstanceIp cfg linkipmap link)
+                           (confdReqQIpList query))
+
+buildResponse _ (ConfdRequest { confdRqType = ReqNodePipByInstPip }) =
+  return queryArgumentError
+
+buildResponse cdata req@(ConfdRequest { confdRqType = ReqNodeDrbd }) = do
+  let cfg = fst cdata
+  node_name <- case confdRqQuery req of
+                 PlainQuery str -> return str
+                 _ -> fail $ "Invalid query type " ++ show (confdRqQuery req)
+  node <- getNode cfg node_name
+  let minors = concatMap (getInstMinorsForNode (nodeName node)) .
+               M.elems . configInstances $ cfg
+      encoded = [J.JSArray [J.showJSON a, J.showJSON b, J.showJSON c,
+                             J.showJSON d, J.showJSON e, J.showJSON f] |
+                 (a, b, c, d, e, f) <- minors]
+  return (ReplyStatusOk, J.showJSON encoded)
+
+-- | Parses a signed request.
+parseRequest :: HashKey -> String -> Result (String, String, ConfdRequest)
+parseRequest key str = do
+  (SignedMessage hmac msg salt) <- fromJResult "parsing request" $ J.decode str
+  req <- if verifyMac key (Just salt) msg hmac
+           then fromJResult "parsing message" $ J.decode msg
+           else Bad "HMAC verification failed"
+  return (salt, msg, req)
+
+-- | Creates a ConfdReply from a given answer.
+serializeResponse :: Result StatusAnswer -> ConfdReply
+serializeResponse r =
+    let (status, result) = case r of
+                    Bad err -> (ReplyStatusError, J.showJSON err)
+                    Ok (code, val) -> (code, val)
+    in ConfdReply { confdReplyProtocol = 1
+                  , confdReplyStatus   = status
+                  , confdReplyAnswer   = result
+                  , confdReplySerial   = 0 }
+
+-- | Signs a message with a given key and salt.
+signMessage :: HashKey -> String -> String -> SignedMessage
+signMessage key salt msg =
+  SignedMessage { signedMsgMsg  = msg
+                , signedMsgSalt = salt
+                , signedMsgHmac = hmac
+                }
+    where hmac = computeMac key (Just salt) msg
+
+-- * Configuration handling
+
+-- ** Helper functions
+
+-- | Helper function for logging transition into polling mode.
+moveToPolling :: String -> INotify -> FilePath -> CRef -> MVar ServerState
+              -> IO ReloadModel
+moveToPolling msg inotify path cref mstate = do
+  logInfo $ "Moving to polling mode: " ++ msg
+  let inotiaction = addNotifier inotify path cref mstate
+  _ <- forkIO $ onReloadTimer inotiaction path cref mstate
+  return initialPoll
+
+-- | Helper function for logging transition into inotify mode.
+moveToNotify :: IO ReloadModel
+moveToNotify = do
+  logInfo "Moving to inotify mode"
+  return ReloadNotify
+
+-- ** Configuration loading
+
+-- | (Re)loads the configuration.
+updateConfig :: FilePath -> CRef -> IO ()
+updateConfig path r = do
+  newcfg <- loadConfig path
+  let !newdata = case newcfg of
+                   Ok !cfg -> Ok (cfg, buildLinkIpInstnameMap cfg)
+                   Bad _ -> Bad "Cannot load configuration"
+  writeIORef r newdata
+  case newcfg of
+    Ok cfg -> logInfo ("Loaded new config, serial " ++
+                       show (configSerial cfg))
+    Bad msg -> logError $ "Failed to load config: " ++ msg
+  return ()
+
+-- | Wrapper over 'updateConfig' that handles IO errors.
+safeUpdateConfig :: FilePath -> FStat -> CRef -> IO (FStat, ConfigReload)
+safeUpdateConfig path oldfstat cref = do
+  catch (do
+          nt <- needsReload oldfstat path
+          case nt of
+            Nothing -> return (oldfstat, ConfigToDate)
+            Just nt' -> do
+                    updateConfig path cref
+                    return (nt', ConfigReloaded)
+        ) (\e -> do
+             let msg = "Failure during configuration update: " ++
+                       show (e::IOError)
+             writeIORef cref (Bad msg)
+             return (nullFStat, ConfigIOError)
+          )
+
+-- | Computes the file cache data from a FileStatus structure.
+buildFileStatus :: FileStatus -> FStat
+buildFileStatus ofs =
+    let modt = modificationTime ofs
+        inum = fileID ofs
+        fsize = fileSize ofs
+    in (modt, inum, fsize)
+
+-- | Wrapper over 'buildFileStatus'. This reads the data from the
+-- filesystem and then builds our cache structure.
+getFStat :: FilePath -> IO FStat
+getFStat p = getFileStatus p >>= (return . buildFileStatus)
+
+-- | Check if the file needs reloading
+needsReload :: FStat -> FilePath -> IO (Maybe FStat)
+needsReload oldstat path = do
+  newstat <- getFStat path
+  return $ if newstat /= oldstat
+             then Just newstat
+             else Nothing
+
+-- ** Watcher threads
+
+-- $watcher
+-- We have three threads/functions that can mutate the server state:
+--
+-- 1. the long-interval watcher ('onTimeoutTimer')
+--
+-- 2. the polling watcher ('onReloadTimer')
+--
+-- 3. the inotify event handler ('onInotify')
+--
+-- All of these will mutate the server state under 'modifyMVar' or
+-- 'modifyMVar_', so that server transitions are more or less
+-- atomic. The inotify handler remains active during polling mode, but
+-- checks for polling mode and doesn't do anything in this case (this
+-- check is needed even if we would unregister the event handler due
+-- to how events are serialised).
+
+-- | Long-interval reload watcher.
+--
+-- This is on top of the inotify-based triggered reload.
+onTimeoutTimer :: IO Bool -> FilePath -> CRef -> MVar ServerState -> IO ()
+onTimeoutTimer inotiaction path cref state = do
+  threadDelay configReloadTimeout
+  modifyMVar_ state (onTimeoutInner path cref)
+  _ <- inotiaction
+  onTimeoutTimer inotiaction path cref state
+
+-- | Inner onTimeout handler.
+--
+-- This mutates the server state under a modifyMVar_ call. It never
+-- changes the reload model, just does a safety reload and tried to
+-- re-establish the inotify watcher.
+onTimeoutInner :: FilePath -> CRef -> ServerState -> IO ServerState
+onTimeoutInner path cref state  = do
+  (newfstat, _) <- safeUpdateConfig path (reloadFStat state) cref
+  return state { reloadFStat = newfstat }
+
+-- | Short-interval (polling) reload watcher.
+--
+-- This is only active when we're in polling mode; it will
+-- automatically exit when it detects that the state has changed to
+-- notification.
+onReloadTimer :: IO Bool -> FilePath -> CRef -> MVar ServerState -> IO ()
+onReloadTimer inotiaction path cref state = do
+  continue <- modifyMVar state (onReloadInner inotiaction path cref)
+  if continue
+    then do
+      threadDelay configReloadRatelimit
+      onReloadTimer inotiaction path cref state
+    else -- the inotify watch has been re-established, we can exit
+      return ()
+
+-- | Inner onReload handler.
+--
+-- This again mutates the state under a modifyMVar call, and also
+-- returns whether the thread should continue or not.
+onReloadInner :: IO Bool -> FilePath -> CRef -> ServerState
+              -> IO (ServerState, Bool)
+onReloadInner _ _ _ state@(ServerState { reloadModel = ReloadNotify } ) =
+  return (state, False)
+onReloadInner inotiaction path cref
+              state@(ServerState { reloadModel = ReloadPoll pround } ) = do
+  (newfstat, reload) <- safeUpdateConfig path (reloadFStat state) cref
+  let state' = state { reloadFStat = newfstat }
+  -- compute new poll model based on reload data; however, failure to
+  -- re-establish the inotifier means we stay on polling
+  newmode <- case reload of
+               ConfigToDate ->
+                 if pround >= maxIdlePollRounds
+                   then do -- try to switch to notify
+                     result <- inotiaction
+                     if result
+                       then moveToNotify
+                       else return initialPoll
+                   else return (ReloadPoll (pround + 1))
+               _ -> return initialPoll
+  let continue = case newmode of
+                   ReloadNotify -> False
+                   _            -> True
+  return (state' { reloadModel = newmode }, continue)
+
+-- | Setup inotify watcher.
+--
+-- This tries to setup the watch descriptor; in case of any IO errors,
+-- it will return False.
+addNotifier :: INotify -> FilePath -> CRef -> MVar ServerState -> IO Bool
+addNotifier inotify path cref mstate = do
+  catch (addWatch inotify [CloseWrite] path
+                    (onInotify inotify path cref mstate) >> return True)
+        (\e -> const (return False) (e::IOError))
+
+-- | Inotify event handler.
+onInotify :: INotify -> String -> CRef -> MVar ServerState -> Event -> IO ()
+onInotify inotify path cref mstate Ignored = do
+  logInfo "File lost, trying to re-establish notifier"
+  modifyMVar_ mstate $ \state -> do
+    result <- addNotifier inotify path cref mstate
+    (newfstat, _) <- safeUpdateConfig path (reloadFStat state) cref
+    let state' = state { reloadFStat = newfstat }
+    if result
+      then return state' -- keep notify
+      else do
+        mode <- moveToPolling "cannot re-establish inotify watch" inotify
+                  path cref mstate
+        return state' { reloadModel = mode }
+
+onInotify inotify path cref mstate _ = do
+  modifyMVar_ mstate $ \state ->
+    if (reloadModel state == ReloadNotify)
+       then do
+         ctime <- getCurrentTime
+         (newfstat, _) <- safeUpdateConfig path (reloadFStat state) cref
+         let state' = state { reloadFStat = newfstat, reloadTime = ctime }
+         if abs (reloadTime state - ctime) <
+            fromIntegral C.confdConfigReloadRatelimit
+           then do
+             mode <- moveToPolling "too many reloads" inotify path cref mstate
+             return state' { reloadModel = mode }
+           else return state'
+      else return state
+
+-- ** Client input/output handlers
+
+-- | Main loop for a given client.
+responder :: CRef -> S.Socket -> HashKey -> String -> S.SockAddr -> IO ()
+responder cfgref socket hmac msg peer = do
+  ctime <- getCurrentTime
+  case parseMessage hmac msg ctime of
+    Ok (origmsg, rq) -> do
+              logDebug $ "Processing request: " ++ origmsg
+              mcfg <- readIORef cfgref
+              let response = respondInner mcfg hmac rq
+              _ <- S.sendTo socket response peer
+              return ()
+    Bad err -> logInfo $ "Failed to parse incoming message: " ++ err
+  return ()
+
+-- | Mesage parsing. This can either result in a good, valid message,
+-- or fail in the Result monad.
+parseMessage :: HashKey -> String -> Integer
+             -> Result (String, ConfdRequest)
+parseMessage hmac msg curtime = do
+  (salt, origmsg, request) <- parseRequest hmac msg
+  ts <- tryRead "Parsing timestamp" salt::Result Integer
+  if (abs (ts - curtime) > (fromIntegral C.confdMaxClockSkew))
+    then fail "Too old/too new timestamp or clock skew"
+    else return (origmsg, request)
+
+-- | Inner helper function for a given client. This generates the
+-- final encoded message (as a string), ready to be sent out to the
+-- client.
+respondInner :: Result (ConfigData, LinkIpMap) -> HashKey
+             -> ConfdRequest -> String
+respondInner cfg hmac rq =
+  let rsalt = confdRqRsalt rq
+      innermsg = serializeResponse (cfg >>= flip buildResponse rq)
+      innerserialised = J.encodeStrict innermsg
+      outermsg = signMessage hmac rsalt innerserialised
+      outerserialised = confdMagicFourcc ++ J.encodeStrict outermsg
+  in outerserialised
+
+-- | Main listener loop.
+listener :: S.Socket -> HashKey
+         -> (S.Socket -> HashKey -> String -> S.SockAddr -> IO ())
+         -> IO ()
+listener s hmac resp = do
+  (msg, _, peer) <- S.recvFrom s 4096
+  if confdMagicFourcc `isPrefixOf` msg
+    then (forkIO $ resp s hmac (drop 4 msg) peer) >> return ()
+    else logDebug "Invalid magic code!" >> return ()
+  return ()
+
+-- | Main function.
+main :: DaemonOptions -> IO ()
+main opts = do
+  parseresult <- parseAddress opts C.defaultConfdPort
+  (af_family, bindaddr) <- exitIfBad "parsing bind address" parseresult
+  s <- S.socket af_family S.Datagram S.defaultProtocol
+  S.bindSocket s bindaddr
+  cref <- newIORef (Bad "Configuration not yet loaded")
+  statemvar <- newMVar initialState
+  hmac <- getClusterHmac
+  -- Inotify setup
+  inotify <- initINotify
+  let inotiaction = addNotifier inotify C.clusterConfFile cref statemvar
+  -- fork the timeout timer
+  _ <- forkIO $ onTimeoutTimer inotiaction C.clusterConfFile cref statemvar
+  -- fork the polling timer
+  _ <- forkIO $ onReloadTimer inotiaction C.clusterConfFile cref statemvar
+  -- and finally enter the responder loop
+  forever $ listener s hmac (responder cref)
diff --git a/htools/Ganeti/Config.hs b/htools/Ganeti/Config.hs
new file mode 100644 (file)
index 0000000..aad9a7e
--- /dev/null
@@ -0,0 +1,200 @@
+{-| Implementation of the Ganeti configuration database.
+
+-}
+
+{-
+
+Copyright (C) 2011, 2012 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.Config
+    ( LinkIpMap
+    , loadConfig
+    , getNodeInstances
+    , getDefaultNicLink
+    , getInstancesIpByLink
+    , getNode
+    , getInstance
+    , getInstPrimaryNode
+    , getInstMinorsForNode
+    , buildLinkIpInstnameMap
+    , instNodes
+    ) where
+
+import Data.List (foldl')
+import qualified Data.Map as M
+import qualified Data.Set as S
+import qualified Text.JSON as J
+
+import Ganeti.HTools.JSON
+import Ganeti.BasicTypes
+
+import qualified Ganeti.Constants as C
+import Ganeti.Objects
+
+-- | Type alias for the link and ip map.
+type LinkIpMap = M.Map String (M.Map String String)
+
+-- | Reads the config file.
+readConfig :: FilePath -> IO String
+readConfig = readFile
+
+-- | Parses the configuration file.
+parseConfig :: String -> Result ConfigData
+parseConfig = fromJResult "parsing configuration" . J.decodeStrict
+
+-- | Wrapper over 'readConfig' and 'parseConfig'.
+loadConfig :: FilePath -> IO (Result ConfigData)
+loadConfig = fmap parseConfig . readConfig
+
+-- * Query functions
+
+-- | Computes the nodes covered by a disk.
+computeDiskNodes :: Disk -> S.Set String
+computeDiskNodes dsk =
+  case diskLogicalId dsk of
+    LIDDrbd8 nodeA nodeB _ _ _ _ -> S.fromList [nodeA, nodeB]
+    _ -> S.empty
+
+-- | Computes all disk-related nodes of an instance. For non-DRBD,
+-- this will be empty, for DRBD it will contain both the primary and
+-- the secondaries.
+instDiskNodes :: Instance -> S.Set String
+instDiskNodes = S.unions . map computeDiskNodes . instDisks
+
+-- | Computes all nodes of an instance.
+instNodes :: Instance -> S.Set String
+instNodes inst = instPrimaryNode inst `S.insert` instDiskNodes inst
+
+-- | Computes the secondary nodes of an instance. Since this is valid
+-- only for DRBD, we call directly 'instDiskNodes', skipping over the
+-- extra primary insert.
+instSecondaryNodes :: Instance -> S.Set String
+instSecondaryNodes inst =
+  instPrimaryNode inst `S.delete` instDiskNodes inst
+
+-- | Get instances of a given node.
+getNodeInstances :: ConfigData -> String -> ([Instance], [Instance])
+getNodeInstances cfg nname =
+    let all_inst = M.elems . configInstances $ cfg
+        pri_inst = filter ((== nname) . instPrimaryNode) all_inst
+        sec_inst = filter ((nname `S.member`) . instSecondaryNodes) all_inst
+    in (pri_inst, sec_inst)
+
+-- | Returns the default cluster link.
+getDefaultNicLink :: ConfigData -> String
+getDefaultNicLink =
+  nicpLink . (M.! C.ppDefault) . clusterNicparams . configCluster
+
+-- | Returns instances of a given link.
+getInstancesIpByLink :: LinkIpMap -> String -> [String]
+getInstancesIpByLink linkipmap link =
+  M.keys $ M.findWithDefault M.empty link linkipmap
+
+-- | Generic lookup function that converts from a possible abbreviated
+-- name to a full name.
+getItem :: String -> String -> M.Map String a -> Result a
+getItem kind name allitems = do
+  let lresult = lookupName (M.keys allitems) name
+      err = \details -> Bad $ kind ++ " name " ++ name ++ " " ++ details
+  fullname <- case lrMatchPriority lresult of
+                PartialMatch -> Ok $ lrContent lresult
+                ExactMatch -> Ok $ lrContent lresult
+                MultipleMatch -> err "has multiple matches"
+                FailMatch -> err "not found"
+  maybe (err "not found after successfull match?!") Ok $
+        M.lookup fullname allitems
+
+-- | Looks up a node.
+getNode :: ConfigData -> String -> Result Node
+getNode cfg name = getItem "Node" name (configNodes cfg)
+
+-- | Looks up an instance.
+getInstance :: ConfigData -> String -> Result Instance
+getInstance cfg name = getItem "Instance" name (configInstances cfg)
+
+-- | Looks up an instance's primary node.
+getInstPrimaryNode :: ConfigData -> String -> Result Node
+getInstPrimaryNode cfg name =
+  getInstance cfg name >>= return . instPrimaryNode >>= getNode cfg
+
+-- | Filters DRBD minors for a given node.
+getDrbdMinorsForNode :: String -> Disk -> [(Int, String)]
+getDrbdMinorsForNode node disk =
+  let child_minors = concatMap (getDrbdMinorsForNode node) (diskChildren disk)
+      this_minors =
+        case diskLogicalId disk of
+          LIDDrbd8 nodeA nodeB _ minorA minorB _
+            | nodeA == node -> [(minorA, nodeB)]
+            | nodeB == node -> [(minorB, nodeA)]
+          _ -> []
+  in this_minors ++ child_minors
+
+-- | String for primary role.
+rolePrimary :: String
+rolePrimary = "primary"
+
+-- | String for secondary role.
+roleSecondary :: String
+roleSecondary = "secondary"
+
+-- | Gets the list of DRBD minors for an instance that are related to
+-- a given node.
+getInstMinorsForNode :: String -> Instance
+                     -> [(String, Int, String, String, String, String)]
+getInstMinorsForNode node inst =
+  let role = if node == instPrimaryNode inst
+               then rolePrimary
+               else roleSecondary
+      iname = instName inst
+  -- FIXME: the disk/ build there is hack-ish; unify this in a
+  -- separate place, or reuse the iv_name (but that is deprecated on
+  -- the Python side)
+  in concatMap (\(idx, dsk) ->
+            [(node, minor, iname, "disk/" ++ show idx, role, peer)
+               | (minor, peer) <- getDrbdMinorsForNode node dsk]) .
+     zip [(0::Int)..] . instDisks $ inst
+
+-- | Builds link -> ip -> instname map.
+--
+-- TODO: improve this by splitting it into multiple independent functions:
+--
+-- * abstract the \"fetch instance with filled params\" functionality
+--
+-- * abstsract the [instance] -> [(nic, instance_name)] part
+--
+-- * etc.
+buildLinkIpInstnameMap :: ConfigData -> LinkIpMap
+buildLinkIpInstnameMap cfg =
+  let cluster = configCluster cfg
+      instances = M.elems . configInstances $ cfg
+      defparams = (M.!) (clusterNicparams cluster) C.ppDefault
+      nics = concatMap (\i -> [(instName i, nic) | nic <- instNics i])
+             instances
+  in foldl' (\accum (iname, nic) ->
+               let pparams = nicNicparams nic
+                   fparams = fillNICParams defparams pparams
+                   link = nicpLink fparams
+               in case nicIp nic of
+                    Nothing -> accum
+                    Just ip -> let oldipmap = M.findWithDefault (M.empty)
+                                              link accum
+                                   newipmap = M.insert ip iname oldipmap
+                               in M.insert link newipmap accum
+            ) M.empty nics
diff --git a/htools/Ganeti/Daemon.hs b/htools/Ganeti/Daemon.hs
new file mode 100644 (file)
index 0000000..c6708f1
--- /dev/null
@@ -0,0 +1,353 @@
+{-| Implementation of the generic daemon functionality.
+
+-}
+
+{-
+
+Copyright (C) 2011, 2012 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.Daemon
+  ( DaemonOptions(..)
+  , OptType
+  , defaultOptions
+  , oShowHelp
+  , oShowVer
+  , oNoDaemonize
+  , oNoUserChecks
+  , oDebug
+  , oPort
+  , oBindAddress
+  , oSyslogUsage
+  , parseArgs
+  , parseAddress
+  , writePidFile
+  , genericMain
+  ) where
+
+import Control.Exception
+import Control.Monad
+import Data.Maybe (fromMaybe)
+import qualified Data.Version
+import Data.Word
+import GHC.IO.Handle (hDuplicateTo)
+import qualified Network.Socket as Socket
+import Prelude hiding (catch)
+import System.Console.GetOpt
+import System.Exit
+import System.Environment
+import System.Info
+import System.IO
+import System.Posix.Directory
+import System.Posix.Files
+import System.Posix.IO
+import System.Posix.Process
+import System.Posix.Types
+import System.Posix.Signals
+import Text.Printf
+
+import Ganeti.Logging
+import Ganeti.Runtime
+import Ganeti.BasicTypes
+import Ganeti.HTools.Utils
+import qualified Ganeti.HTools.Version as Version(version)
+import qualified Ganeti.Constants as C
+import qualified Ganeti.Ssconf as Ssconf
+
+-- * Constants
+
+-- | \/dev\/null path.
+devNull :: FilePath
+devNull = "/dev/null"
+
+-- * Data types
+
+-- | Command line options structure.
+data DaemonOptions = DaemonOptions
+  { optShowHelp     :: Bool           -- ^ Just show the help
+  , optShowVer      :: Bool           -- ^ Just show the program version
+  , optDaemonize    :: Bool           -- ^ Whether to daemonize or not
+  , optPort         :: Maybe Word16   -- ^ Override for the network port
+  , optDebug        :: Bool           -- ^ Enable debug messages
+  , optNoUserChecks :: Bool           -- ^ Ignore user checks
+  , optBindAddress  :: Maybe String   -- ^ Override for the bind address
+  , optSyslogUsage  :: Maybe SyslogUsage -- ^ Override for Syslog usage
+  }
+
+-- | Default values for the command line options.
+defaultOptions :: DaemonOptions
+defaultOptions  = DaemonOptions
+  { optShowHelp     = False
+  , optShowVer      = False
+  , optDaemonize    = True
+  , optPort         = Nothing
+  , optDebug        = False
+  , optNoUserChecks = False
+  , optBindAddress  = Nothing
+  , optSyslogUsage  = Nothing
+  }
+
+-- | Abrreviation for the option type.
+type OptType = OptDescr (DaemonOptions -> Result DaemonOptions)
+
+-- | Helper function for required arguments which need to be converted
+-- as opposed to stored just as string.
+reqWithConversion :: (String -> Result a)
+                  -> (a -> DaemonOptions -> Result DaemonOptions)
+                  -> String
+                  -> ArgDescr (DaemonOptions -> Result DaemonOptions)
+reqWithConversion conversion_fn updater_fn metavar =
+  ReqArg (\string_opt opts -> do
+            parsed_value <- conversion_fn string_opt
+            updater_fn parsed_value opts) metavar
+
+-- * Command line options
+
+oShowHelp :: OptType
+oShowHelp = Option "h" ["help"]
+            (NoArg (\ opts -> Ok opts { optShowHelp = True}))
+            "Show the help message and exit"
+
+oShowVer :: OptType
+oShowVer = Option "V" ["version"]
+           (NoArg (\ opts -> Ok opts { optShowVer = True}))
+           "Show the version of the program and exit"
+
+oNoDaemonize :: OptType
+oNoDaemonize = Option "f" ["foreground"]
+               (NoArg (\ opts -> Ok opts { optDaemonize = False}))
+               "Don't detach from the current terminal"
+
+oDebug :: OptType
+oDebug = Option "d" ["debug"]
+         (NoArg (\ opts -> Ok opts { optDebug = True }))
+         "Enable debug messages"
+
+oNoUserChecks :: OptType
+oNoUserChecks = Option "" ["no-user-checks"]
+         (NoArg (\ opts -> Ok opts { optNoUserChecks = True }))
+         "Ignore user checks"
+
+oPort :: Int -> OptType
+oPort def = Option "p" ["port"]
+            (reqWithConversion (tryRead "reading port")
+             (\port opts -> Ok opts { optPort = Just port }) "PORT")
+            ("Network port (default: " ++ show def ++ ")")
+
+oBindAddress :: OptType
+oBindAddress = Option "b" ["bind"]
+               (ReqArg (\addr opts -> Ok opts { optBindAddress = Just addr })
+                "ADDR")
+               "Bind address (default depends on cluster configuration)"
+
+oSyslogUsage :: OptType
+oSyslogUsage = Option "" ["syslog"]
+               (reqWithConversion syslogUsageFromRaw
+                (\su opts -> Ok opts { optSyslogUsage = Just su })
+                "SYSLOG")
+               ("Enable logging to syslog (except debug \
+                \messages); one of 'no', 'yes' or 'only' [" ++ C.syslogUsage ++
+                "]")
+
+-- | 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 (DaemonOptions, [String]) -- ^ The resulting options
+                                          -- and leftover arguments
+parseOpts argv progname options =
+  case getOpt Permute options argv of
+    (opt_list, args, []) ->
+      do
+        parsed_opts <-
+          exitIfBad "Error while parsing command line arguments" $
+          foldM (flip id) defaultOptions opt_list
+        return (parsed_opts, args)
+    (_, _, errs) -> do
+      hPutStrLn stderr $ "Command line error: "  ++ concat errs
+      hPutStrLn stderr $ usageHelp progname options
+      exitWith $ ExitFailure 2
+
+-- | Small wrapper over getArgs and 'parseOpts'.
+parseArgs :: String -> [OptType] -> IO (DaemonOptions, [String])
+parseArgs cmd options = do
+  cmd_args <- getArgs
+  parseOpts cmd_args cmd options
+
+-- * Daemon-related functions
+-- | PID file mode.
+pidFileMode :: FileMode
+pidFileMode = unionFileModes ownerReadMode ownerWriteMode
+
+-- | Writes a PID file and locks it.
+_writePidFile :: FilePath -> IO Fd
+_writePidFile path = do
+  fd <- createFile path pidFileMode
+  setLock fd (WriteLock, AbsoluteSeek, 0, 0)
+  my_pid <- getProcessID
+  _ <- fdWrite fd (show my_pid ++ "\n")
+  return fd
+
+-- | Helper to format an IOError.
+formatIOError :: String -> IOError -> String
+formatIOError msg err = msg ++ ": " ++  show err
+
+-- | Wrapper over '_writePidFile' that transforms IO exceptions into a
+-- 'Bad' value.
+writePidFile :: FilePath -> IO (Result Fd)
+writePidFile path = do
+  catch (fmap Ok $ _writePidFile path)
+    (return . Bad . formatIOError "Failure during writing of the pid file")
+
+-- | Sets up a daemon's environment.
+setupDaemonEnv :: FilePath -> FileMode -> IO ()
+setupDaemonEnv cwd umask = do
+  changeWorkingDirectory cwd
+  _ <- setFileCreationMask umask
+  _ <- createSession
+  return ()
+
+-- | Signal handler for reopening log files.
+handleSigHup :: FilePath -> IO ()
+handleSigHup path = do
+  setupDaemonFDs (Just path)
+  logInfo "Reopening log files after receiving SIGHUP"
+
+-- | Sets up a daemon's standard file descriptors.
+setupDaemonFDs :: Maybe FilePath -> IO ()
+setupDaemonFDs logfile = do
+  null_in_handle <- openFile devNull ReadMode
+  null_out_handle <- openFile (fromMaybe devNull logfile) AppendMode
+  hDuplicateTo null_in_handle stdin
+  hDuplicateTo null_out_handle stdout
+  hDuplicateTo null_out_handle stderr
+  hClose null_in_handle
+  hClose null_out_handle
+
+-- | Computes the default bind address for a given family.
+defaultBindAddr :: Int                  -- ^ The port we want
+                -> Socket.Family        -- ^ The cluster IP family
+                -> Result (Socket.Family, Socket.SockAddr)
+defaultBindAddr port Socket.AF_INET =
+  Ok $ (Socket.AF_INET,
+        Socket.SockAddrInet (fromIntegral port) Socket.iNADDR_ANY)
+defaultBindAddr port Socket.AF_INET6 =
+  Ok $ (Socket.AF_INET6,
+        Socket.SockAddrInet6 (fromIntegral port) 0 Socket.iN6ADDR_ANY 0)
+defaultBindAddr _ fam = Bad $ "Unsupported address family: " ++ show fam
+
+-- | Default hints for the resolver
+resolveAddrHints :: Maybe Socket.AddrInfo
+resolveAddrHints =
+  Just Socket.defaultHints { Socket.addrFlags = [Socket.AI_NUMERICHOST,
+                                                 Socket.AI_NUMERICSERV] }
+
+-- | Resolves a numeric address.
+resolveAddr :: Int -> String -> IO (Result (Socket.Family, Socket.SockAddr))
+resolveAddr port str = do
+  resolved <- Socket.getAddrInfo resolveAddrHints (Just str) (Just (show port))
+  return $ case resolved of
+             [] -> Bad "Invalid results from lookup?"
+             best:_ -> Ok $ (Socket.addrFamily best, Socket.addrAddress best)
+
+-- | Based on the options, compute the socket address to use for the
+-- daemon.
+parseAddress :: DaemonOptions      -- ^ Command line options
+             -> Int                -- ^ Default port for this daemon
+             -> IO (Result (Socket.Family, Socket.SockAddr))
+parseAddress opts defport = do
+  let port = maybe defport fromIntegral $ optPort opts
+  def_family <- Ssconf.getPrimaryIPFamily Nothing
+  ainfo <- case optBindAddress opts of
+             Nothing -> return (def_family >>= defaultBindAddr port)
+             Just saddr -> catch (resolveAddr port saddr)
+                           (annotateIOError $ "Invalid address " ++ saddr)
+  return ainfo
+
+-- | Run an I/O action as a daemon.
+--
+-- WARNING: this only works in single-threaded mode (either using the
+-- single-threaded runtime, or using the multi-threaded one but with
+-- only one OS thread, i.e. -N1).
+--
+-- FIXME: this doesn't support error reporting and the prepfn
+-- functionality.
+daemonize :: FilePath -> IO () -> IO ()
+daemonize logfile action = do
+  -- first fork
+  _ <- forkProcess $ do
+    -- in the child
+    setupDaemonEnv "/" (unionFileModes groupModes otherModes)
+    setupDaemonFDs $ Just logfile
+    _ <- installHandler lostConnection (Catch (handleSigHup logfile)) Nothing
+    _ <- forkProcess action
+    exitImmediately ExitSuccess
+  exitImmediately ExitSuccess
+
+-- | Generic daemon startup.
+genericMain :: GanetiDaemon -> [OptType] -> (DaemonOptions -> IO ()) -> IO ()
+genericMain daemon options main = do
+  let progname = daemonName daemon
+  (opts, args) <- parseArgs progname options
+
+  when (optShowHelp opts) $ do
+    putStr $ usageHelp progname options
+    exitWith ExitSuccess
+  when (optShowVer opts) $ 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
+
+  exitUnless (null args) "This program doesn't take any arguments"
+
+  unless (optNoUserChecks opts) $ do
+    runtimeEnts <- getEnts
+    ents <- exitIfBad "Can't find required user/groups" runtimeEnts
+    verifyDaemonUser daemon ents
+
+  syslog <- case optSyslogUsage opts of
+              Nothing -> exitIfBad "Invalid cluster syslog setting" $
+                         syslogUsageFromRaw C.syslogUsage
+              Just v -> return v
+  let processFn = if optDaemonize opts
+                    then daemonize (daemonLogFile daemon)
+                    else id
+  processFn $ innerMain daemon opts syslog (main opts)
+
+-- | Inner daemon function.
+--
+-- This is executed after daemonization.
+innerMain :: GanetiDaemon -> DaemonOptions -> SyslogUsage -> IO () -> IO ()
+innerMain daemon opts syslog main = do
+  let logfile = if optDaemonize opts
+                  then Nothing
+                  else Just $ daemonLogFile daemon
+  setupLogging logfile (daemonName daemon) (optDebug opts) True False syslog
+  pid_fd <- writePidFile (daemonPidFile daemon)
+  _ <- exitIfBad "Cannot write PID file; already locked? Error" pid_fd
+  logNotice "starting"
+  main
index d5fdc69..29f1728 100644 (file)
@@ -8,7 +8,7 @@ used in many other places and this is more IO oriented.
 
 {-
 
-Copyright (C) 2009, 2010, 2011 Google Inc.
+Copyright (C) 2009, 2010, 2011, 2012 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
@@ -28,68 +28,79 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 -}
 
 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
+  ( Options(..)
+  , OptType
+  , parseOpts
+  , parseOptsInner
+  , parseYesNo
+  , parseISpecString
+  , shTemplate
+  , defaultLuxiSocket
+  , maybePrintNodes
+  , maybePrintInsts
+  , maybeShowWarnings
+  , printKeys
+  , printFinal
+  , setNodeStatus
+  -- * The options
+  , oDataFile
+  , oDiskMoves
+  , oDiskTemplate
+  , oSpindleUse
+  , oDynuFile
+  , oEvacMode
+  , oExInst
+  , oExTags
+  , oExecJobs
+  , oGroup
+  , oIAllocSrc
+  , oInstMoves
+  , oLuxiSocket
+  , oMachineReadable
+  , oMaxCpu
+  , oMaxSolLength
+  , oMinDisk
+  , oMinGain
+  , oMinGainLim
+  , oMinScore
+  , oNoHeaders
+  , oNoSimulation
+  , oNodeSim
+  , oOfflineNode
+  , oOutputDir
+  , oPrintCommands
+  , oPrintInsts
+  , oPrintNodes
+  , oQuiet
+  , oRapiMaster
+  , oReplay
+  , oSaveCluster
+  , oSelInst
+  , oShowHelp
+  , oShowVer
+  , oStdSpec
+  , oTestCount
+  , oTieredSpec
+  , oVerbose
+  ) where
 
 import Control.Monad
+import Data.Char (toUpper)
 import Data.Maybe (fromMaybe)
 import qualified Data.Version
 import System.Console.GetOpt
 import System.IO
 import System.Info
-import System
+import System.Exit
 import Text.Printf (printf)
 
 import qualified Ganeti.HTools.Version as Version(version)
+import qualified Ganeti.HTools.Container as Container
+import qualified Ganeti.HTools.Node as Node
 import qualified Ganeti.Constants as C
 import Ganeti.HTools.Types
 import Ganeti.HTools.Utils
+import Ganeti.BasicTypes
 
 -- * Constants
 
@@ -103,86 +114,109 @@ defaultLuxiSocket = C.masterSocket
 
 -- | 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
+  { optDataFile    :: Maybe FilePath -- ^ Path to the cluster data file
+  , optDiskMoves   :: Bool           -- ^ Allow disk moves
+  , optInstMoves   :: Bool           -- ^ Allow instance moves
+  , optDiskTemplate :: Maybe DiskTemplate  -- ^ Override for the disk template
+  , optSpindleUse  :: Maybe Int      -- ^ Override for the spindle usage
+  , 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
+  , optIAllocSrc   :: Maybe FilePath -- ^ The iallocation spec
+  , optSelInst     :: [String]       -- ^ Instances to be excluded
+  , 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        :: Maybe Double   -- ^ Override 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
+  , optNoSimulation :: Bool          -- ^ Skip the rebalancing dry-run
+  , optNodeSim     :: [String]       -- ^ Cluster simulation mode
+  , optOffline     :: [String]       -- ^ Names of offline nodes
+  , 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
+  , optStdSpec     :: Maybe RSpec    -- ^ Requested standard specs
+  , optTestCount   :: Maybe Int      -- ^ Optional test count override
+  , 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
- }
+  { optDataFile    = Nothing
+  , optDiskMoves   = True
+  , optInstMoves   = True
+  , optDiskTemplate = Nothing
+  , optSpindleUse  = Nothing
+  , optDynuFile    = Nothing
+  , optEvacMode    = False
+  , optExInst      = []
+  , optExTags      = Nothing
+  , optExecJobs    = False
+  , optGroup       = Nothing
+  , optIAllocSrc   = Nothing
+  , optSelInst     = []
+  , optLuxi        = Nothing
+  , optMachineReadable = False
+  , optMaster      = ""
+  , optMaxLength   = -1
+  , optMcpu        = Nothing
+  , optMdsk        = defReservedDiskRatio
+  , optMinGain     = 1e-2
+  , optMinGainLim  = 1e-1
+  , optMinScore    = 1e-9
+  , optNoHeaders   = False
+  , optNoSimulation = False
+  , optNodeSim     = []
+  , optOffline     = []
+  , optOutPath     = "."
+  , optSaveCluster = Nothing
+  , optShowCmds    = Nothing
+  , optShowHelp    = False
+  , optShowInsts   = False
+  , optShowNodes   = Nothing
+  , optShowVer     = False
+  , optStdSpec     = Nothing
+  , optTestCount   = Nothing
+  , optTieredSpec  = Nothing
+  , optReplay      = Nothing
+  , optVerbose     = 1
+  }
 
 -- | Abrreviation for the option type.
 type OptType = OptDescr (Options -> Result Options)
 
+-- * Helper functions
+
+parseISpecString :: String -> String -> Result RSpec
+parseISpecString descr inp = do
+  let sp = sepSplit ',' inp
+      err = Bad ("Invalid " ++ descr ++ " specification: '" ++ inp ++
+                 "', expected disk,ram,cpu")
+  when (length sp /= 3) err
+  prs <- mapM (\(fn, val) -> fn val) $
+         zip [ annotateResult (descr ++ " specs disk") . parseUnit
+             , annotateResult (descr ++ " specs memory") . parseUnit
+             , tryRead (descr ++ " specs cpus")
+             ] sp
+  case prs of
+    [dsk, ram, cpu] -> return $ RSpec cpu ram dsk
+    _ -> err
+
 -- * Command line options
 
 oDataFile :: OptType
@@ -199,9 +233,20 @@ oDiskMoves = Option "" ["no-disk-moves"]
 oDiskTemplate :: OptType
 oDiskTemplate = Option "" ["disk-template"]
                 (ReqArg (\ t opts -> do
-                           dt <- dtFromString t
-                           return $ opts { optDiskTemplate = dt }) "TEMPLATE")
-                "select the desired disk template"
+                           dt <- diskTemplateFromRaw t
+                           return $ opts { optDiskTemplate = Just dt })
+                 "TEMPLATE") "select the desired disk template"
+
+oSpindleUse :: OptType
+oSpindleUse = Option "" ["spindle-use"]
+              (ReqArg (\ n opts -> do
+                         su <- tryRead "parsing spindle-use" n
+                         when (su < 0) $
+                              fail "Invalid value of the spindle-use\
+                                   \ (expected >= 0)"
+                         return $ opts { optSpindleUse = Just su })
+               "SPINDLES") "select how many virtual spindle instances use\
+                           \ [default read from cluster]"
 
 oSelInst :: OptType
 oSelInst = Option "" ["select-instances"]
@@ -247,32 +292,10 @@ 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"
+oIAllocSrc :: OptType
+oIAllocSrc = Option "I" ["ialloc-src"]
+             (ReqArg (\ f opts -> Ok opts { optIAllocSrc = Just f }) "FILE")
+             "Specify an iallocator spec as the cluster data source"
 
 oLuxiSocket :: OptType
 oLuxiSocket = Option "L" ["luxi"]
@@ -282,7 +305,7 @@ oLuxiSocket = Option "L" ["luxi"]
 
 oMachineReadable :: OptType
 oMachineReadable = Option "" ["machine-readable"]
-          (OptArg (\ f opts -> do
+                   (OptArg (\ f opts -> do
                      flag <- parseYesNo True f
                      return $ opts { optMachineReadable = flag }) "CHOICE")
           "enable machine readable output (pass either 'yes' or 'no' to\
@@ -291,15 +314,21 @@ oMachineReadable = Option "" ["machine-readable"]
 
 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]"
+          (ReqArg (\ n opts -> do
+                     mcpu <- tryRead "parsing max-cpu" n
+                     when (mcpu <= 0) $
+                          fail "Invalid value of the max-cpu ratio,\
+                               \ expected >0"
+                     return $ opts { optMcpu = Just mcpu }) "RATIO")
+          "maximum virtual-to-physical cpu ratio for nodes (from 0\
+          \ upwards) [default read from cluster]"
 
 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)"
+                "cap the solution at this many balancing or allocation \
+                \ rounds (useful for very unbalanced clusters or empty \
+                \ clusters)"
 
 oMinDisk :: OptType
 oMinDisk = Option "" ["min-disk"]
@@ -326,21 +355,22 @@ oNoHeaders = Option "" ["no-headers"]
              (NoArg (\ opts -> Ok opts { optNoHeaders = True }))
              "do not show a header line"
 
+oNoSimulation :: OptType
+oNoSimulation = Option "" ["no-simulation"]
+                (NoArg (\opts -> Ok opts {optNoSimulation = True}))
+                "do not perform rebalancing simulation"
+
 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'"
+            "simulate an empty cluster, given as\
+            \ 'alloc_policy,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")
@@ -363,11 +393,11 @@ oPrintInsts = Option "" ["print-instances"]
 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 }) .
+                          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"
 
@@ -396,23 +426,27 @@ oShowVer = Option "V" ["version"]
            (NoArg (\ opts -> Ok opts { optShowVer = True}))
            "show the version of the program"
 
+oStdSpec :: OptType
+oStdSpec = Option "" ["standard-alloc"]
+             (ReqArg (\ inp opts -> do
+                        tspec <- parseISpecString "standard" inp
+                        return $ opts { optStdSpec = Just tspec } )
+              "STDSPEC")
+             "enable standard specs allocation, given as 'disk,ram,cpu'"
+
+oTestCount :: OptType
+oTestCount = Option "" ["test-count"]
+             (ReqArg (\ inp opts -> do
+                        tcount <- tryRead "parsing test count" inp
+                        return $ opts { optTestCount = Just tcount } )
+              "COUNT")
+             "override the target test count"
+
 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 <- parseISpecString "tiered" inp
+                        return $ opts { optTieredSpec = Just tspec } )
               "TSPEC")
              "enable tiered specs allocation, given as 'disk,ram,cpu'"
 
@@ -429,20 +463,28 @@ oVerbose = Option "v" ["verbose"]
 -- * Functions
 
 -- | Helper for parsing a yes\/no command line flag.
-parseYesNo :: Bool         -- ^ Default whalue (when we get a @Nothing@)
+parseYesNo :: Bool         -- ^ Default value (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'"
+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)
+  usageInfo (printf "%s %s\nUsage: %s [OPTION...]"
+             progname Version.version progname)
+
+-- | Show the program version info.
+versionInfo :: String -> String
+versionInfo progname =
+  printf "%s %s\ncompiled with %s %s\nrunning on %s %s\n"
+         progname Version.version compilerName
+         (Data.Version.showVersion compilerVersion)
+         os arch
 
 -- | Command line parser, using the 'Options' structure.
 parseOpts :: [String]               -- ^ The command line arguments
@@ -451,45 +493,47 @@ parseOpts :: [String]               -- ^ The command line arguments
           -> 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
+  case parseOptsInner argv progname options of
+    Left (code, msg) -> do
+      hPutStr (if code == 0 then stdout else stderr) msg
+      exitWith (if code == 0 then ExitSuccess else ExitFailure code)
+    Right result ->
+      return result
+
+-- | Inner parse options. The arguments are similar to 'parseOpts',
+-- but it returns either a 'Left' composed of exit code and message,
+-- or a 'Right' for the success case.
+parseOptsInner :: [String] -> String -> [OptType]
+               -> Either (Int, String) (Options, [String])
+parseOptsInner argv progname options =
+  case getOpt Permute options argv of
+    (o, n, []) ->
+      let (pr, args) = (foldM (flip id) defaultOptions o, n)
+      in case pr of
+           Bad msg -> Left (1, "Error while parsing command\
+                               \line arguments:\n" ++ msg ++ "\n")
+           Ok po ->
+             select (Right (po, args))
+                 [ (optShowHelp po, Left (0, usageHelp progname options))
+                 , (optShowVer po,  Left (0, versionInfo progname))
+                 ]
+    (_, _, errs) ->
+      Left (2, "Command line error: "  ++ concat errs ++ "\n" ++
+            usageHelp progname options)
 
 -- | 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"
+  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
@@ -522,3 +566,53 @@ maybeShowWarnings fix_msgs =
   unless (null fix_msgs) $ do
     hPutStrLn stderr "Warning: cluster has inconsistent data:"
     hPutStrLn stderr . unlines . map (printf "  - %s") $ fix_msgs
+
+-- | Format a list of key, value as a shell fragment.
+printKeys :: String              -- ^ Prefix to printed variables
+          -> [(String, String)]  -- ^ List of (key, value) pairs to be printed
+          -> IO ()
+printKeys prefix = mapM_ (\(k, v) ->
+                       printf "%s_%s=%s\n" prefix (map toUpper k) (ensureQuoted v))
+
+-- | Prints the final @OK@ marker in machine readable output.
+printFinal :: String    -- ^ Prefix to printed variable
+           -> Bool      -- ^ Whether output should be machine readable
+                        -- Note: if not, there is nothing to print
+           -> IO ()
+printFinal prefix True =
+  -- this should be the final entry
+  printKeys prefix [("OK", "1")]
+
+printFinal _ False = return ()
+
+-- | Potentially set the node as offline based on passed offline list.
+setNodeOffline :: [Ndx] -> Node.Node -> Node.Node
+setNodeOffline offline_indices n =
+  if Node.idx n `elem` offline_indices
+    then Node.setOffline n True
+    else n
+
+-- | Set node properties based on command line options.
+setNodeStatus :: Options -> Node.List -> IO Node.List
+setNodeStatus opts fixed_nl = do
+  let offline_passed = optOffline opts
+      all_nodes = Container.elems fixed_nl
+      offline_lkp = map (lookupName (map Node.name all_nodes)) offline_passed
+      offline_wrong = filter (not . goodLookupResult) offline_lkp
+      offline_names = map lrContent offline_lkp
+      offline_indices = map Node.idx $
+                        filter (\n -> Node.name n `elem` offline_names)
+                               all_nodes
+      m_cpu = optMcpu opts
+      m_dsk = optMdsk opts
+
+  unless (null offline_wrong) $ do
+         exitErr $ printf "wrong node name(s) set as offline: %s\n"
+                   (commaJoin (map lrContent offline_wrong))
+  let setMCpuFn = case m_cpu of
+                    Nothing -> id
+                    Just new_mcpu -> flip Node.setMcpu new_mcpu
+  let nm = Container.map (setNodeOffline offline_indices .
+                          flip Node.setMdsk m_dsk .
+                          setMCpuFn) fixed_nl
+  return nm
index 36595c8..9500aea 100644 (file)
@@ -7,7 +7,7 @@ goes into the /Main/ module for the individual binaries.
 
 {-
 
-Copyright (C) 2009, 2010, 2011 Google Inc.
+Copyright (C) 2009, 2010, 2011, 2012 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
@@ -27,59 +27,57 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 -}
 
 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
+  (
+    -- * Types
+    AllocSolution(..)
+  , EvacSolution(..)
+  , Table(..)
+  , CStats(..)
+  , AllocResult
+  , AllocMethod
+  -- * 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
+  , 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.Maybe (fromJust, isNothing)
 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
@@ -94,23 +92,21 @@ import qualified Ganeti.OpCodes as OpCodes
 
 -- | 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
+  { asFailures :: [FailMode]              -- ^ Failure counts
+  , asAllocs   :: Int                     -- ^ Good allocation count
+  , asSolution :: Maybe Node.AllocElement -- ^ The actual allocation result
+  , asLog      :: [String]                -- ^ 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 jobs
-    }
+  { esMoved   :: [(Idx, Gdx, [Ndx])]  -- ^ Instances moved successfully
+  , esFailed  :: [(Idx, String)]      -- ^ Instances which were not
+                                      -- relocated
+  , esOpCodes :: [[OpCodes.OpCode]]   -- ^ List of jobs
+  } deriving (Show)
 
 -- | Allocation results, as used in 'iterateAlloc' and 'tieredAlloc'.
 type AllocResult = (FailStats, Node.List, Instance.List,
@@ -118,15 +114,17 @@ type AllocResult = (FailStats, Node.List, Instance.List,
 
 -- | 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)]
+-- For a one-node allocation, this will be a @Left ['Ndx']@, whereas
+-- for a two-node allocation, this will be a @Right [('Ndx',
+-- ['Ndx'])]@. In the latter case, the list is basically an
+-- association list, grouped by primary node and holding the potential
+-- secondary nodes in the sub-list.
+type AllocNodes = Either [Ndx] [(Ndx, [Ndx])]
 
 -- | The empty solution we start with when computing allocations.
 emptyAllocSolution :: AllocSolution
 emptyAllocSolution = AllocSolution { asFailures = [], asAllocs = 0
-                                   , asSolutions = [], asLog = [] }
+                                   , asSolution = Nothing, asLog = [] }
 
 -- | The empty evac solution.
 emptyEvacSolution :: EvacSolution
@@ -140,32 +138,43 @@ 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)
+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 total virtual cpus
+  , csNcpu :: Double  -- ^ Equivalent to 'csIcpu' but in terms of
+                      -- physical CPUs, i.e. normalised used phys CPUs
+  , 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)
+
+-- | A simple type for allocation functions.
+type AllocMethod =  Node.List           -- ^ Node list
+                 -> Instance.List       -- ^ Instance list
+                 -> Maybe Int           -- ^ Optional allocation limit
+                 -> Instance.Instance   -- ^ Instance spec for allocation
+                 -> AllocNodes          -- ^ Which nodes we should allocate on
+                 -> [Instance.Instance] -- ^ Allocated instances
+                 -> [CStats]            -- ^ Running cluster stats
+                 -> Result AllocResult  -- ^ Allocation result
+
+-- | A simple type for the running solution of evacuations.
+type EvacInnerState =
+  Either String (Node.List, Instance.Instance, Score, Ndx)
 
 -- * Utility functions
 
@@ -190,57 +199,71 @@ computeBadItems nl il =
   in
     (bad_nodes, bad_instances)
 
+-- | Extracts the node pairs for an instance. This can fail if the
+-- instance is single-homed. FIXME: this needs to be improved,
+-- together with the general enhancement for handling non-DRBD moves.
+instanceNodes :: Node.List -> Instance.Instance ->
+                 (Ndx, Ndx, Node.Node, Node.Node)
+instanceNodes nl inst =
+  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
+  in (old_pdx, old_sdx, old_p, old_s)
+
 -- | 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
+emptyCStats = CStats 0 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)
-          }
+  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, csNcpu = x_ncpu,
+               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
+      inc_ncpu = fromIntegral (Node.uCpu node) /
+                 iPolicyVcpuRatio (Node.iPolicy 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
+        , csNcpu = x_ncpu + inc_ncpu
+        , 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 }
+  let cs = foldl' updateCStats emptyCStats . Container.elems $ nl
+  in cs { csScore = compCV nl }
 
 -- | Compute the delta between two cluster state.
 --
@@ -250,18 +273,27 @@ totalResources nl =
 -- 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)
+  let CStats {csImem = i_imem, csIdsk = i_idsk, csIcpu = i_icpu,
+              csNcpu = i_ncpu } = cini
+      CStats {csImem = f_imem, csIdsk = f_idsk, csIcpu = f_icpu,
+              csTmem = t_mem, csTdsk = t_dsk, csVcpu = f_vcpu,
+              csNcpu = f_ncpu, csTcpu = f_tcpu } = cfin
+      rini = AllocInfo { allocInfoVCpus = fromIntegral i_icpu
+                       , allocInfoNCpus = i_ncpu
+                       , allocInfoMem   = fromIntegral i_imem
+                       , allocInfoDisk  = fromIntegral i_idsk
+                       }
+      rfin = AllocInfo { allocInfoVCpus = fromIntegral (f_icpu - i_icpu)
+                       , allocInfoNCpus = f_ncpu - i_ncpu
+                       , allocInfoMem   = fromIntegral (f_imem - i_imem)
+                       , allocInfoDisk  = fromIntegral (f_idsk - i_idsk)
+                       }
+      runa = AllocInfo { allocInfoVCpus = fromIntegral (f_vcpu - f_icpu)
+                       , allocInfoNCpus = f_tcpu - f_ncpu
+                       , allocInfoMem   = truncate t_mem - fromIntegral f_imem
+                       , allocInfoDisk  = 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)]
@@ -277,6 +309,7 @@ detailedCVInfo = [ (1,  "free_mem_cv")
                  , (1,  "disk_load_cv")
                  , (1,  "net_load_cv")
                  , (2,  "pri_tags_score")
+                 , (1,  "spindles_cv")
                  ]
 
 -- | Holds the weights used by 'compCVNodes' for each metric.
@@ -286,46 +319,46 @@ 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 ]
+  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
+      -- metric: spindles %
+      spindles_cv = map (\n -> Node.instSpindles n / Node.hiSpindles n) nodes
+  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, stdDev spindles_cv ]
 
 -- | Compute the /total/ variance.
 compCVNodes :: [Node.Node] -> Double
@@ -344,127 +377,128 @@ getOnline = filter (not . Node.offline) . Container.elems
 -- | 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
+  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
+  let (old_pdx, old_sdx, old_p, old_s) = instanceNodes nl inst
+      int_p = Node.removePri old_p inst
+      int_s = Node.removeSec old_s inst
+      new_nl = do -- Maybe monad
+        new_p <- Node.addPriEx (Node.offline old_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
+
+-- Failover to any (fa)
+applyMove nl inst (FailoverToAny new_pdx) = do
+  let (old_pdx, old_sdx, old_pnode, _) = instanceNodes nl inst
+      new_pnode = Container.find new_pdx nl
+      force_failover = Node.offline old_pnode
+  new_pnode' <- Node.addPriEx force_failover new_pnode inst
+  let old_pnode' = Node.removePri old_pnode inst
+      inst' = Instance.setPri inst new_pdx
+      nl' = Container.addTwo old_pdx old_pnode' new_pdx new_pnode' nl
+  return (nl', inst', new_pdx, old_sdx)
 
 -- 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
+  let (old_pdx, old_sdx, old_p, old_s) = instanceNodes nl inst
+      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
+  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
+  let (old_pdx, old_sdx, old_p, old_s) = instanceNodes nl inst
+      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
+  let (old_pdx, old_sdx, old_p, old_s) = instanceNodes nl inst
+      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)
+  let p = Container.find new_pdx nl
+      new_inst = Instance.setBoth inst new_pdx Node.noSecondary
+  in do
+    Instance.instMatchesPolicy inst (Node.iPolicy p)
+    new_p <- Node.addPri p inst
+    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)
+  let tgt_p = Container.find new_pdx nl
+      tgt_s = Container.find new_sdx nl
+  in do
+    Instance.instMatchesPolicy inst (Node.iPolicy tgt_p)
+    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.
@@ -474,41 +508,48 @@ checkSingleStep :: Table -- ^ The original 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
+  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 :: MirrorType -- ^ The mirroring type of the instance
+              -> 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 MirrorNone _ _ _ = []
+
+possibleMoves MirrorExternal _ False _ = []
 
-possibleMoves _ False tdx =
-    [ReplaceSecondary tdx]
+possibleMoves MirrorExternal _ True tdx =
+  [ FailoverToAny tdx ]
 
-possibleMoves True True tdx =
-    [ReplaceSecondary tdx,
-     ReplaceAndFailover tdx,
-     ReplacePrimary tdx,
-     FailoverAndReplace tdx]
+possibleMoves MirrorInternal _ False tdx =
+  [ ReplaceSecondary tdx ]
 
-possibleMoves False True tdx =
-    [ReplaceSecondary tdx,
-     ReplaceAndFailover tdx]
+possibleMoves MirrorInternal True True tdx =
+  [ ReplaceSecondary tdx
+  , ReplaceAndFailover tdx
+  , ReplacePrimary tdx
+  , FailoverAndReplace tdx
+  ]
+
+possibleMoves MirrorInternal False True tdx =
+  [ ReplaceSecondary tdx
+  , ReplaceAndFailover tdx
+  ]
 
 -- | Compute the best move for a given instance.
 checkInstanceMove :: [Ndx]             -- ^ Allowed target node indices
@@ -518,18 +559,21 @@ checkInstanceMove :: [Ndx]             -- ^ Allowed target node indices
                   -> 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
+  let opdx = Instance.pNode target
+      osdx = Instance.sNode target
+      bad_nodes = [opdx, osdx]
+      nodes = filter (`notElem` bad_nodes) nodes_idx
+      mir_type = Instance.mirrorType target
+      use_secondary = elem osdx nodes_idx && inst_moves
+      aft_failover = if mir_type == MirrorInternal && use_secondary
+                       -- if drbd and 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 []
+      all_moves =
+        if disk_moves
+          then concatMap (possibleMoves mir_type use_secondary inst_moves)
+               nodes
+          else []
     in
       -- iterate over the possible nodes for this instance
       foldl' (checkSingleStep ini_tbl target) aft_failover all_moves
@@ -542,19 +586,19 @@ checkMove :: [Ndx]               -- ^ Allowed target node indices
           -> [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
+  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
 
@@ -564,9 +608,9 @@ doNextBalance :: Table     -- ^ The starting table
               -> 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
+  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
@@ -579,15 +623,15 @@ tryBalance :: Table       -- ^ The starting table
 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_nodes = Container.elems ini_nl
+        (offline_nodes, online_nodes) = partition Node.offline all_nodes
         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
+                      then let bad_nodes = map Node.idx offline_nodes
+                           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
+        node_idx = map Node.idx online_nodes
         fin_tbl = checkMove node_idx disk_moves inst_moves ini_tbl reloc_inst
         (Table _ _ fin_cv _) = fin_tbl
     in
@@ -603,50 +647,61 @@ collapseFailures flst =
     map (\k -> (k, foldl' (\a e -> if e == k then a + 1 else a) 0 flst))
             [minBound..maxBound]
 
+-- | Compares two Maybe AllocElement and chooses the besst score.
+bestAllocElement :: Maybe Node.AllocElement
+                 -> Maybe Node.AllocElement
+                 -> Maybe Node.AllocElement
+bestAllocElement a Nothing = a
+bestAllocElement Nothing b = b
+bestAllocElement a@(Just (_, _, _, ascore)) b@(Just (_, _, _, bscore)) =
+  if ascore < bscore then a else b
+
 -- | 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
+concatAllocs as (OpGood ns) =
+  let -- Choose the old or new solution, based on the cluster score
+    cntok = asAllocs as
+    osols = asSolution as
+    nsols = bestAllocElement osols (Just ns)
+    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 }
+  in nsols `seq` nsuc `seq` as { asAllocs = nsuc, asSolution = nsols }
+
+-- | Sums two 'AllocSolution' structures.
+sumAllocs :: AllocSolution -> AllocSolution -> AllocSolution
+sumAllocs (AllocSolution aFails aAllocs aSols aLog)
+          (AllocSolution bFails bAllocs bSols bLog) =
+  -- note: we add b first, since usually it will be smaller; when
+  -- fold'ing, a will grow and grow whereas b is the per-group
+  -- result, hence smaller
+  let nFails  = bFails ++ aFails
+      nAllocs = aAllocs + bAllocs
+      nSols   = bestAllocElement aSols bSols
+      nLog    = bLog ++ aLog
+  in AllocSolution nFails nAllocs nSols nLog
 
 -- | Given a solution, generates a reasonable description for it.
 describeSolution :: AllocSolution -> String
 describeSolution as =
   let fcnt = asFailures as
-      sols = asSolutions as
+      sols = asSolution 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)
+  in case sols of
+     Nothing -> "No valid allocation solutions, failure reasons: " ++
+                (if null fcnt then "unknown reasons" else freasons)
+     Just (_, _, nodes, cv) ->
+         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
@@ -658,7 +713,7 @@ annotateSolution as = as { asLog = describeSolution as : asLog as }
 -- for proper jobset execution, we should reverse all lists.
 reverseEvacSolution :: EvacSolution -> EvacSolution
 reverseEvacSolution (EvacSolution f m o) =
-    EvacSolution (reverse f) (reverse m) (reverse 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
@@ -668,18 +723,20 @@ genAllocNodes :: Group.List        -- ^ Group list
                                    -- unallocable nodes
               -> Result AllocNodes -- ^ The (monadic) result
 genAllocNodes gl nl count drop_unalloc =
-    let filter_fn = if 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"
+      all_nodes = filter_fn $ getOnline nl
+      all_pairs = [(Node.idx p,
+                    [Node.idx s | s <- all_nodes,
+                                       Node.idx p /= Node.idx s,
+                                       Node.group p == Node.group s]) |
+                   p <- all_nodes]
+  in case count of
+       1 -> Ok (Left (map Node.idx all_nodes))
+       2 -> Ok (Right (filter (not . null . snd) all_pairs))
+       _ -> Bad "Unsupported number of nodes, only one or two  supported"
 
 -- | Try to allocate an instance on the cluster.
 tryAlloc :: (Monad m) =>
@@ -688,22 +745,22 @@ tryAlloc :: (Monad m) =>
          -> Instance.Instance -- ^ The instance to allocate
          -> AllocNodes        -- ^ The allocation targets
          -> m AllocSolution   -- ^ Possible solution list
+tryAlloc _  _ _    (Right []) = fail "Not enough online nodes"
 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
-
+  let psols = parMap rwhnf (\(p, ss) ->
+                              foldl' (\cstate ->
+                                        concatAllocs cstate .
+                                        allocateOnPair nl inst p)
+                              emptyAllocSolution ss) ok_pairs
+      sols = foldl' sumAllocs emptyAllocSolution psols
+  in return $ annotateSolution sols
+
+tryAlloc _  _ _    (Left []) = fail "No online nodes"
 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
+  let sols = foldl' (\cstate ->
+                       concatAllocs cstate . allocateOnSingle nl inst
+                    ) emptyAllocSolution all_nodes
+  in return $ annotateSolution sols
 
 -- | Given a group/result, describe it as a nice (list of) messages.
 solutionDescription :: Group.List -> (Gdx, Result AllocSolution) -> [String]
@@ -713,7 +770,7 @@ solutionDescription gl (groupId, result) =
     Bad message -> [printf "Group %s: error %s" gname message]
   where grp = Container.find groupId gl
         gname = Group.name grp
-        pol = apolToString (Group.allocPolicy grp)
+        pol = allocPolicyToRaw (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
@@ -722,23 +779,23 @@ 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
+  where unallocable = not . Group.isAllocable . flip Container.find gl
+        fn accu (gdx, rasol) =
+          case rasol of
+            Bad _ -> accu
+            Ok sol | isNothing (asSolution 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
+  let extractScore (_, _, _, x) = x
+      solScore (gdx, sol) = (Group.allocPolicy (Container.find gdx gl),
+                             (extractScore . fromJust . asSolution) sol)
+  in sortBy (comparing solScore) sols
 
 -- | Finds the best group for an instance on a multi-group cluster.
 --
@@ -765,9 +822,13 @@ findBestAllocGroup mggl mgnl mgil allowed_gdxs inst cnt =
       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)
+       then if null groups'
+              then Bad $ "no groups for evacuation: allowed groups was" ++
+                     show allowed_gdxs ++ ", all groups: " ++
+                     show (map fst groups)
+              else 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
@@ -783,73 +844,6 @@ tryMGAlloc mggl mgnl mgil inst cnt = do
       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
@@ -858,7 +852,7 @@ tryEvac nl il idxs ex_ndx = do
 -- 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 ++
+  fail $ "Instances with disk template '" ++ diskTemplateToRaw dt ++
          "' can't execute change secondary"
 failOnSecondaryChange _ _ = return ()
 
@@ -876,10 +870,11 @@ nodeEvacInstance :: Node.List         -- ^ The node list (cluster-wide)
                  -> [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 nl il mode inst@(Instance.Instance
+                                  {Instance.diskTemplate = dt@DTDiskless})
+                 gdx avail_nodes =
+                   failOnSecondaryChange mode dt >>
+                   evacOneNodeOnly nl il inst gdx avail_nodes
 
 nodeEvacInstance _ _ _ (Instance.Instance
                         {Instance.diskTemplate = DTPlain}) _ _ =
@@ -889,15 +884,23 @@ 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 nl il mode inst@(Instance.Instance
+                                  {Instance.diskTemplate = dt@DTSharedFile})
+                 gdx avail_nodes =
+                   failOnSecondaryChange mode dt >>
+                   evacOneNodeOnly nl il inst gdx avail_nodes
 
-nodeEvacInstance _ _ mode (Instance.Instance
-                           {Instance.diskTemplate = dt@DTBlock}) _ _ =
-                  failOnSecondaryChange mode dt >>
-                  fail "Block device relocations not implemented yet"
+nodeEvacInstance nl il mode inst@(Instance.Instance
+                                  {Instance.diskTemplate = dt@DTBlock})
+                 gdx avail_nodes =
+                   failOnSecondaryChange mode dt >>
+                   evacOneNodeOnly nl il inst gdx avail_nodes
+
+nodeEvacInstance nl il mode inst@(Instance.Instance
+                                  {Instance.diskTemplate = dt@DTRbd})
+                 gdx avail_nodes =
+                   failOnSecondaryChange mode dt >>
+                   evacOneNodeOnly nl il inst gdx avail_nodes
 
 nodeEvacInstance nl il ChangePrimary
                  inst@(Instance.Instance {Instance.diskTemplate = DTDrbd8})
@@ -912,15 +915,7 @@ nodeEvacInstance nl il ChangePrimary
 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)
+  evacOneNodeOnly nl il inst gdx avail_nodes
 
 -- The algorithm for ChangeAll is as follows:
 --
@@ -961,46 +956,70 @@ nodeEvacInstance nl il ChangeAll
 
     return (nl', il', ops)
 
--- | Inner fold function for changing secondary of a DRBD instance.
+-- | Generic function for changing one node of an instance.
+--
+-- This is similar to 'nodeEvacInstance' but will be used in a few of
+-- its sub-patterns. It folds the inner function 'evacOneNodeInner'
+-- over the list of available nodes, which results in the best choice
+-- for relocation.
+evacOneNodeOnly :: Node.List         -- ^ The node list (cluster-wide)
+                -> Instance.List     -- ^ Instance list (cluster-wide)
+                -> 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])
+evacOneNodeOnly nl il inst gdx avail_nodes = do
+  op_fn <- case Instance.mirrorType inst of
+             MirrorNone -> Bad "Can't relocate/evacuate non-mirrored instances"
+             MirrorInternal -> Ok ReplaceSecondary
+             MirrorExternal -> Ok FailoverToAny
+  (nl', inst', _, ndx) <- annotateResult "Can't find any good node" $
+                          eitherToResult $
+                          foldl' (evacOneNodeInner nl inst gdx op_fn)
+                          (Left "no nodes available") avail_nodes
+  let idx = Instance.idx inst
+      il' = Container.add idx inst' il
+      ops = iMoveToJob nl' il' idx (op_fn ndx)
+  return (nl', il', ops)
+
+-- | Inner fold function for changing one node of an instance.
+--
+-- Depending on the instance disk template, this will either change
+-- the secondary (for DRBD) or the primary node (for shared
+-- storage). However, the operation is generic otherwise.
 --
 -- 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
+evacOneNodeInner :: Node.List         -- ^ Cluster node list
+                 -> Instance.Instance -- ^ Instance being evacuated
+                 -> Gdx               -- ^ The group index of the instance
+                 -> (Ndx -> IMove)    -- ^ Operation constructor
+                 -> EvacInnerState    -- ^ Current best solution
+                 -> Ndx               -- ^ Node we're evaluating as target
+                 -> EvacInnerState    -- ^ New best solution
+evacOneNodeInner nl inst gdx op_fn accu ndx =
+  case applyMove nl inst (op_fn ndx) of
+    OpFail fm -> let fail_msg = "Node " ++ Container.nameOf nl ndx ++
+                                " failed: " ++ show fm
+                 in either (const $ Left fail_msg) (const accu) accu
+    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.
 --
@@ -1019,48 +1038,47 @@ evacDrbdAllInner :: Node.List         -- ^ Cluster node list
                  -> (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)
+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.
@@ -1082,14 +1100,14 @@ updateEvacSolution :: (Node.List, Instance.List, EvacSolution)
                    -> 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})
+  (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)
+  (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
@@ -1099,24 +1117,24 @@ tryNodeEvac :: Group.List    -- ^ The cluster groups
             -> [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
-                            pdx = Instance.pNode inst in
-                        updateEvacSolution state (Instance.idx inst) $
-                        availableGroupNodes group_ndx
-                          (IntSet.insert pdx 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)
+  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
+                      pdx = Instance.pNode inst in
+                  updateEvacSolution state (Instance.idx inst) $
+                  availableGroupNodes group_ndx
+                    (IntSet.insert pdx 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.
 --
@@ -1145,84 +1163,74 @@ tryChangeGroup :: Group.List    -- ^ The cluster groups
                -> [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
+  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
+      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)
+
+-- | Standard-sized allocation method.
+--
+-- This places instances of the same size on the cluster until we're
+-- out of space. The result will be a list of identically-sized
+-- instances.
+iterateAlloc :: AllocMethod
 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
+  let depth = length ixes
+      newname = printf "new-%d" depth::String
+      newidx = Container.size il
+      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, asSolution = sols3 }) ->
+         let newsol = Ok (collapseFailures errs, nl, il, ixes, cstats) in
+         case sols3 of
+           Nothing -> newsol
+           Just (xnl, xi, _, _) ->
+             if limit == Just 0
+               then newsol
+               else iterateAlloc xnl (Container.add newidx xi il)
+                      newlimit newinst allocnodes (xi:ixes)
+                      (totalResources xnl:cstats)
+
+-- | Tiered allocation method.
+--
+-- This places instances on the cluster, and decreases the spec until
+-- we can allocate again. The result will be a list of decreasing
+-- instance specs.
+tieredAlloc :: AllocMethod
 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 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
@@ -1243,15 +1251,17 @@ computeMoves :: Instance.Instance -- ^ The instance to be moved
                 -- 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
+  case mv of
+    Failover -> ("f", [mig])
+    FailoverToAny _ -> (printf "fa:%s" c, [mig_any])
+    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.isRunning i then "migrate" else "failover"
+        mig = printf "%s -f %s" morf inam::String
+        mig_any = printf "%s -f -n %s %s" morf c inam::String
+        rep n = printf "replace-disks -n %s %s" n inam::String
 
 -- | Converts a placement to string format.
 printSolutionLine :: Node.List     -- ^ The node list
@@ -1263,23 +1273,26 @@ printSolutionLine :: Node.List     -- ^ The node list
                                    -- 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)
+  let pmlen = (2*nmlen + 1)
+      (i, p, s, mv, c) = plc
+      old_sec = Instance.sNode inst
+      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 old_sec nl
+      (moves, cmds) =  computeMoves inst inam mv npri nsec
+      -- FIXME: this should check instead/also the disk template
+      ostr = if old_sec == Node.noSecondary
+               then printf "%s" opri::String
+               else printf "%s:%s" opri osec::String
+      nstr = if s == Node.noSecondary
+               then printf "%s" npri::String
+               else printf "%s:%s" npri nsec::String
+  in (printf "  %3d. %-*s %-*s => %-*s %12.8f a=%s"
+      pos imlen inam pmlen ostr pmlen nstr c moves,
+      cmds)
 
 -- | Return the instance and involved nodes in an instance move.
 --
@@ -1295,17 +1308,17 @@ involvedNodes :: Instance.List -- ^ Instance list, used for retrieving
                                -- 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
+  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)
+  | 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.
@@ -1316,11 +1329,11 @@ splitJobs = fst . foldl mergeJobs ([], [])
 -- 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
+  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
 
@@ -1328,59 +1341,61 @@ formatJob jsn jsl (sn, (_, _, _, cmds)) =
 -- also beautify the display a little.
 formatCmds :: [JobSet] -> String
 formatCmds =
-    unlines .
-    concatMap (\(jsn, js) -> concatMap (formatJob jsn (length js))
-                             (zip [1..] js)) .
-    zip [1..]
+  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
+  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 printTable "" 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
+  let sil = sortBy (comparing Instance.idx) (Container.elems il)
+      helper inst = [ if Instance.isRunning 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
+                    , 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 printTable "" 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
+printStats :: String -> Node.List -> String
+printStats lp nl =
+  let dcvs = compDetailedCV $ Container.elems nl
+      (weights, names) = unzip detailedCVInfo
+      hd = zip3 (weights ++ repeat 1) (names ++ repeat "unknown") dcvs
+      header = [ "Field", "Value", "Weight" ]
+      formatted = map (\(w, h, val) ->
+                         [ h
+                         , printf "%.8f" val
+                         , printf "x%.2f" w
+                         ]) hd
+  in printTable lp header formatted $ False:repeat True
 
 -- | Convert a placement into a list of OpCodes (basically a job).
 iMoveToJob :: Node.List        -- ^ The node list; only used for node
@@ -1394,18 +1409,20 @@ iMoveToJob :: Node.List        -- ^ The node list; only used for node
            -> [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 ]
+  let inst = Container.find idx il
+      iname = Instance.name inst
+      lookNode  = Just . Container.nameOf nl
+      opF = OpCodes.OpInstanceMigrate iname True False True Nothing
+      opFA n = OpCodes.OpInstanceMigrate iname True False True (lookNode n)
+      opR n = OpCodes.OpInstanceReplaceDisks iname (lookNode n)
+              OpCodes.ReplaceNewSecondary [] Nothing
+  in case move of
+       Failover -> [ opF ]
+       FailoverToAny np -> [ opFA np ]
+       ReplacePrimary np -> [ opF, opR np, opF ]
+       ReplaceSecondary ns -> [ opR ns ]
+       ReplaceAndFailover np -> [ opR np, opF ]
+       FailoverAndReplace ns -> [ opF, opR ns ]
 
 -- * Node group functions
 
@@ -1420,9 +1437,9 @@ instanceGroup nl i =
       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
+       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
@@ -1454,17 +1471,17 @@ nodesToEvacuate :: Instance.List -- ^ The cluster-wide instance list
                 -> [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
+  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
index 36a0fbf..3f1cebb 100644 (file)
@@ -2,7 +2,8 @@
 
 {- | Compatibility helper module.
 
-This module holds definitions that help with supporting multiple library versions or transitions between versions.
+This module holds definitions that help with supporting multiple
+library versions or transitions between versions.
 
 -}
 
@@ -28,9 +29,9 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 -}
 
 module Ganeti.HTools.Compat
-    ( rwhnf
-    , Control.Parallel.Strategies.parMap
-    ) where
+  ( rwhnf
+  , Control.Parallel.Strategies.parMap
+  ) where
 
 import qualified Control.Parallel.Strategies
 
index 5b2d3cc..ec8a11c 100644 (file)
@@ -27,33 +27,32 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 -}
 
 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
+  ( -- * 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
 
@@ -86,8 +85,8 @@ nameOf c k = T.nameOf $ find k c
 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
+  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
index 0b63a2c..bd258f5 100644 (file)
@@ -8,7 +8,7 @@ libraries implementing the low-level protocols.
 
 {-
 
-Copyright (C) 2009, 2010, 2011 Google Inc.
+Copyright (C) 2009, 2010, 2011, 2012 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
@@ -28,47 +28,49 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 -}
 
 module Ganeti.HTools.ExtLoader
-    ( loadExternalData
-    , commonSuffix
-    , maybeSaveData
-    ) where
+  ( loadExternalData
+  , commonSuffix
+  , maybeSaveData
+  ) where
 
 import Control.Monad
+import Control.Exception
 import Data.Maybe (isJust, fromJust)
+import Prelude hiding (catch)
 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 qualified Ganeti.HTools.IAlloc as IAlloc
 import Ganeti.HTools.Loader (mergeData, checkData, ClusterData(..)
                             , commonSuffix)
 
 import Ganeti.HTools.Types
 import Ganeti.HTools.CLI
-import Ganeti.HTools.Utils (sepSplit, tryRead)
+import Ganeti.HTools.Utils (sepSplit, tryRead, exitIfBad, exitWhen)
 
 -- | Error beautifier.
 wrapIO :: IO (Result a) -> IO (Result a)
-wrapIO = flip catch (return . Bad . show)
+wrapIO = flip catch (\e -> return . Bad . show $ (e::IOException))
 
 -- | 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
+  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
@@ -78,10 +80,12 @@ loadExternalData opts = do
       lsock = optLuxi opts
       tfile = optDataFile opts
       simdata = optNodeSim opts
+      iallocsrc = optIAllocSrc opts
       setRapi = mhost /= ""
       setLuxi = isJust lsock
       setSim = (not . null) simdata
       setFile = isJust tfile
+      setIAllocSrc = isJust iallocsrc
       allSet = filter id [setRapi, setLuxi, setFile]
       exTags = case optExTags opts of
                  Nothing -> []
@@ -89,39 +93,23 @@ loadExternalData opts = do
       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)
+  exitWhen (length allSet > 1) "Only one of the rapi, luxi, and data\
+                               \ files options should be given"
+
+  util_contents <- maybe (return "") readFile (optDynuFile opts)
+  util_data <- exitIfBad "can't parse utilisation data" .
+               mapM parseUtilisation $ lines util_contents
   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
-      )
+    case () of
+      _ | setRapi -> wrapIO $ Rapi.loadData mhost
+        | setLuxi -> wrapIO $ Luxi.loadData $ fromJust lsock
+        | setSim -> Simu.loadData simdata
+        | setFile -> wrapIO $ Text.loadData $ fromJust tfile
+        | setIAllocSrc -> wrapIO $ IAlloc.loadData $ fromJust iallocsrc
+        | otherwise -> return $ Bad "No backend selected! Exiting."
+
+  let ldresult = input_data >>= mergeData util_data exTags selInsts exInsts
+  cdata <- exitIfBad "failed to load data, aborting" ldresult
   let (fix_msgs, nl) = checkData (cdNodes cdata) (cdInstances cdata)
 
   unless (optVerbose opts == 0) $ maybeShowWarnings fix_msgs
index 6df5f4c..8eb7e4f 100644 (file)
@@ -24,14 +24,14 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 -}
 
 module Ganeti.HTools.Group
-    ( Group(..)
-    , List
-    , AssocList
-    -- * Constructor
-    , create
-    , setIdx
-    , isAllocable
-    ) where
+  ( Group(..)
+  , List
+  , AssocList
+  -- * Constructor
+  , create
+  , setIdx
+  , isAllocable
+  ) where
 
 import qualified Ganeti.HTools.Container as Container
 
@@ -41,20 +41,21 @@ import qualified Ganeti.HTools.Types as T
 
 -- | 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)
+  { 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
+  , iPolicy     :: T.IPolicy     -- ^ The instance 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]
+  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)]
@@ -65,13 +66,14 @@ 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
-          }
+create :: String -> T.GroupID -> T.AllocPolicy -> T.IPolicy -> Group
+create name_init id_init apol_init ipol_init =
+  Group { name        = name_init
+        , uuid        = id_init
+        , allocPolicy = apol_init
+        , iPolicy     = ipol_init
+        , idx         = -1
+        }
 
 -- | Sets the group index.
 --
index 91b3706..3142755 100644 (file)
@@ -4,7 +4,7 @@
 
 {-
 
-Copyright (C) 2009, 2010, 2011 Google Inc.
+Copyright (C) 2009, 2010, 2011, 2012 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
@@ -24,17 +24,19 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 -}
 
 module Ganeti.HTools.IAlloc
-    ( readRequest
-    , runIAllocator
-    ) where
+  ( readRequest
+  , runIAllocator
+  , processRelocate
+  , loadData
+  ) where
 
 import Data.Either ()
-import Data.Maybe (fromMaybe, isJust)
+import Data.Maybe (fromMaybe)
 import Data.List
 import Control.Monad
 import Text.JSON (JSObject, JSValue(JSArray),
                   makeObj, encodeStrict, decodeStrict, fromJSObject, showJSON)
-import System (exitWith, ExitCode(..))
+import System.Exit
 import System.IO
 
 import qualified Ganeti.HTools.Cluster as Cluster
@@ -45,10 +47,11 @@ 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.JSON
 import Ganeti.HTools.Types
 
+{-# ANN module "HLint: ignore Eta reduce" #-}
+
 -- | Type alias for the result of an IAllocator call.
 type IAllocResult = (String, JSValue, Node.List, Instance.List)
 
@@ -67,8 +70,8 @@ parseBaseInstance n a = do
   vcpus <- extract "vcpus"
   tags  <- extract "tags"
   dt    <- extract "disk_template"
-  let running = "running"
-  return (n, Instance.create n mem disk vcpus running tags True 0 0 dt)
+  su    <- extract "spindle_use"
+  return (n, Instance.create n mem disk vcpus Running tags True 0 0 dt su)
 
 -- | Parses an instance as found in the cluster instance list.
 parseInstance :: NameAssoc -- ^ The node name-to-index association list
@@ -83,8 +86,9 @@ parseInstance ktn n a = do
            else readEitherString $ head nodes
   pidx <- lookupNode ktn n pnode
   let snodes = tail nodes
-  sidx <- (if null snodes then return Node.noSecondary
-           else readEitherString (head snodes) >>= lookupNode ktn n)
+  sidx <- if null snodes
+            then return Node.noSecondary
+            else readEitherString (head snodes) >>= lookupNode ktn n
   return (n, Instance.setBoth (snd base) pidx sidx)
 
 -- | Parses a node as found in the cluster node list.
@@ -101,17 +105,20 @@ parseNode ktg n a = do
   vm_capable  <- annotateResult desc $ maybeFromObj a "vm_capable"
   let vm_capable' = fromMaybe True vm_capable
   gidx <- lookupGroup ktg n guuid
-  node <- (if offline || drained || not vm_capable'
-           then return $ Node.create n 0 0 0 0 0 0 True gidx
-           else do
-             mtotal <- extract "total_memory"
-             mnode  <- extract "reserved_memory"
-             mfree  <- extract "free_memory"
-             dtotal <- extract "total_disk"
-             dfree  <- extract "free_disk"
-             ctotal <- extract "total_cpus"
-             return $ Node.create n mtotal mnode mfree
-                    dtotal dfree ctotal False gidx)
+  node <- if offline || drained || not vm_capable'
+            then return $ Node.create n 0 0 0 0 0 0 True 0 gidx
+            else do
+              mtotal <- extract "total_memory"
+              mnode  <- extract "reserved_memory"
+              mfree  <- extract "free_memory"
+              dtotal <- extract "total_disk"
+              dfree  <- extract "free_disk"
+              ctotal <- extract "total_cpus"
+              ndparams <- extract "ndparams" >>= asJSObject
+              spindles <- tryFromObj desc (fromJSObject ndparams)
+                          "spindle_count"
+              return $ Node.create n mtotal mnode mfree
+                     dtotal dfree ctotal False spindles gidx
   return (n, node)
 
 -- | Parses a group as found in the cluster group list.
@@ -122,7 +129,8 @@ parseGroup u a = do
   let extract x = tryFromObj ("invalid data for group '" ++ u ++ "'") a x
   name <- extract "name"
   apol <- extract "alloc_policy"
-  return (u, Group.create name u apol)
+  ipol <- extract "ipolicy"
+  return (u, Group.create name u apol ipol)
 
 -- | Top-level parser.
 --
@@ -155,7 +163,7 @@ parseData body = do
   let (kti, il) = assignIndices iobj
   -- cluster tags
   ctags <- extrObj "cluster_tags"
-  cdata1 <- mergeData [] [] [] [] (ClusterData gl nl il ctags)
+  cdata1 <- mergeData [] [] [] [] (ClusterData gl nl il ctags defIPolicy)
   let (msgs, fix_nl) = checkData (cdNodes cdata1) (cdInstances cdata1)
       cdata = cdata1 { cdNodes = fix_nl }
       map_n = cdNodes cdata
@@ -163,40 +171,40 @@ parseData body = do
       map_g = cdGroups cdata
   optype <- extrReq "type"
   rqtype <-
-      case () of
-        _ | optype == C.iallocatorModeAlloc ->
-              do
-                rname     <- extrReq "name"
-                req_nodes <- extrReq "required_nodes"
-                inew      <- parseBaseInstance rname request
-                let io = snd inew
-                return $ Allocate io req_nodes
-          | optype == C.iallocatorModeReloc ->
-              do
-                rname     <- extrReq "name"
-                ridx      <- lookupInstance kti rname
-                req_nodes <- extrReq "required_nodes"
-                ex_nodes  <- extrReq "relocate_from"
-                ex_idex   <- mapM (Container.findByName map_n) ex_nodes
-                return $ Relocate ridx req_nodes (map Node.idx ex_idex)
-          | optype == C.iallocatorModeChgGroup ->
-              do
-                rl_names <- extrReq "instances"
-                rl_insts <- mapM (liftM Instance.idx .
-                                  Container.findByName map_i) rl_names
-                gr_uuids <- extrReq "target_groups"
-                gr_idxes <- mapM (liftM Group.idx .
-                                  Container.findByName map_g) gr_uuids
-                return $ ChangeGroup rl_insts gr_idxes
-          | optype == C.iallocatorModeNodeEvac ->
-              do
-                rl_names <- extrReq "instances"
-                rl_insts <- mapM (Container.findByName map_i) rl_names
-                let rl_idx = map Instance.idx rl_insts
-                rl_mode <- extrReq "evac_mode"
-                return $ NodeEvacuate rl_idx rl_mode
+    case () of
+      _ | optype == C.iallocatorModeAlloc ->
+            do
+              rname     <- extrReq "name"
+              req_nodes <- extrReq "required_nodes"
+              inew      <- parseBaseInstance rname request
+              let io = snd inew
+              return $ Allocate io req_nodes
+        | optype == C.iallocatorModeReloc ->
+            do
+              rname     <- extrReq "name"
+              ridx      <- lookupInstance kti rname
+              req_nodes <- extrReq "required_nodes"
+              ex_nodes  <- extrReq "relocate_from"
+              ex_idex   <- mapM (Container.findByName map_n) ex_nodes
+              return $ Relocate ridx req_nodes (map Node.idx ex_idex)
+        | optype == C.iallocatorModeChgGroup ->
+            do
+              rl_names <- extrReq "instances"
+              rl_insts <- mapM (liftM Instance.idx .
+                                Container.findByName map_i) rl_names
+              gr_uuids <- extrReq "target_groups"
+              gr_idxes <- mapM (liftM Group.idx .
+                                Container.findByName map_g) gr_uuids
+              return $ ChangeGroup rl_insts gr_idxes
+        | optype == C.iallocatorModeNodeEvac ->
+            do
+              rl_names <- extrReq "instances"
+              rl_insts <- mapM (Container.findByName map_i) rl_names
+              let rl_idx = map Instance.idx rl_insts
+              rl_mode <- extrReq "evac_mode"
+              return $ NodeEvacuate rl_idx rl_mode
 
-          | otherwise -> fail ("Invalid request type '" ++ optype ++ "'")
+        | otherwise -> fail ("Invalid request type '" ++ optype ++ "'")
   return (msgs, Request rqtype cdata)
 
 -- | Formats the result into a valid IAllocator response message.
@@ -205,11 +213,10 @@ formatResponse :: Bool     -- ^ Whether the request was successful
                -> JSValue  -- ^ The JSON encoded result
                -> String   -- ^ The full JSON-formatted message
 formatResponse success info result =
-    let
-        e_success = ("success", showJSON success)
-        e_info = ("info", showJSON info)
-        e_result = ("result", result)
-    in encodeStrict $ makeObj [e_success, e_info, e_result]
+  let e_success = ("success", showJSON success)
+      e_info = ("info", showJSON info)
+      e_result = ("result", result)
+  in encodeStrict $ makeObj [e_success, e_info, e_result]
 
 -- | Flatten the log of a solution into a string.
 describeSolution :: Cluster.AllocSolution -> String
@@ -219,13 +226,12 @@ describeSolution = intercalate ", " . Cluster.asLog
 formatAllocate :: Instance.List -> Cluster.AllocSolution -> Result IAllocResult
 formatAllocate il as = do
   let info = describeSolution as
-  case Cluster.asSolutions as of
-    [] -> fail info
-    (nl, inst, nodes, _):[] ->
-        do
-          let il' = Container.add (Instance.idx inst) inst il
-          return (info, showJSON $ map Node.name nodes, nl, il')
-    _ -> fail "Internal error: multiple allocation solutions"
+  case Cluster.asSolution as of
+    Nothing -> fail info
+    Just (nl, inst, nodes, _) ->
+      do
+        let il' = Container.add (Instance.idx inst) inst il
+        return (info, showJSON $ map Node.name nodes, nl, il')
 
 -- | Convert a node-evacuation/change group result.
 formatNodeEvac :: Group.List
@@ -234,17 +240,17 @@ formatNodeEvac :: Group.List
                -> (Node.List, Instance.List, Cluster.EvacSolution)
                -> Result IAllocResult
 formatNodeEvac gl nl il (fin_nl, fin_il, es) =
-    let iname = Instance.name . flip Container.find il
-        nname = Node.name . flip Container.find nl
-        gname = Group.name . flip Container.find gl
-        fes = map (\(idx, msg) -> (iname idx, msg)) $ Cluster.esFailed es
-        mes = map (\(idx, gdx, ndxs) -> (iname idx, gname gdx, map nname ndxs))
-              $ Cluster.esMoved es
-        failed = length fes
-        moved  = length mes
-        info = show failed ++ " instances failed to move and " ++ show moved ++
-               " were moved successfully"
-    in Ok (info, showJSON (mes, fes, Cluster.esOpCodes es), fin_nl, fin_il)
+  let iname = Instance.name . flip Container.find il
+      nname = Node.name . flip Container.find nl
+      gname = Group.name . flip Container.find gl
+      fes = map (\(idx, msg) -> (iname idx, msg)) $ Cluster.esFailed es
+      mes = map (\(idx, gdx, ndxs) -> (iname idx, gname gdx, map nname ndxs))
+            $ Cluster.esMoved es
+      failed = length fes
+      moved  = length mes
+      info = show failed ++ " instances failed to move and " ++ show moved ++
+             " were moved successfully"
+  in Ok (info, showJSON (mes, fes, Cluster.esOpCodes es), fin_nl, fin_il)
 
 -- | Runs relocate for a single instance.
 --
@@ -263,13 +269,20 @@ processRelocate :: Group.List      -- ^ The group list
 processRelocate gl nl il idx 1 exndx = do
   let orig = Container.find idx il
       sorig = Instance.sNode orig
-  when (exndx /= [sorig]) $
+      porig = Instance.pNode orig
+      mir_type = Instance.mirrorType orig
+  (exp_node, node_type, reloc_type) <-
+    case mir_type of
+      MirrorNone -> fail "Can't relocate non-mirrored instances"
+      MirrorInternal -> return (sorig, "secondary", ChangeSecondary)
+      MirrorExternal -> return (porig, "primary", ChangePrimary)
+  when (exndx /= [exp_node]) $
        -- FIXME: we can't use the excluded nodes here; the logic is
        -- already _but only partially_ implemented in tryNodeEvac...
        fail $ "Unsupported request: excluded nodes not equal to\
-              \ instance's secondary node (" ++ show sorig ++ " versus " ++
-              show exndx ++ ")"
-  (nl', il', esol) <- Cluster.tryNodeEvac gl nl il ChangeSecondary [idx]
+              \ instance's " ++  node_type ++ "(" ++ show exp_node
+              ++ " versus " ++ show exndx ++ ")"
+  (nl', il', esol) <- Cluster.tryNodeEvac gl nl il reloc_type [idx]
   nodes <- case lookup idx (Cluster.esFailed esol) of
              Just msg -> fail msg
              Nothing ->
@@ -281,16 +294,28 @@ processRelocate gl nl il idx 1 exndx = do
   let inst = Container.find idx il'
       pnode = Instance.pNode inst
       snode = Instance.sNode inst
-  when (snode == sorig) $
-       fail "Internal error: instance didn't change secondary node?!"
-  when (snode == pnode) $
-       fail "Internal error: selected primary as new secondary?!"
-
-  nodes' <- if (nodes == [pnode, snode])
+  nodes' <-
+    case mir_type of
+      MirrorNone -> fail "Internal error: mirror type none after relocation?!"
+      MirrorInternal ->
+        do
+          when (snode == sorig) $
+               fail "Internal error: instance didn't change secondary node?!"
+          when (snode == pnode) $
+               fail "Internal error: selected primary as new secondary?!"
+          if nodes == [pnode, snode]
             then return [snode] -- only the new secondary is needed
             else fail $ "Internal error: inconsistent node list (" ++
                  show nodes ++ ") versus instance nodes (" ++ show pnode ++
                  "," ++ show snode ++ ")"
+      MirrorExternal ->
+        do
+          when (pnode == porig) $
+               fail "Internal error: instance didn't change primary node?!"
+          if nodes == [pnode]
+            then return nodes
+            else fail $ "Internal error: inconsistent node list (" ++
+                 show nodes ++ ") versus instance node (" ++ show pnode ++ ")"
   return (nl', il', nodes')
 
 processRelocate _ _ _ _ reqn _ =
@@ -299,53 +324,52 @@ processRelocate _ _ _ _ reqn _ =
 formatRelocate :: (Node.List, Instance.List, [Ndx])
                -> Result IAllocResult
 formatRelocate (nl, il, ndxs) =
-    let nodes = map (`Container.find` nl) ndxs
-        names = map Node.name nodes
-    in Ok ("success", showJSON names, nl, il)
+  let nodes = map (`Container.find` nl) ndxs
+      names = map Node.name nodes
+  in Ok ("success", showJSON names, nl, il)
 
 -- | Process a request and return new node lists.
 processRequest :: Request -> Result IAllocResult
 processRequest request =
-  let Request rqtype (ClusterData gl nl il _) = request
+  let Request rqtype (ClusterData gl nl il _ _) = request
   in case rqtype of
        Allocate xi reqn ->
-           Cluster.tryMGAlloc gl nl il xi reqn >>= formatAllocate il
+         Cluster.tryMGAlloc gl nl il xi reqn >>= formatAllocate il
        Relocate idx reqn exnodes ->
-           processRelocate gl nl il idx reqn exnodes >>= formatRelocate
+         processRelocate gl nl il idx reqn exnodes >>= formatRelocate
        ChangeGroup gdxs idxs ->
-           Cluster.tryChangeGroup gl nl il idxs gdxs >>=
-                  formatNodeEvac gl nl il
+         Cluster.tryChangeGroup gl nl il idxs gdxs >>=
+                formatNodeEvac gl nl il
        NodeEvacuate xi mode ->
-           Cluster.tryNodeEvac gl nl il mode xi >>=
-                  formatNodeEvac gl nl il
+         Cluster.tryNodeEvac gl nl il mode xi >>=
+                formatNodeEvac gl nl il
 
 -- | Reads the request from the data file(s).
-readRequest :: Options -> [String] -> IO Request
-readRequest opts args = do
-  when (null args) $ do
-         hPutStrLn stderr "Error: this program needs an input file."
-         exitWith $ ExitFailure 1
-
-  input_data <- readFile (head args)
-  r1 <- case parseData input_data of
-          Bad err -> do
-            hPutStrLn stderr $ "Error: " ++ err
-            exitWith $ ExitFailure 1
-          Ok (fix_msgs, rq) -> maybeShowWarnings fix_msgs >> return rq
-  (if isJust (optDataFile opts) ||  (not . null . optNodeSim) opts
-   then do
-     cdata <- loadExternalData opts
-     let Request rqt _ = r1
-     return $ Request rqt cdata
-   else return r1)
+readRequest :: FilePath -> IO Request
+readRequest fp = do
+  input_data <- case fp of
+                  "-" -> getContents
+                  _   -> readFile fp
+  case parseData input_data of
+    Bad err -> do
+      hPutStrLn stderr $ "Error: " ++ err
+      exitWith $ ExitFailure 1
+    Ok (fix_msgs, rq) -> maybeShowWarnings fix_msgs >> return rq
 
 -- | Main iallocator pipeline.
 runIAllocator :: Request -> (Maybe (Node.List, Instance.List), String)
 runIAllocator request =
   let (ok, info, result, cdata) =
-          case processRequest request of
-            Ok (msg, r, nl, il) -> (True, "Request successful: " ++ msg, r,
-                                    Just (nl, il))
-            Bad msg -> (False, "Request failed: " ++ msg, JSArray [], Nothing)
+        case processRequest request of
+          Ok (msg, r, nl, il) -> (True, "Request successful: " ++ msg, r,
+                                  Just (nl, il))
+          Bad msg -> (False, "Request failed: " ++ msg, JSArray [], Nothing)
       rstring = formatResponse ok info result
   in (cdata, rstring)
+
+-- | Load the data from an iallocation request file
+loadData :: FilePath -- ^ The path to the file
+         -> IO (Result ClusterData)
+loadData fp = do
+  Request _ cdata <- readRequest fp
+  return $ Ok cdata
index a6debe8..76465ce 100644 (file)
@@ -7,7 +7,7 @@ intelligence is in the "Node" and "Cluster" modules.
 
 {-
 
-Copyright (C) 2009, 2010, 2011 Google Inc.
+Copyright (C) 2009, 2010, 2011, 2012 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
@@ -27,62 +27,100 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 -}
 
 module Ganeti.HTools.Instance
-    ( Instance(..)
-    , AssocList
-    , List
-    , create
-    , setIdx
-    , setName
-    , setAlias
-    , setPri
-    , setSec
-    , setBoth
-    , setMovable
-    , specOf
-    , shrinkByType
-    , runningStates
-    , localStorageTemplates
-    , hasSecondary
-    , requiredNodes
-    , allNodes
-    , usesLocalStorage
-    ) where
+  ( Instance(..)
+  , AssocList
+  , List
+  , create
+  , isRunning
+  , isOffline
+  , notOffline
+  , instanceDown
+  , usesSecMem
+  , applyIfOnline
+  , setIdx
+  , setName
+  , setAlias
+  , setPri
+  , setSec
+  , setBoth
+  , setMovable
+  , specOf
+  , instBelowISpec
+  , instAboveISpec
+  , instMatchesPolicy
+  , shrinkByType
+  , localStorageTemplates
+  , hasSecondary
+  , requiredNodes
+  , allNodes
+  , usesLocalStorage
+  , mirrorType
+  ) where
 
 import qualified Ganeti.HTools.Types as T
 import qualified Ganeti.HTools.Container as Container
-import qualified Ganeti.Constants as C
+
+import Ganeti.HTools.Utils
 
 -- * Type declarations
 
 -- | The instance type.
 data Instance = Instance
-    { name         :: String    -- ^ The instance name
-    , alias        :: String    -- ^ The shortened name
-    , mem          :: Int       -- ^ Memory of the instance
-    , dsk          :: Int       -- ^ Disk size of instance
-    , vcpus        :: Int       -- ^ Number of VCPUs
-    , running      :: Bool      -- ^ Is the instance running?
-    , runSt        :: String    -- ^ Original (text) run status
-    , pNode        :: T.Ndx     -- ^ Original primary node
-    , sNode        :: T.Ndx     -- ^ Original secondary node
-    , idx          :: T.Idx     -- ^ Internal index
-    , util         :: T.DynUtil -- ^ Dynamic resource usage
-    , movable      :: Bool      -- ^ Can and should the instance be moved?
-    , autoBalance  :: Bool      -- ^ Is the instance auto-balanced?
-    , tags         :: [String]  -- ^ List of instance tags
-    , diskTemplate :: T.DiskTemplate -- ^ The disk template of the instance
-    } deriving (Show, Read)
+  { name         :: String    -- ^ The instance name
+  , alias        :: String    -- ^ The shortened name
+  , mem          :: Int       -- ^ Memory of the instance
+  , dsk          :: Int       -- ^ Disk size of instance
+  , vcpus        :: Int       -- ^ Number of VCPUs
+  , runSt        :: T.InstanceStatus -- ^ Original run status
+  , pNode        :: T.Ndx     -- ^ Original primary node
+  , sNode        :: T.Ndx     -- ^ Original secondary node
+  , idx          :: T.Idx     -- ^ Internal index
+  , util         :: T.DynUtil -- ^ Dynamic resource usage
+  , movable      :: Bool      -- ^ Can and should the instance be moved?
+  , autoBalance  :: Bool      -- ^ Is the instance auto-balanced?
+  , tags         :: [String]  -- ^ List of instance tags
+  , diskTemplate :: T.DiskTemplate -- ^ The disk template of the instance
+  , spindleUse   :: Int       -- ^ The numbers of used spindles
+  } deriving (Show, Read, Eq)
 
 instance T.Element Instance where
-    nameOf   = name
-    idxOf    = idx
-    setAlias = setAlias
-    setIdx   = setIdx
-    allNames n = [name n, alias n]
-
--- | Constant holding the running instance states.
-runningStates :: [String]
-runningStates = [C.inststRunning, C.inststErrorup]
+  nameOf   = name
+  idxOf    = idx
+  setAlias = setAlias
+  setIdx   = setIdx
+  allNames n = [name n, alias n]
+
+-- | Check if instance is running.
+isRunning :: Instance -> Bool
+isRunning (Instance {runSt = T.Running}) = True
+isRunning (Instance {runSt = T.ErrorUp}) = True
+isRunning _                              = False
+
+-- | Check if instance is offline.
+isOffline :: Instance -> Bool
+isOffline (Instance {runSt = T.AdminOffline}) = True
+isOffline _                                   = False
+
+
+-- | Helper to check if the instance is not offline.
+notOffline :: Instance -> Bool
+notOffline = not . isOffline
+
+-- | Check if instance is down.
+instanceDown :: Instance -> Bool
+instanceDown inst | isRunning inst = False
+instanceDown inst | isOffline inst = False
+instanceDown _                     = True
+
+-- | Apply the function if the instance is online. Otherwise use
+-- the initial value
+applyIfOnline :: Instance -> (a -> a) -> a -> a
+applyIfOnline = applyIf . notOffline
+
+-- | Helper for determining whether an instance's memory needs to be
+-- taken into account for secondary memory reservation.
+usesSecMem :: Instance -> Bool
+usesSecMem inst = notOffline inst && autoBalance inst
 
 -- | Constant holding the local storage templates.
 --
@@ -101,7 +139,12 @@ localStorageTemplates = [ T.DTDrbd8, T.DTPlain ]
 -- instance. Further the movable state can be restricted more due to
 -- user choices, etc.
 movableDiskTemplates :: [T.DiskTemplate]
-movableDiskTemplates = [ T.DTDrbd8, T.DTBlock, T.DTSharedFile ]
+movableDiskTemplates =
+  [ T.DTDrbd8
+  , T.DTBlock
+  , T.DTSharedFile
+  , T.DTRbd
+  ]
 
 -- | A simple name for the int, instance association list.
 type AssocList = [(T.Idx, Instance)]
@@ -115,26 +158,27 @@ type List = Container.Container Instance
 --
 -- Some parameters are not initialized by function, and must be set
 -- later (via 'setIdx' for example).
-create :: String -> Int -> Int -> Int -> String
-       -> [String] -> Bool -> T.Ndx -> T.Ndx -> T.DiskTemplate -> Instance
+create :: String -> Int -> Int -> Int -> T.InstanceStatus
+       -> [String] -> Bool -> T.Ndx -> T.Ndx -> T.DiskTemplate -> Int
+       -> Instance
 create name_init mem_init dsk_init vcpus_init run_init tags_init
-       auto_balance_init pn sn dt =
-    Instance { name = name_init
-             , alias = name_init
-             , mem = mem_init
-             , dsk = dsk_init
-             , vcpus = vcpus_init
-             , running = run_init `elem` runningStates
-             , runSt = run_init
-             , pNode = pn
-             , sNode = sn
-             , idx = -1
-             , util = T.baseUtil
-             , tags = tags_init
-             , movable = supportsMoves dt
-             , autoBalance = auto_balance_init
-             , diskTemplate = dt
-             }
+       auto_balance_init pn sn dt su =
+  Instance { name = name_init
+           , alias = name_init
+           , mem = mem_init
+           , dsk = dsk_init
+           , vcpus = vcpus_init
+           , runSt = run_init
+           , pNode = pn
+           , sNode = sn
+           , idx = -1
+           , util = T.baseUtil
+           , tags = tags_init
+           , movable = supportsMoves dt
+           , autoBalance = auto_balance_init
+           , diskTemplate = dt
+           , spindleUse = su
+           }
 
 -- | Changes the index.
 --
@@ -207,7 +251,34 @@ shrinkByType _ f = T.Bad $ "Unhandled failure mode " ++ show f
 -- | Return the spec of an instance.
 specOf :: Instance -> T.RSpec
 specOf Instance { mem = m, dsk = d, vcpus = c } =
-    T.RSpec { T.rspecCpu = c, T.rspecMem = m, T.rspecDsk = d }
+  T.RSpec { T.rspecCpu = c, T.rspecMem = m, T.rspecDsk = d }
+
+-- | Checks if an instance is smaller than a given spec. Returns
+-- OpGood for a correct spec, otherwise OpFail one of the possible
+-- failure modes.
+instBelowISpec :: Instance -> T.ISpec -> T.OpResult ()
+instBelowISpec inst ispec
+  | mem inst > T.iSpecMemorySize ispec = T.OpFail T.FailMem
+  | dsk inst > T.iSpecDiskSize ispec   = T.OpFail T.FailDisk
+  | vcpus inst > T.iSpecCpuCount ispec = T.OpFail T.FailCPU
+  | otherwise = T.OpGood ()
+
+-- | Checks if an instance is bigger than a given spec.
+instAboveISpec :: Instance -> T.ISpec -> T.OpResult ()
+instAboveISpec inst ispec
+  | mem inst < T.iSpecMemorySize ispec = T.OpFail T.FailMem
+  | dsk inst < T.iSpecDiskSize ispec   = T.OpFail T.FailDisk
+  | vcpus inst < T.iSpecCpuCount ispec = T.OpFail T.FailCPU
+  | otherwise = T.OpGood ()
+
+-- | Checks if an instance matches a policy.
+instMatchesPolicy :: Instance -> T.IPolicy -> T.OpResult ()
+instMatchesPolicy inst ipol = do
+  instAboveISpec inst (T.iPolicyMinSpec ipol)
+  instBelowISpec inst (T.iPolicyMaxSpec ipol)
+  if (diskTemplate inst `elem` T.iPolicyDiskTemplates ipol)
+    then T.OpGood ()
+    else T.OpFail T.FailDisk
 
 -- | Checks whether the instance uses a secondary node.
 --
@@ -234,3 +305,7 @@ usesLocalStorage = (`elem` localStorageTemplates) . diskTemplate
 -- | Checks whether a given disk template supported moves.
 supportsMoves :: T.DiskTemplate -> Bool
 supportsMoves = (`elem` movableDiskTemplates)
+
+-- | A simple wrapper over 'T.templateMirrorType'.
+mirrorType :: Instance -> T.MirrorType
+mirrorType = T.templateMirrorType . diskTemplate
diff --git a/htools/Ganeti/HTools/JSON.hs b/htools/Ganeti/HTools/JSON.hs
new file mode 100644 (file)
index 0000000..684711f
--- /dev/null
@@ -0,0 +1,134 @@
+{-| JSON utility functions. -}
+
+{-
+
+Copyright (C) 2009, 2010, 2011, 2012 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.JSON
+  ( fromJResult
+  , readEitherString
+  , JSRecord
+  , loadJSArray
+  , fromObj
+  , maybeFromObj
+  , fromObjWithDefault
+  , fromKeyValue
+  , fromJVal
+  , asJSObject
+  , asObjectList
+  , tryFromObj
+  , toArray
+  )
+  where
+
+import Control.Monad (liftM)
+import Data.Maybe (fromMaybe)
+import Text.Printf (printf)
+
+import qualified Text.JSON as J
+
+import Ganeti.BasicTypes
+
+-- * JSON-related functions
+
+-- | A type alias for the list-based representation of J.JSObject.
+type JSRecord = [(String, J.JSValue)]
+
+-- | Converts a JSON Result into a monadic value.
+fromJResult :: Monad m => String -> J.Result a -> m a
+fromJResult s (J.Error x) = fail (s ++ ": " ++ x)
+fromJResult _ (J.Ok x) = return x
+
+-- | Tries to read a string from a JSON value.
+--
+-- In case the value was not a string, we fail the read (in the
+-- context of the current monad.
+readEitherString :: (Monad m) => J.JSValue -> m String
+readEitherString v =
+  case v of
+    J.JSString s -> return $ J.fromJSString s
+    _ -> fail "Wrong JSON type"
+
+-- | Converts a JSON message into an array of JSON objects.
+loadJSArray :: (Monad m)
+               => String -- ^ Operation description (for error reporting)
+               -> String -- ^ Input message
+               -> m [J.JSObject J.JSValue]
+loadJSArray s = fromJResult s . J.decodeStrict
+
+-- | Reads the value of a key in a JSON object.
+fromObj :: (J.JSON a, Monad m) => JSRecord -> String -> m a
+fromObj o k =
+  case lookup k o of
+    Nothing -> fail $ printf "key '%s' not found, object contains only %s"
+               k (show (map fst o))
+    Just val -> fromKeyValue k val
+
+-- | Reads the value of an optional key in a JSON object.
+maybeFromObj :: (J.JSON a, Monad m) =>
+                JSRecord -> String -> m (Maybe a)
+maybeFromObj o k =
+  case lookup k o of
+    Nothing -> return Nothing
+    Just val -> liftM Just (fromKeyValue k val)
+
+-- | Reads the value of a key in a JSON object with a default if missing.
+fromObjWithDefault :: (J.JSON a, Monad m) =>
+                      JSRecord -> String -> a -> m a
+fromObjWithDefault o k d = liftM (fromMaybe d) $ maybeFromObj o k
+
+-- | Reads a JValue, that originated from an object key.
+fromKeyValue :: (J.JSON a, Monad m)
+              => String     -- ^ The key name
+              -> J.JSValue  -- ^ The value to read
+              -> m a
+fromKeyValue k val =
+  fromJResult (printf "key '%s'" k) (J.readJSON val)
+
+-- | Small wrapper over readJSON.
+fromJVal :: (Monad m, J.JSON a) => J.JSValue -> m a
+fromJVal v =
+  case J.readJSON v of
+    J.Error s -> fail ("Cannot convert value '" ++ show v ++
+                       "', error: " ++ s)
+    J.Ok x -> return x
+
+-- | Converts a JSON value into a JSON object.
+asJSObject :: (Monad m) => J.JSValue -> m (J.JSObject J.JSValue)
+asJSObject (J.JSObject a) = return a
+asJSObject _ = fail "not an object"
+
+-- | Coneverts a list of JSON values into a list of JSON objects.
+asObjectList :: (Monad m) => [J.JSValue] -> m [J.JSObject J.JSValue]
+asObjectList = mapM asJSObject
+
+-- | Try to extract a key from a object with better error reporting
+-- than fromObj.
+tryFromObj :: (J.JSON a) =>
+              String     -- ^ Textual "owner" in error messages
+           -> JSRecord   -- ^ The object array
+           -> String     -- ^ The desired key from the object
+           -> Result a
+tryFromObj t o = annotateResult t . fromObj o
+
+-- | Ensure a given JSValue is actually a JSArray.
+toArray :: (Monad m) => J.JSValue -> m [J.JSValue]
+toArray (J.JSArray arr) = return arr
+toArray o = fail $ "Invalid input, expected array but got " ++ show o
index db08d73..851c84b 100644 (file)
@@ -7,7 +7,7 @@ has been loaded from external sources.
 
 {-
 
-Copyright (C) 2009, 2010, 2011 Google Inc.
+Copyright (C) 2009, 2010, 2011, 2012 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
@@ -27,27 +27,20 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 -}
 
 module Ganeti.HTools.Loader
-    ( mergeData
-    , checkData
-    , assignIndices
-    , lookupName
-    , goodLookupResult
-    , lookupNode
-    , lookupInstance
-    , lookupGroup
-    , commonSuffix
-    , RqType(..)
-    , Request(..)
-    , ClusterData(..)
-    , emptyCluster
-    , compareNameComponent
-    , prefixMatch
-    , LookupResult(..)
-    , MatchPriority(..)
-    ) where
+  ( mergeData
+  , checkData
+  , assignIndices
+  , lookupNode
+  , lookupInstance
+  , lookupGroup
+  , commonSuffix
+  , RqType(..)
+  , Request(..)
+  , ClusterData(..)
+  , emptyCluster
+  ) where
 
 import Data.List
-import Data.Function
 import qualified Data.Map as M
 import Text.Printf (printf)
 
@@ -55,7 +48,9 @@ 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 qualified Ganeti.HTools.Cluster as Cluster
 
+import Ganeti.BasicTypes
 import Ganeti.HTools.Types
 import Ganeti.HTools.Utils
 
@@ -74,122 +69,52 @@ request-specific fields.
 
 -}
 data RqType
-    = Allocate Instance.Instance Int -- ^ A new instance allocation
-    | Relocate Idx Int [Ndx]         -- ^ Choose a new secondary node
-    | NodeEvacuate [Idx] EvacMode    -- ^ node-evacuate mode
-    | ChangeGroup [Gdx] [Idx]        -- ^ Multi-relocate mode
+  = Allocate Instance.Instance Int -- ^ A new instance allocation
+  | Relocate Idx Int [Ndx]         -- ^ Choose a new secondary node
+  | NodeEvacuate [Idx] EvacMode    -- ^ node-evacuate mode
+  | ChangeGroup [Gdx] [Idx]        -- ^ Multi-relocate mode
     deriving (Show, Read)
 
 -- | A complete request, as received from Ganeti.
 data Request = Request RqType ClusterData
-    deriving (Show, Read)
+               deriving (Show, Read)
 
 -- | The cluster state.
 data ClusterData = ClusterData
-    { cdGroups    :: Group.List    -- ^ The node group list
-    , cdNodes     :: Node.List     -- ^ The node list
-    , cdInstances :: Instance.List -- ^ The instance list
-    , cdTags      :: [String]      -- ^ The cluster tags
-    } deriving (Show, Read)
-
--- | The priority of a match in a lookup result.
-data MatchPriority = ExactMatch
-                   | MultipleMatch
-                   | PartialMatch
-                   | FailMatch
-                   deriving (Show, Read, Enum, Eq, Ord)
-
--- | The result of a name lookup in a list.
-data LookupResult = LookupResult
-    { lrMatchPriority :: MatchPriority -- ^ The result type
-    -- | Matching value (for ExactMatch, PartialMatch), Lookup string otherwise
-    , lrContent :: String
-    } deriving (Show, Read)
-
--- | Lookup results have an absolute preference ordering.
-instance Eq LookupResult where
-  (==) = (==) `on` lrMatchPriority
-
-instance Ord LookupResult where
-  compare = compare `on` lrMatchPriority
+  { cdGroups    :: Group.List    -- ^ The node group list
+  , cdNodes     :: Node.List     -- ^ The node list
+  , cdInstances :: Instance.List -- ^ The instance list
+  , cdTags      :: [String]      -- ^ The cluster tags
+  , cdIPolicy   :: IPolicy       -- ^ The cluster instance policy
+  } deriving (Show, Read, Eq)
 
 -- | An empty cluster.
 emptyCluster :: ClusterData
 emptyCluster = ClusterData Container.empty Container.empty Container.empty []
+                 defIPolicy
 
 -- * Functions
 
 -- | Lookups a node into an assoc list.
 lookupNode :: (Monad m) => NameAssoc -> String -> String -> m Ndx
 lookupNode ktn inst node =
-    case M.lookup node ktn of
-      Nothing -> fail $ "Unknown node '" ++ node ++ "' for instance " ++ inst
-      Just idx -> return idx
+  case M.lookup node ktn of
+    Nothing -> fail $ "Unknown node '" ++ node ++ "' for instance " ++ inst
+    Just idx -> return idx
 
 -- | Lookups an instance into an assoc list.
 lookupInstance :: (Monad m) => NameAssoc -> String -> m Idx
 lookupInstance kti inst =
-    case M.lookup inst kti of
-      Nothing -> fail $ "Unknown instance '" ++ inst ++ "'"
-      Just idx -> return idx
+  case M.lookup inst kti of
+    Nothing -> fail $ "Unknown instance '" ++ inst ++ "'"
+    Just idx -> return idx
 
 -- | Lookups a group into an assoc list.
 lookupGroup :: (Monad m) => NameAssoc -> String -> String -> m Gdx
 lookupGroup ktg nname gname =
-    case M.lookup gname ktg of
-      Nothing -> fail $ "Unknown group '" ++ gname ++ "' for node " ++ nname
-      Just idx -> return idx
-
--- | Check for prefix matches in names.
--- Implemented in Ganeti core utils.text.MatchNameComponent
--- as the regexp r"^%s(\..*)?$" % re.escape(key)
-prefixMatch :: String  -- ^ Lookup
-            -> String  -- ^ Full name
-            -> Bool    -- ^ Whether there is a prefix match
-prefixMatch lkp = isPrefixOf (lkp ++ ".")
-
--- | Is the lookup priority a "good" one?
-goodMatchPriority :: MatchPriority -> Bool
-goodMatchPriority ExactMatch = True
-goodMatchPriority PartialMatch = True
-goodMatchPriority _ = False
-
--- | Is the lookup result an actual match?
-goodLookupResult :: LookupResult -> Bool
-goodLookupResult = goodMatchPriority . lrMatchPriority
-
--- | Compares a canonical name and a lookup string.
-compareNameComponent :: String        -- ^ Canonical (target) name
-                     -> String        -- ^ Partial (lookup) name
-                     -> LookupResult  -- ^ Result of the lookup
-compareNameComponent cnl lkp =
-  select (LookupResult FailMatch lkp)
-  [ (cnl == lkp          , LookupResult ExactMatch cnl)
-  , (prefixMatch lkp cnl , LookupResult PartialMatch cnl)
-  ]
-
--- | Lookup a string and choose the best result.
-chooseLookupResult :: String       -- ^ Lookup key
-                   -> String       -- ^ String to compare to the lookup key
-                   -> LookupResult -- ^ Previous result
-                   -> LookupResult -- ^ New result
-chooseLookupResult lkp cstr old =
-  -- default: use class order to pick the minimum result
-  select (min new old)
-  -- special cases:
-  -- short circuit if the new result is an exact match
-  [ ((lrMatchPriority new) == ExactMatch, new)
-  -- if both are partial matches generate a multiple match
-  , (partial2, LookupResult MultipleMatch lkp)
-  ] where new = compareNameComponent cstr lkp
-          partial2 = all ((PartialMatch==) . lrMatchPriority) [old, new]
-
--- | Find the canonical name for a lookup string in a list of names.
-lookupName :: [String]      -- ^ List of keys
-           -> String        -- ^ Lookup string
-           -> LookupResult  -- ^ Result of the lookup
-lookupName l s = foldr (chooseLookupResult s)
-                       (LookupResult FailMatch s) l
+  case M.lookup gname ktg of
+    Nothing -> fail $ "Unknown group '" ++ gname ++ "' for node " ++ nname
+    Just idx -> return idx
 
 -- | Given a list of elements (and their names), assign indices to them.
 assignIndices :: (Element a) =>
@@ -206,26 +131,32 @@ fixNodes :: Node.List
          -> Instance.Instance
          -> Node.List
 fixNodes accu inst =
-    let
-        pdx = Instance.pNode inst
-        sdx = Instance.sNode inst
-        pold = Container.find pdx accu
-        pnew = Node.setPri pold inst
-        ac2 = Container.add pdx pnew accu
-    in
-      if sdx /= Node.noSecondary
-      then let sold = Container.find sdx accu
-               snew = Node.setSec sold inst
-           in Container.add sdx snew ac2
-      else ac2
+  let pdx = Instance.pNode inst
+      sdx = Instance.sNode inst
+      pold = Container.find pdx accu
+      pnew = Node.setPri pold inst
+      ac2 = Container.add pdx pnew accu
+  in if sdx /= Node.noSecondary
+       then let sold = Container.find sdx accu
+                snew = Node.setSec sold inst
+            in Container.add sdx snew ac2
+       else ac2
+
+-- | Set the node's policy to its group one. Note that this requires
+-- the group to exist (should have been checked before), otherwise it
+-- will abort with a runtime error.
+setNodePolicy :: Group.List -> Node.Node -> Node.Node
+setNodePolicy gl node =
+  let grp = Container.find (Node.group node) gl
+      gpol = Group.iPolicy grp
+  in Node.setPolicy gpol node
 
 -- | Remove non-selected tags from the exclusion list.
 filterExTags :: [String] -> Instance.Instance -> Instance.Instance
 filterExTags tl inst =
-    let old_tags = Instance.tags inst
-        new_tags = filter (\tag -> any (`isPrefixOf` tag) tl)
-                   old_tags
-    in inst { Instance.tags = new_tags }
+  let old_tags = Instance.tags inst
+      new_tags = filter (\tag -> any (`isPrefixOf` tag) tl) old_tags
+  in inst { Instance.tags = new_tags }
 
 -- | Update the movable attribute.
 updateMovable :: [String]           -- ^ Selected instances (if not empty)
@@ -233,9 +164,15 @@ updateMovable :: [String]           -- ^ Selected instances (if not empty)
               -> Instance.Instance  -- ^ Target Instance
               -> Instance.Instance  -- ^ Target Instance with updated attribute
 updateMovable selinsts exinsts inst =
-    if Instance.sNode inst == Node.noSecondary ||
-       Instance.name inst `elem` exinsts ||
-       not (null selinsts || Instance.name inst `elem` selinsts)
+  if Instance.name inst `elem` exinsts ||
+     not (null selinsts || Instance.name inst `elem` selinsts)
+    then Instance.setMovable inst False
+    else inst
+
+-- | Disables moves for instances with a split group.
+disableSplitMoves :: Node.List -> Instance.Instance -> Instance.Instance
+disableSplitMoves nl inst =
+  if not . isOk . Cluster.instanceGroup nl $ inst
     then Instance.setMovable inst False
     else inst
 
@@ -244,23 +181,23 @@ updateMovable selinsts exinsts inst =
 longestDomain :: [String] -> String
 longestDomain [] = ""
 longestDomain (x:xs) =
-      foldr (\ suffix accu -> if all (isSuffixOf suffix) xs
-                              then suffix
-                              else accu)
-      "" $ filter (isPrefixOf ".") (tails x)
+  foldr (\ suffix accu -> if all (isSuffixOf suffix) xs
+                            then suffix
+                            else accu)
+          "" $ filter (isPrefixOf ".") (tails x)
 
 -- | Extracts the exclusion tags from the cluster configuration.
 extractExTags :: [String] -> [String]
 extractExTags =
-    map (drop (length exTagsPrefix)) .
-    filter (isPrefixOf exTagsPrefix)
+  map (drop (length exTagsPrefix)) .
+  filter (isPrefixOf exTagsPrefix)
 
 -- | Extracts the common suffix from node\/instance names.
 commonSuffix :: Node.List -> Instance.List -> String
 commonSuffix nl il =
-    let node_names = map Node.name $ Container.elems nl
-        inst_names = map Instance.name $ Container.elems il
-    in longestDomain (node_names ++ inst_names)
+  let node_names = map Node.name $ Container.elems nl
+      inst_names = map Instance.name $ Container.elems il
+  in longestDomain (node_names ++ inst_names)
 
 -- | Initializer function that loads the data from a node and instance
 -- list and massages it into the correct format.
@@ -270,7 +207,7 @@ mergeData :: [(String, DynUtil)]  -- ^ Instance utilisation data
           -> [String]             -- ^ Excluded instances
           -> ClusterData          -- ^ Data from backends
           -> Result ClusterData   -- ^ Fixed cluster data
-mergeData um extags selinsts exinsts cdata@(ClusterData _ nl il2 tags) =
+mergeData um extags selinsts exinsts cdata@(ClusterData gl nl il2 tags _) =
   let il = Container.elems il2
       il3 = foldl' (\im (name, n_util) ->
                         case Container.findByName im name of
@@ -286,16 +223,18 @@ mergeData um extags selinsts exinsts cdata@(ClusterData _ nl il2 tags) =
       lkp_unknown = filter (not . goodLookupResult) (selinst_lkp ++ exinst_lkp)
       selinst_names = map lrContent selinst_lkp
       exinst_names = map lrContent exinst_lkp
-      il4 = Container.map (filterExTags allextags .
-                           updateMovable selinst_names exinst_names) il3
-      nl2 = foldl' fixNodes nl (Container.elems il4)
-      nl3 = Container.map (`Node.buildPeers` il4) nl2
       node_names = map Node.name (Container.elems nl)
       common_suffix = longestDomain (node_names ++ inst_names)
-      snl = Container.map (computeAlias common_suffix) nl3
-      sil = Container.map (computeAlias common_suffix) il4
+      il4 = Container.map (computeAlias common_suffix .
+                           filterExTags allextags .
+                           updateMovable selinst_names exinst_names) il3
+      nl2 = foldl' fixNodes nl (Container.elems il4)
+      nl3 = Container.map (setNodePolicy gl .
+                           computeAlias common_suffix .
+                           (`Node.buildPeers` il4)) nl2
+      il5 = Container.map (disableSplitMoves nl3) il4
   in if' (null lkp_unknown)
-         (Ok cdata { cdNodes = snl, cdInstances = sil })
+         (Ok cdata { cdNodes = nl3, cdInstances = il5 })
          (Bad $ "Unknown instance(s): " ++ show(map lrContent lkp_unknown))
 
 -- | Checks the cluster data for consistency.
@@ -306,7 +245,7 @@ checkData nl il =
         (\ msgs node ->
              let nname = Node.name node
                  nilst = map (`Container.find` il) (Node.pList node)
-                 dilst = filter (not . Instance.running) nilst
+                 dilst = filter Instance.instanceDown nilst
                  adj_mem = sum . map Instance.mem $ dilst
                  delta_mem = truncate (Node.tMem node)
                              - Node.nMem node
@@ -320,10 +259,9 @@ checkData nl il =
                         (Node.fMem node - adj_mem)
                  umsg1 =
                    if delta_mem > 512 || delta_dsk > 1024
-                      then (printf "node %s is missing %d MB ram \
-                                   \and %d GB disk"
-                                   nname delta_mem (delta_dsk `div` 1024)):
-                           msgs
+                      then printf "node %s is missing %d MB ram \
+                                  \and %d GB disk"
+                                  nname delta_mem (delta_dsk `div` 1024):msgs
                       else msgs
              in (umsg1, newn)
         ) [] nl
@@ -331,14 +269,16 @@ checkData nl il =
 -- | Compute the amount of memory used by primary instances on a node.
 nodeImem :: Node.Node -> Instance.List -> Int
 nodeImem node il =
-    let rfind = flip Container.find il
-    in sum . map (Instance.mem . rfind)
-           $ Node.pList node
+  let rfind = flip Container.find il
+      il' = map rfind $ Node.pList node
+      oil' = filter Instance.notOffline il'
+  in sum . map Instance.mem $ oil'
+
 
 -- | Compute the amount of disk used by instances on a node (either primary
 -- or secondary).
 nodeIdsk :: Node.Node -> Instance.List -> Int
 nodeIdsk node il =
-    let rfind = flip Container.find il
-    in sum . map (Instance.dsk . rfind)
-           $ Node.pList node ++ Node.sList node
+  let rfind = flip Container.find il
+  in sum . map (Instance.dsk . rfind)
+       $ Node.pList node ++ Node.sList node
index ee526be..44d90b8 100644 (file)
@@ -4,7 +4,7 @@
 
 {-
 
-Copyright (C) 2009, 2010, 2011 Google Inc.
+Copyright (C) 2009, 2010, 2011, 2012 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
@@ -24,10 +24,9 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 -}
 
 module Ganeti.HTools.Luxi
-    (
-      loadData
-    , parseData
-    ) where
+  ( loadData
+  , parseData
+  ) where
 
 import qualified Control.Exception as E
 import Text.JSON.Types
@@ -39,45 +38,76 @@ import Ganeti.HTools.Types
 import qualified Ganeti.HTools.Group as Group
 import qualified Ganeti.HTools.Node as Node
 import qualified Ganeti.HTools.Instance as Instance
-import Ganeti.HTools.Utils (fromJVal, annotateResult, tryFromObj, asJSObject)
+import Ganeti.HTools.JSON
+
+{-# ANN module "HLint: ignore Eta reduce" #-}
 
 -- * Utility functions
 
--- | Ensure a given JSValue is actually a JSArray.
-toArray :: (Monad m) => JSValue -> m [JSValue]
-toArray v =
-    case v of
-      JSArray arr -> return arr
-      o -> fail ("Invalid input, expected array but got " ++ show o)
+-- | Get values behind \"data\" part of the result.
+getData :: (Monad m) => JSValue -> m JSValue
+getData (JSObject o) = fromObj (fromJSObject o) "data"
+getData x = fail $ "Invalid input, expected dict entry but got " ++ show x
+
+-- | Converts a (status, value) into m value, if possible.
+parseQueryField :: (Monad m) => JSValue -> m (JSValue, JSValue)
+parseQueryField (JSArray [status, result]) = return (status, result)
+parseQueryField o =
+  fail $ "Invalid query field, expected (status, value) but got " ++ show o
+
+-- | Parse a result row.
+parseQueryRow :: (Monad m) => JSValue -> m [(JSValue, JSValue)]
+parseQueryRow (JSArray arr) = mapM parseQueryField arr
+parseQueryRow o =
+  fail $ "Invalid query row result, expected array but got " ++ show o
+
+-- | Parse an overall query result and get the [(status, value)] list
+-- for each element queried.
+parseQueryResult :: (Monad m) => JSValue -> m [[(JSValue, JSValue)]]
+parseQueryResult (JSArray arr) = mapM parseQueryRow arr
+parseQueryResult o =
+  fail $ "Invalid query result, expected array but got " ++ show o
+
+-- | Prepare resulting output as parsers expect it.
+extractArray :: (Monad m) => JSValue -> m [[(JSValue, JSValue)]]
+extractArray v =
+  getData v >>= parseQueryResult
+
+-- | Testing result status for more verbose error message.
+fromJValWithStatus :: (Text.JSON.JSON a, Monad m) => (JSValue, JSValue) -> m a
+fromJValWithStatus (st, v) = do
+  st' <- fromJVal st
+  L.checkRS st' v >>= fromJVal
 
 -- | Annotate errors when converting values with owner/attribute for
 -- better debugging.
 genericConvert :: (Text.JSON.JSON a) =>
-                  String     -- ^ The object type
-               -> String     -- ^ The object name
-               -> String     -- ^ The attribute we're trying to convert
-               -> JSValue    -- ^ The value we try to convert
-               -> Result a   -- ^ The annotated result
+                  String             -- ^ The object type
+               -> String             -- ^ The object name
+               -> String             -- ^ The attribute we're trying to convert
+               -> (JSValue, JSValue) -- ^ The value we're trying to convert
+               -> Result a           -- ^ The annotated result
 genericConvert otype oname oattr =
-    annotateResult (otype ++ " '" ++ oname ++
-                    "', error while reading attribute '" ++
-                    oattr ++ "'") . fromJVal
+  annotateResult (otype ++ " '" ++ oname ++
+                  "', error while reading attribute '" ++
+                  oattr ++ "'") . fromJValWithStatus
 
 -- * Data querying functionality
 
 -- | The input data for node query.
 queryNodesMsg :: L.LuxiOp
 queryNodesMsg =
-  L.QueryNodes [] ["name", "mtotal", "mnode", "mfree", "dtotal", "dfree",
-                   "ctotal", "offline", "drained", "vm_capable",
-                   "group.uuid"] False
+  L.Query L.QRNode ["name", "mtotal", "mnode", "mfree", "dtotal", "dfree",
+                    "ctotal", "offline", "drained", "vm_capable",
+                    "ndp/spindle_count", "group.uuid"] ()
 
 -- | The input data for instance query.
 queryInstancesMsg :: L.LuxiOp
 queryInstancesMsg =
-  L.QueryInstances [] ["name", "disk_usage", "be/memory", "be/vcpus",
-                       "status", "pnode", "snodes", "tags", "oper_ram",
-                       "be/auto_balance", "disk_template"] False
+  L.Query L.QRInstance ["name", "disk_usage", "be/memory", "be/vcpus",
+                        "status", "pnode", "snodes", "tags", "oper_ram",
+                        "be/auto_balance", "disk_template",
+                        "be/spindle_use"] ()
 
 -- | The input data for cluster query.
 queryClusterInfoMsg :: L.LuxiOp
@@ -86,7 +116,7 @@ queryClusterInfoMsg = L.QueryClusterInfo
 -- | The input data for node group query.
 queryGroupsMsg :: L.LuxiOp
 queryGroupsMsg =
-  L.QueryGroups [] ["uuid", "name", "alloc_policy"] False
+  L.Query L.QRGroup ["uuid", "name", "alloc_policy", "ipolicy"] ()
 
 -- | Wraper over 'callMethod' doing node query.
 queryNodes :: L.Client -> IO (Result JSValue)
@@ -108,85 +138,93 @@ queryGroups = L.callMethod queryGroupsMsg
 getInstances :: NameAssoc
              -> JSValue
              -> Result [(String, Instance.Instance)]
-getInstances ktn arr = toArray arr >>= mapM (parseInstance ktn)
+getInstances ktn arr = extractArray arr >>= mapM (parseInstance ktn)
 
 -- | Construct an instance from a JSON object.
 parseInstance :: NameAssoc
-              -> JSValue
+              -> [(JSValue, JSValue)]
               -> Result (String, Instance.Instance)
-parseInstance ktn (JSArray [ name, disk, mem, vcpus
-                           , status, pnode, snodes, tags, oram
-                           , auto_balance, disk_template ]) = do
-  xname <- annotateResult "Parsing new instance" (fromJVal name)
+parseInstance ktn [ name, disk, mem, vcpus
+                  , status, pnode, snodes, tags, oram
+                  , auto_balance, disk_template, su ] = do
+  xname <- annotateResult "Parsing new instance" (fromJValWithStatus name)
   let convert a = genericConvert "Instance" xname a
   xdisk <- convert "disk_usage" disk
-  xmem <- (case oram of
-             JSRational _ _ -> convert "oper_ram" oram
-             _ -> convert "be/memory" mem)
+  xmem <- case oram of -- FIXME: remove the "guessing"
+            (_, JSRational _ _) -> convert "oper_ram" oram
+            _ -> convert "be/memory" mem
   xvcpus <- convert "be/vcpus" vcpus
   xpnode <- convert "pnode" pnode >>= lookupNode ktn xname
   xsnodes <- convert "snodes" snodes::Result [JSString]
-  snode <- (if null xsnodes then return Node.noSecondary
-            else lookupNode ktn xname (fromJSString $ head xsnodes))
+  snode <- if null xsnodes
+             then return Node.noSecondary
+             else lookupNode ktn xname (fromJSString $ head xsnodes)
   xrunning <- convert "status" status
   xtags <- convert "tags" tags
   xauto_balance <- convert "auto_balance" auto_balance
   xdt <- convert "disk_template" disk_template
+  xsu <- convert "be/spindle_use" su
   let inst = Instance.create xname xmem xdisk xvcpus
-             xrunning xtags xauto_balance xpnode snode xdt
+             xrunning xtags xauto_balance xpnode snode xdt xsu
   return (xname, inst)
 
 parseInstance _ v = fail ("Invalid instance query result: " ++ show v)
 
 -- | Parse a node list in JSON format.
 getNodes :: NameAssoc -> JSValue -> Result [(String, Node.Node)]
-getNodes ktg arr = toArray arr >>= mapM (parseNode ktg)
+getNodes ktg arr = extractArray arr >>= mapM (parseNode ktg)
 
 -- | Construct a node from a JSON object.
-parseNode :: NameAssoc -> JSValue -> Result (String, Node.Node)
-parseNode ktg (JSArray [ name, mtotal, mnode, mfree, dtotal, dfree
-                       , ctotal, offline, drained, vm_capable, g_uuid ])
+parseNode :: NameAssoc -> [(JSValue, JSValue)] -> Result (String, Node.Node)
+parseNode ktg [ name, mtotal, mnode, mfree, dtotal, dfree
+              , ctotal, offline, drained, vm_capable, spindles, g_uuid ]
     = do
-  xname <- annotateResult "Parsing new node" (fromJVal name)
+  xname <- annotateResult "Parsing new node" (fromJValWithStatus name)
   let convert a = genericConvert "Node" xname a
   xoffline <- convert "offline" offline
   xdrained <- convert "drained" drained
   xvm_capable <- convert "vm_capable" vm_capable
+  xspindles <- convert "spindles" spindles
   xgdx   <- convert "group.uuid" g_uuid >>= lookupGroup ktg xname
-  node <- (if xoffline || xdrained || not xvm_capable
-           then return $ Node.create xname 0 0 0 0 0 0 True xgdx
-           else do
-             xmtotal  <- convert "mtotal" mtotal
-             xmnode   <- convert "mnode" mnode
-             xmfree   <- convert "mfree" mfree
-             xdtotal  <- convert "dtotal" dtotal
-             xdfree   <- convert "dfree" dfree
-             xctotal  <- convert "ctotal" ctotal
-             return $ Node.create xname xmtotal xmnode xmfree
-                    xdtotal xdfree xctotal False xgdx)
+  node <- if xoffline || xdrained || not xvm_capable
+            then return $ Node.create xname 0 0 0 0 0 0 True xspindles xgdx
+            else do
+              xmtotal  <- convert "mtotal" mtotal
+              xmnode   <- convert "mnode" mnode
+              xmfree   <- convert "mfree" mfree
+              xdtotal  <- convert "dtotal" dtotal
+              xdfree   <- convert "dfree" dfree
+              xctotal  <- convert "ctotal" ctotal
+              return $ Node.create xname xmtotal xmnode xmfree
+                     xdtotal xdfree xctotal False xspindles xgdx
   return (xname, node)
 
 parseNode _ v = fail ("Invalid node query result: " ++ show v)
 
 -- | Parses the cluster tags.
-getClusterTags :: JSValue -> Result [String]
-getClusterTags v = do
+getClusterData :: JSValue -> Result ([String], IPolicy)
+getClusterData (JSObject obj) = do
   let errmsg = "Parsing cluster info"
-  obj <- annotateResult errmsg $ asJSObject v
-  tryFromObj errmsg (fromJSObject obj) "tags"
+      obj' = fromJSObject obj
+  ctags <- tryFromObj errmsg obj' "tags"
+  cpol <- tryFromObj errmsg obj' "ipolicy"
+  return (ctags, cpol)
+
+getClusterData _ = Bad $ "Cannot parse cluster info, not a JSON record"
 
 -- | Parses the cluster groups.
 getGroups :: JSValue -> Result [(String, Group.Group)]
-getGroups arr = toArray arr >>= mapM parseGroup
+getGroups jsv = extractArray jsv >>= mapM parseGroup
 
 -- | Parses a given group information.
-parseGroup :: JSValue -> Result (String, Group.Group)
-parseGroup (JSArray [ uuid, name, apol ]) = do
-  xname <- annotateResult "Parsing new group" (fromJVal name)
+parseGroup :: [(JSValue, JSValue)] -> Result (String, Group.Group)
+parseGroup [uuid, name, apol, ipol] = do
+  xname <- annotateResult "Parsing new group" (fromJValWithStatus name)
   let convert a = genericConvert "Group" xname a
   xuuid <- convert "uuid" uuid
   xapol <- convert "alloc_policy" apol
-  return (xuuid, Group.create xname xuuid xapol)
+  xipol <- convert "ipolicy" ipol
+  return (xuuid, Group.create xname xuuid xapol xipol)
 
 parseGroup v = fail ("Invalid group query result: " ++ show v)
 
@@ -218,8 +256,8 @@ parseData (groups, nodes, instances, cinfo) = do
   let (node_names, node_idx) = assignIndices node_data
   inst_data <- instances >>= getInstances node_names
   let (_, inst_idx) = assignIndices inst_data
-  ctags <- cinfo >>= getClusterTags
-  return (ClusterData group_idx node_idx inst_idx ctags)
+  (ctags, cpol) <- cinfo >>= getClusterData
+  return (ClusterData group_idx node_idx inst_idx ctags cpol)
 
 -- | Top level function for data loading.
 loadData :: String -- ^ Unix socket to use as source
index 7b4e70e..f744e58 100644 (file)
@@ -6,7 +6,7 @@
 
 {-
 
-Copyright (C) 2009, 2010, 2011 Google Inc.
+Copyright (C) 2009, 2010, 2011, 2012 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
@@ -26,50 +26,51 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 -}
 
 module Ganeti.HTools.Node
-    ( Node(..)
-    , List
-    -- * Constructor
-    , create
-    -- ** Finalization after data loading
-    , buildPeers
-    , setIdx
-    , setAlias
-    , setOffline
-    , setXmem
-    , setFmem
-    , setPri
-    , setSec
-    , setMdsk
-    , setMcpu
-    -- * Tag maps
-    , addTags
-    , delTags
-    , rejectAddTags
-    -- * Instance (re)location
-    , removePri
-    , removeSec
-    , addPri
-    , addPriEx
-    , addSec
-    , addSecEx
-    -- * Stats
-    , availDisk
-    , availMem
-    , availCpu
-    , iMem
-    , iDsk
-    , conflictingPrimaries
-    -- * Formatting
-    , defaultFields
-    , showHeader
-    , showField
-    , list
-    -- * Misc stuff
-    , AssocList
-    , AllocElement
-    , noSecondary
-    , computeGroups
-    ) where
+  ( Node(..)
+  , List
+  -- * Constructor
+  , create
+  -- ** Finalization after data loading
+  , buildPeers
+  , setIdx
+  , setAlias
+  , setOffline
+  , setXmem
+  , setFmem
+  , setPri
+  , setSec
+  , setMdsk
+  , setMcpu
+  , setPolicy
+  -- * Tag maps
+  , addTags
+  , delTags
+  , rejectAddTags
+  -- * Instance (re)location
+  , removePri
+  , removeSec
+  , addPri
+  , addPriEx
+  , addSec
+  , addSecEx
+  -- * Stats
+  , availDisk
+  , availMem
+  , availCpu
+  , iMem
+  , iDsk
+  , conflictingPrimaries
+  -- * Formatting
+  , defaultFields
+  , showHeader
+  , showField
+  , list
+  -- * Misc stuff
+  , AssocList
+  , AllocElement
+  , noSecondary
+  , computeGroups
+  ) where
 
 import Data.List hiding (group)
 import qualified Data.Map as Map
@@ -90,48 +91,52 @@ type TagMap = Map.Map String Int
 
 -- | The node type.
 data Node = Node
-    { name     :: String    -- ^ The node name
-    , alias    :: String    -- ^ The shortened name (for display purposes)
-    , tMem     :: Double    -- ^ Total memory (MiB)
-    , nMem     :: Int       -- ^ Node memory (MiB)
-    , fMem     :: Int       -- ^ Free memory (MiB)
-    , xMem     :: Int       -- ^ Unaccounted memory (MiB)
-    , tDsk     :: Double    -- ^ Total disk space (MiB)
-    , fDsk     :: Int       -- ^ Free disk space (MiB)
-    , tCpu     :: Double    -- ^ Total CPU count
-    , uCpu     :: Int       -- ^ Used VCPU count
-    , pList    :: [T.Idx]   -- ^ List of primary instance indices
-    , sList    :: [T.Idx]   -- ^ List of secondary instance indices
-    , idx      :: T.Ndx     -- ^ Internal index for book-keeping
-    , peers    :: P.PeerMap -- ^ Pnode to instance mapping
-    , failN1   :: Bool      -- ^ Whether the node has failed n1
-    , rMem     :: Int       -- ^ Maximum memory needed for failover by
-                            -- primaries of this node
-    , pMem     :: Double    -- ^ Percent of free memory
-    , pDsk     :: Double    -- ^ Percent of free disk
-    , pRem     :: Double    -- ^ Percent of reserved memory
-    , pCpu     :: Double    -- ^ Ratio of virtual to physical CPUs
-    , mDsk     :: Double    -- ^ Minimum free disk ratio
-    , mCpu     :: Double    -- ^ Max ratio of virt-to-phys CPUs
-    , loDsk    :: Int       -- ^ Autocomputed from mDsk low disk
-                            -- threshold
-    , hiCpu    :: Int       -- ^ Autocomputed from mCpu high cpu
-                            -- threshold
-    , offline  :: Bool      -- ^ Whether the node should not be used
-                            -- for allocations and skipped from score
-                            -- computations
-    , utilPool :: T.DynUtil -- ^ Total utilisation capacity
-    , utilLoad :: T.DynUtil -- ^ Sum of instance utilisation
-    , pTags    :: TagMap    -- ^ Map of primary instance tags and their count
-    , group    :: T.Gdx     -- ^ The node's group (index)
-    } deriving (Show, Read, Eq)
+  { name     :: String    -- ^ The node name
+  , alias    :: String    -- ^ The shortened name (for display purposes)
+  , tMem     :: Double    -- ^ Total memory (MiB)
+  , nMem     :: Int       -- ^ Node memory (MiB)
+  , fMem     :: Int       -- ^ Free memory (MiB)
+  , xMem     :: Int       -- ^ Unaccounted memory (MiB)
+  , tDsk     :: Double    -- ^ Total disk space (MiB)
+  , fDsk     :: Int       -- ^ Free disk space (MiB)
+  , tCpu     :: Double    -- ^ Total CPU count
+  , uCpu     :: Int       -- ^ Used VCPU count
+  , spindleCount :: Int   -- ^ Node spindles (spindle_count node parameter)
+  , pList    :: [T.Idx]   -- ^ List of primary instance indices
+  , sList    :: [T.Idx]   -- ^ List of secondary instance indices
+  , idx      :: T.Ndx     -- ^ Internal index for book-keeping
+  , peers    :: P.PeerMap -- ^ Pnode to instance mapping
+  , failN1   :: Bool      -- ^ Whether the node has failed n1
+  , rMem     :: Int       -- ^ Maximum memory needed for failover by
+                          -- primaries of this node
+  , pMem     :: Double    -- ^ Percent of free memory
+  , pDsk     :: Double    -- ^ Percent of free disk
+  , pRem     :: Double    -- ^ Percent of reserved memory
+  , pCpu     :: Double    -- ^ Ratio of virtual to physical CPUs
+  , mDsk     :: Double    -- ^ Minimum free disk ratio
+  , loDsk    :: Int       -- ^ Autocomputed from mDsk low disk
+                          -- threshold
+  , hiCpu    :: Int       -- ^ Autocomputed from mCpu high cpu
+                          -- threshold
+  , hiSpindles :: Double  -- ^ Auto-computed from policy spindle_ratio
+                          -- and the node spindle count
+  , instSpindles :: Double -- ^ Spindles used by instances
+  , offline  :: Bool      -- ^ Whether the node should not be used for
+                          -- allocations and skipped from score
+                          -- computations
+  , utilPool :: T.DynUtil -- ^ Total utilisation capacity
+  , utilLoad :: T.DynUtil -- ^ Sum of instance utilisation
+  , pTags    :: TagMap    -- ^ Map of primary instance tags and their count
+  , group    :: T.Gdx     -- ^ The node's group (index)
+  , iPolicy  :: T.IPolicy -- ^ The instance policy (of the node's group)
+  } deriving (Show, Read, Eq)
 
 instance T.Element Node where
-    nameOf = name
-    idxOf = idx
-    setAlias = setAlias
-    setIdx = setIdx
-    allNames n = [name n, alias n]
+  nameOf = name
+  idxOf = idx
+  setAlias = setAlias
+  setIdx = setIdx
+  allNames n = [name n, alias n]
 
 -- | A simple name for the int, node association list.
 type AssocList = [(T.Ndx, Node)]
@@ -160,8 +165,8 @@ addTags = foldl' addTag
 -- | Adjust or delete a tag from a tagmap.
 delTag :: TagMap -> String -> TagMap
 delTag t s = Map.update (\v -> if v > 1
-                               then Just (v-1)
-                               else Nothing)
+                                 then Just (v-1)
+                                 else Nothing)
              s t
 
 -- | Remove multiple tags.
@@ -180,6 +185,18 @@ rejectAddTags t = any (`Map.member` t)
 conflictingPrimaries :: Node -> Int
 conflictingPrimaries (Node { pTags = t }) = Foldable.sum t - Map.size t
 
+-- | Helper function to increment a base value depending on the passed
+-- boolean argument.
+incIf :: (Num a) => Bool -> a -> a -> a
+incIf True  base delta = base + delta
+incIf False base _     = base
+
+-- | Helper function to decrement a base value depending on the passed
+-- boolean argument.
+decIf :: (Num a) => Bool -> a -> a -> a
+decIf True  base delta = base - delta
+decIf False base _     = base
+
 -- * Initialization functions
 
 -- | Create a new node.
@@ -187,47 +204,56 @@ conflictingPrimaries (Node { pTags = t }) = Foldable.sum t - Map.size t
 -- The index and the peers maps are empty, and will be need to be
 -- update later via the 'setIdx' and 'buildPeers' functions.
 create :: String -> Double -> Int -> Int -> Double
-       -> Int -> Double -> Bool -> T.Gdx -> Node
+       -> Int -> Double -> Bool -> Int -> T.Gdx -> Node
 create name_init mem_t_init mem_n_init mem_f_init
-       dsk_t_init dsk_f_init cpu_t_init offline_init group_init =
-    Node { name = name_init
-         , alias = name_init
-         , tMem = mem_t_init
-         , nMem = mem_n_init
-         , fMem = mem_f_init
-         , tDsk = dsk_t_init
-         , fDsk = dsk_f_init
-         , tCpu = cpu_t_init
-         , uCpu = 0
-         , pList = []
-         , sList = []
-         , failN1 = True
-         , idx = -1
-         , peers = P.empty
-         , rMem = 0
-         , pMem = fromIntegral mem_f_init / mem_t_init
-         , pDsk = fromIntegral dsk_f_init / dsk_t_init
-         , pRem = 0
-         , pCpu = 0
-         , offline = offline_init
-         , xMem = 0
-         , mDsk = T.defReservedDiskRatio
-         , mCpu = T.defVcpuRatio
-         , loDsk = mDskToloDsk T.defReservedDiskRatio dsk_t_init
-         , hiCpu = mCpuTohiCpu T.defVcpuRatio cpu_t_init
-         , utilPool = T.baseUtil
-         , utilLoad = T.zeroUtil
-         , pTags = Map.empty
-         , group = group_init
-         }
+       dsk_t_init dsk_f_init cpu_t_init offline_init spindles_init
+       group_init =
+  Node { name = name_init
+       , alias = name_init
+       , tMem = mem_t_init
+       , nMem = mem_n_init
+       , fMem = mem_f_init
+       , tDsk = dsk_t_init
+       , fDsk = dsk_f_init
+       , tCpu = cpu_t_init
+       , spindleCount = spindles_init
+       , uCpu = 0
+       , pList = []
+       , sList = []
+       , failN1 = True
+       , idx = -1
+       , peers = P.empty
+       , rMem = 0
+       , pMem = fromIntegral mem_f_init / mem_t_init
+       , pDsk = computePDsk dsk_f_init dsk_t_init
+       , pRem = 0
+       , pCpu = 0
+       , offline = offline_init
+       , xMem = 0
+       , mDsk = T.defReservedDiskRatio
+       , loDsk = mDskToloDsk T.defReservedDiskRatio dsk_t_init
+       , hiCpu = mCpuTohiCpu (T.iPolicyVcpuRatio T.defIPolicy) cpu_t_init
+       , hiSpindles = computeHiSpindles (T.iPolicySpindleRatio T.defIPolicy)
+                      spindles_init
+       , instSpindles = 0
+       , utilPool = T.baseUtil
+       , utilLoad = T.zeroUtil
+       , pTags = Map.empty
+       , group = group_init
+       , iPolicy = T.defIPolicy
+       }
 
 -- | Conversion formula from mDsk\/tDsk to loDsk.
 mDskToloDsk :: Double -> Double -> Int
-mDskToloDsk mval tdsk = floor (mval * tdsk)
+mDskToloDsk mval = floor . (mval *)
 
 -- | Conversion formula from mCpu\/tCpu to hiCpu.
 mCpuTohiCpu :: Double -> Double -> Int
-mCpuTohiCpu mval tcpu = floor (mval * tcpu)
+mCpuTohiCpu mval = floor . (mval *)
+
+-- | Conversiojn formula from spindles and spindle ratio to hiSpindles.
+computeHiSpindles :: Double -> Int -> Double
+computeHiSpindles spindle_ratio = (spindle_ratio *) . fromIntegral
 
 -- | Changes the index.
 --
@@ -253,9 +279,21 @@ setXmem t val = t { xMem = val }
 setMdsk :: Node -> Double -> Node
 setMdsk t val = t { mDsk = val, loDsk = mDskToloDsk val (tDsk t) }
 
--- | Sets the max cpu usage ratio.
+-- | Sets the max cpu usage ratio. This will update the node's
+-- ipolicy, losing sharing (but it should be a seldomly done operation).
 setMcpu :: Node -> Double -> Node
-setMcpu t val = t { mCpu = val, hiCpu = mCpuTohiCpu val (tCpu t) }
+setMcpu t val =
+  let new_ipol = (iPolicy t) { T.iPolicyVcpuRatio = val }
+  in t { hiCpu = mCpuTohiCpu val (tCpu t), iPolicy = new_ipol }
+
+-- | Sets the policy.
+setPolicy :: T.IPolicy -> Node -> Node
+setPolicy pol node =
+  node { iPolicy = pol
+       , hiCpu = mCpuTohiCpu (T.iPolicyVcpuRatio pol) (tCpu node)
+       , hiSpindles = computeHiSpindles (T.iPolicySpindleRatio pol)
+                      (spindleCount node)
+       }
 
 -- | Computes the maximum reserved memory for peers from a peer map.
 computeMaxRes :: P.PeerMap -> P.Elem
@@ -264,18 +302,23 @@ computeMaxRes = P.maxElem
 -- | Builds the peer map for a given node.
 buildPeers :: Node -> Instance.List -> Node
 buildPeers t il =
-    let mdata = map
-                (\i_idx -> let inst = Container.find i_idx il
-                               mem = if Instance.autoBalance inst
+  let mdata = map
+              (\i_idx -> let inst = Container.find i_idx il
+                             mem = if Instance.usesSecMem inst
                                      then Instance.mem inst
                                      else 0
-                           in (Instance.pNode inst, mem))
-                (sList t)
-        pmap = P.accumArray (+) mdata
-        new_rmem = computeMaxRes pmap
-        new_failN1 = fMem t <= new_rmem
-        new_prem = fromIntegral new_rmem / tMem t
-    in t {peers=pmap, failN1 = new_failN1, rMem = new_rmem, pRem = new_prem}
+                         in (Instance.pNode inst, mem))
+              (sList t)
+      pmap = P.accumArray (+) mdata
+      new_rmem = computeMaxRes pmap
+      new_failN1 = fMem t <= new_rmem
+      new_prem = fromIntegral new_rmem / tMem t
+  in t {peers=pmap, failN1 = new_failN1, rMem = new_rmem, pRem = new_prem}
+
+-- | Calculate the new spindle usage
+calcSpindleUse :: Node -> Instance.Instance -> Double
+calcSpindleUse n i = incIf (Instance.usesLocalStorage i) (instSpindles n)
+                       (fromIntegral $ Instance.spindleUse i)
 
 -- | Assigns an instance to a node as primary and update the used VCPU
 -- count, utilisation data and tags map.
@@ -285,76 +328,89 @@ setPri t inst = t { pList = Instance.idx inst:pList t
                   , pCpu = fromIntegral new_count / tCpu t
                   , utilLoad = utilLoad t `T.addUtil` Instance.util inst
                   , pTags = addTags (pTags t) (Instance.tags inst)
+                  , instSpindles = calcSpindleUse t inst
                   }
-    where new_count = uCpu t + Instance.vcpus inst
+  where new_count = Instance.applyIfOnline inst (+ Instance.vcpus inst)
+                    (uCpu t )
 
 -- | Assigns an instance to a node as secondary without other updates.
 setSec :: Node -> Instance.Instance -> Node
 setSec t inst = t { sList = Instance.idx inst:sList t
                   , utilLoad = old_load { T.dskWeight = T.dskWeight old_load +
                                           T.dskWeight (Instance.util inst) }
+                  , instSpindles = calcSpindleUse t inst
                   }
-    where old_load = utilLoad t
+  where old_load = utilLoad t
+
+-- | Computes the new 'pDsk' value, handling nodes without local disk
+-- storage (we consider all their disk used).
+computePDsk :: Int -> Double -> Double
+computePDsk _    0     = 1
+computePDsk used total = fromIntegral used / total
 
 -- * Update functions
 
 -- | Sets the free memory.
 setFmem :: Node -> Int -> Node
 setFmem t new_mem =
-    let new_n1 = new_mem <= rMem t
-        new_mp = fromIntegral new_mem / tMem t
-    in t { fMem = new_mem, failN1 = new_n1, pMem = new_mp }
+  let new_n1 = new_mem <= rMem t
+      new_mp = fromIntegral new_mem / tMem t
+  in t { fMem = new_mem, failN1 = new_n1, pMem = new_mp }
 
 -- | Removes a primary instance.
 removePri :: Node -> Instance.Instance -> Node
 removePri t inst =
-    let iname = Instance.idx inst
-        new_plist = delete iname (pList t)
-        new_mem = fMem t + Instance.mem inst
-        new_dsk = fDsk t + Instance.dsk inst
-        new_mp = fromIntegral new_mem / tMem t
-        new_dp = fromIntegral new_dsk / tDsk t
-        new_failn1 = new_mem <= rMem t
-        new_ucpu = uCpu t - Instance.vcpus inst
-        new_rcpu = fromIntegral new_ucpu / tCpu t
-        new_load = utilLoad t `T.subUtil` Instance.util inst
-    in t { pList = new_plist, fMem = new_mem, fDsk = new_dsk
-         , failN1 = new_failn1, pMem = new_mp, pDsk = new_dp
-         , uCpu = new_ucpu, pCpu = new_rcpu, utilLoad = new_load
-         , pTags = delTags (pTags t) (Instance.tags inst) }
+  let iname = Instance.idx inst
+      i_online = Instance.notOffline inst
+      uses_disk = Instance.usesLocalStorage inst
+      new_plist = delete iname (pList t)
+      new_mem = incIf i_online (fMem t) (Instance.mem inst)
+      new_dsk = incIf uses_disk (fDsk t) (Instance.dsk inst)
+      new_spindles = decIf uses_disk (instSpindles t) 1
+      new_mp = fromIntegral new_mem / tMem t
+      new_dp = computePDsk new_dsk (tDsk t)
+      new_failn1 = new_mem <= rMem t
+      new_ucpu = decIf i_online (uCpu t) (Instance.vcpus inst)
+      new_rcpu = fromIntegral new_ucpu / tCpu t
+      new_load = utilLoad t `T.subUtil` Instance.util inst
+  in t { pList = new_plist, fMem = new_mem, fDsk = new_dsk
+       , failN1 = new_failn1, pMem = new_mp, pDsk = new_dp
+       , uCpu = new_ucpu, pCpu = new_rcpu, utilLoad = new_load
+       , pTags = delTags (pTags t) (Instance.tags inst)
+       , instSpindles = new_spindles
+       }
 
 -- | Removes a secondary instance.
 removeSec :: Node -> Instance.Instance -> Node
 removeSec t inst =
-    let iname = Instance.idx inst
-        uses_disk = Instance.usesLocalStorage inst
-        cur_dsk = fDsk t
-        pnode = Instance.pNode inst
-        new_slist = delete iname (sList t)
-        new_dsk = if uses_disk
-                  then cur_dsk + Instance.dsk inst
-                  else cur_dsk
-        old_peers = peers t
-        old_peem = P.find pnode old_peers
-        new_peem =  if Instance.autoBalance inst
-                    then old_peem - Instance.mem inst
-                    else old_peem
-        new_peers = if new_peem > 0
+  let iname = Instance.idx inst
+      uses_disk = Instance.usesLocalStorage inst
+      cur_dsk = fDsk t
+      pnode = Instance.pNode inst
+      new_slist = delete iname (sList t)
+      new_dsk = incIf uses_disk cur_dsk (Instance.dsk inst)
+      new_spindles = decIf uses_disk (instSpindles t) 1
+      old_peers = peers t
+      old_peem = P.find pnode old_peers
+      new_peem = decIf (Instance.usesSecMem inst) old_peem (Instance.mem inst)
+      new_peers = if new_peem > 0
                     then P.add pnode new_peem old_peers
                     else P.remove pnode old_peers
-        old_rmem = rMem t
-        new_rmem = if old_peem < old_rmem
+      old_rmem = rMem t
+      new_rmem = if old_peem < old_rmem
                    then old_rmem
                    else computeMaxRes new_peers
-        new_prem = fromIntegral new_rmem / tMem t
-        new_failn1 = fMem t <= new_rmem
-        new_dp = fromIntegral new_dsk / tDsk t
-        old_load = utilLoad t
-        new_load = old_load { T.dskWeight = T.dskWeight old_load -
-                                            T.dskWeight (Instance.util inst) }
-    in t { sList = new_slist, fDsk = new_dsk, peers = new_peers
-         , failN1 = new_failn1, rMem = new_rmem, pDsk = new_dp
-         , pRem = new_prem, utilLoad = new_load }
+      new_prem = fromIntegral new_rmem / tMem t
+      new_failn1 = fMem t <= new_rmem
+      new_dp = computePDsk new_dsk (tDsk t)
+      old_load = utilLoad t
+      new_load = old_load { T.dskWeight = T.dskWeight old_load -
+                                          T.dskWeight (Instance.util inst) }
+  in t { sList = new_slist, fDsk = new_dsk, peers = new_peers
+       , failN1 = new_failn1, rMem = new_rmem, pDsk = new_dp
+       , pRem = new_prem, utilLoad = new_load
+       , instSpindles = new_spindles
+       }
 
 -- | Adds a primary instance (basic version).
 addPri :: Node -> Instance.Instance -> T.OpResult Node
@@ -371,38 +427,42 @@ addPriEx :: Bool               -- ^ Whether to override the N+1 and
                                -- either the new version of the node
                                -- or a failure mode
 addPriEx force t inst =
-    let iname = Instance.idx inst
-        uses_disk = Instance.usesLocalStorage inst
-        cur_dsk = fDsk t
-        new_mem = fMem t - Instance.mem inst
-        new_dsk = if uses_disk
-                  then cur_dsk - Instance.dsk inst
-                  else cur_dsk
-        new_failn1 = new_mem <= rMem t
-        new_ucpu = uCpu t + Instance.vcpus inst
-        new_pcpu = fromIntegral new_ucpu / tCpu t
-        new_dp = fromIntegral new_dsk / tDsk t
-        l_cpu = mCpu t
-        new_load = utilLoad t `T.addUtil` Instance.util inst
-        inst_tags = Instance.tags inst
-        old_tags = pTags t
-        strict = not force
-    in case () of
-         _ | new_mem <= 0 -> T.OpFail T.FailMem
-           | uses_disk && new_dsk <= 0 -> T.OpFail T.FailDisk
-           | uses_disk && mDsk t > new_dp && strict -> T.OpFail T.FailDisk
-           | new_failn1 && not (failN1 t) && strict -> T.OpFail T.FailMem
-           | l_cpu >= 0 && l_cpu < new_pcpu && strict -> T.OpFail T.FailCPU
-           | rejectAddTags old_tags inst_tags -> T.OpFail T.FailTags
-           | otherwise ->
-               let new_plist = iname:pList t
-                   new_mp = fromIntegral new_mem / tMem t
-                   r = t { pList = new_plist, fMem = new_mem, fDsk = new_dsk
-                         , failN1 = new_failn1, pMem = new_mp, pDsk = new_dp
-                         , uCpu = new_ucpu, pCpu = new_pcpu
-                         , utilLoad = new_load
-                         , pTags = addTags old_tags inst_tags }
-               in T.OpGood r
+  let iname = Instance.idx inst
+      i_online = Instance.notOffline inst
+      uses_disk = Instance.usesLocalStorage inst
+      cur_dsk = fDsk t
+      new_mem = decIf i_online (fMem t) (Instance.mem inst)
+      new_dsk = decIf uses_disk cur_dsk (Instance.dsk inst)
+      new_spindles = incIf uses_disk (instSpindles t) 1
+      new_failn1 = new_mem <= rMem t
+      new_ucpu = incIf i_online (uCpu t) (Instance.vcpus inst)
+      new_pcpu = fromIntegral new_ucpu / tCpu t
+      new_dp = computePDsk new_dsk (tDsk t)
+      l_cpu = T.iPolicyVcpuRatio $ iPolicy t
+      new_load = utilLoad t `T.addUtil` Instance.util inst
+      inst_tags = Instance.tags inst
+      old_tags = pTags t
+      strict = not force
+  in case () of
+       _ | new_mem <= 0 -> T.OpFail T.FailMem
+         | uses_disk && new_dsk <= 0 -> T.OpFail T.FailDisk
+         | uses_disk && mDsk t > new_dp && strict -> T.OpFail T.FailDisk
+         | uses_disk && new_spindles > hiSpindles t
+             && strict -> T.OpFail T.FailDisk
+         | new_failn1 && not (failN1 t) && strict -> T.OpFail T.FailMem
+         | l_cpu >= 0 && l_cpu < new_pcpu && strict -> T.OpFail T.FailCPU
+         | rejectAddTags old_tags inst_tags -> T.OpFail T.FailTags
+         | otherwise ->
+           let new_plist = iname:pList t
+               new_mp = fromIntegral new_mem / tMem t
+               r = t { pList = new_plist, fMem = new_mem, fDsk = new_dsk
+                     , failN1 = new_failn1, pMem = new_mp, pDsk = new_dp
+                     , uCpu = new_ucpu, pCpu = new_pcpu
+                     , utilLoad = new_load
+                     , pTags = addTags old_tags inst_tags
+                     , instSpindles = new_spindles
+                     }
+           in T.OpGood r
 
 -- | Adds a secondary instance (basic version).
 addSec :: Node -> Instance.Instance -> T.Ndx -> T.OpResult Node
@@ -411,45 +471,49 @@ addSec = addSecEx False
 -- | Adds a secondary instance (extended version).
 addSecEx :: Bool -> Node -> Instance.Instance -> T.Ndx -> T.OpResult Node
 addSecEx force t inst pdx =
-    let iname = Instance.idx inst
-        old_peers = peers t
-        old_mem = fMem t
-        new_dsk = fDsk t - Instance.dsk inst
-        secondary_needed_mem = if Instance.autoBalance inst
+  let iname = Instance.idx inst
+      old_peers = peers t
+      old_mem = fMem t
+      new_dsk = fDsk t - Instance.dsk inst
+      new_spindles = instSpindles t + 1
+      secondary_needed_mem = if Instance.usesSecMem inst
                                then Instance.mem inst
                                else 0
-        new_peem = P.find pdx old_peers + secondary_needed_mem
-        new_peers = P.add pdx new_peem old_peers
-        new_rmem = max (rMem t) new_peem
-        new_prem = fromIntegral new_rmem / tMem t
-        new_failn1 = old_mem <= new_rmem
-        new_dp = fromIntegral new_dsk / tDsk t
-        old_load = utilLoad t
-        new_load = old_load { T.dskWeight = T.dskWeight old_load +
-                                            T.dskWeight (Instance.util inst) }
-        strict = not force
-    in case () of
-         _ | not (Instance.hasSecondary inst) -> T.OpFail T.FailDisk
-           | new_dsk <= 0 -> T.OpFail T.FailDisk
-           | mDsk t > new_dp && strict -> T.OpFail T.FailDisk
-           | secondary_needed_mem >= old_mem && strict -> T.OpFail T.FailMem
-           | new_failn1 && not (failN1 t) && strict -> T.OpFail T.FailMem
-           | otherwise ->
-               let new_slist = iname:sList t
-                   r = t { sList = new_slist, fDsk = new_dsk
-                         , peers = new_peers, failN1 = new_failn1
-                         , rMem = new_rmem, pDsk = new_dp
-                         , pRem = new_prem, utilLoad = new_load }
-               in T.OpGood r
+      new_peem = P.find pdx old_peers + secondary_needed_mem
+      new_peers = P.add pdx new_peem old_peers
+      new_rmem = max (rMem t) new_peem
+      new_prem = fromIntegral new_rmem / tMem t
+      new_failn1 = old_mem <= new_rmem
+      new_dp = computePDsk new_dsk (tDsk t)
+      old_load = utilLoad t
+      new_load = old_load { T.dskWeight = T.dskWeight old_load +
+                                          T.dskWeight (Instance.util inst) }
+      strict = not force
+  in case () of
+       _ | not (Instance.hasSecondary inst) -> T.OpFail T.FailDisk
+         | new_dsk <= 0 -> T.OpFail T.FailDisk
+         | mDsk t > new_dp && strict -> T.OpFail T.FailDisk
+         | new_spindles > hiSpindles t && strict -> T.OpFail T.FailDisk
+         | secondary_needed_mem >= old_mem && strict -> T.OpFail T.FailMem
+         | new_failn1 && not (failN1 t) && strict -> T.OpFail T.FailMem
+         | otherwise ->
+           let new_slist = iname:sList t
+               r = t { sList = new_slist, fDsk = new_dsk
+                     , peers = new_peers, failN1 = new_failn1
+                     , rMem = new_rmem, pDsk = new_dp
+                     , pRem = new_prem, utilLoad = new_load
+                     , instSpindles = new_spindles
+                     }
+           in T.OpGood r
 
 -- * Stats functions
 
 -- | Computes the amount of available disk on a given node.
 availDisk :: Node -> Int
 availDisk t =
-    let _f = fDsk t
-        _l = loDsk t
-    in if _f < _l
+  let _f = fDsk t
+      _l = loDsk t
+  in if _f < _l
        then 0
        else _f - _l
 
@@ -460,18 +524,18 @@ iDsk t = truncate (tDsk t) - fDsk t
 -- | Computes the amount of available memory on a given node.
 availMem :: Node -> Int
 availMem t =
-    let _f = fMem t
-        _l = rMem t
-    in if _f < _l
+  let _f = fMem t
+      _l = rMem t
+  in if _f < _l
        then 0
        else _f - _l
 
 -- | Computes the amount of available memory on a given node.
 availCpu :: Node -> Int
 availCpu t =
-    let _u = uCpu t
-        _l = hiCpu t
-    in if _l >= _u
+  let _u = uCpu t
+      _l = hiCpu t
+  in if _l >= _u
        then _l - _u
        else 0
 
@@ -486,91 +550,96 @@ showField :: Node   -- ^ Node which we're querying
           -> String -- ^ Field name
           -> String -- ^ Field value as string
 showField t field =
-    case field of
-      "idx"  -> printf "%4d" $ idx t
-      "name" -> alias t
-      "fqdn" -> name t
-      "status" -> case () of
-                    _ | offline t -> "-"
-                      | failN1 t -> "*"
-                      | otherwise -> " "
-      "tmem" -> printf "%5.0f" $ tMem t
-      "nmem" -> printf "%5d" $ nMem t
-      "xmem" -> printf "%5d" $ xMem t
-      "fmem" -> printf "%5d" $ fMem t
-      "imem" -> printf "%5d" $ iMem t
-      "rmem" -> printf "%5d" $ rMem t
-      "amem" -> printf "%5d" $ fMem t - rMem t
-      "tdsk" -> printf "%5.0f" $ tDsk t / 1024
-      "fdsk" -> printf "%5d" $ fDsk t `div` 1024
-      "tcpu" -> printf "%4.0f" $ tCpu t
-      "ucpu" -> printf "%4d" $ uCpu t
-      "pcnt" -> printf "%3d" $ length (pList t)
-      "scnt" -> printf "%3d" $ length (sList t)
-      "plist" -> show $ pList t
-      "slist" -> show $ sList t
-      "pfmem" -> printf "%6.4f" $ pMem t
-      "pfdsk" -> printf "%6.4f" $ pDsk t
-      "rcpu"  -> printf "%5.2f" $ pCpu t
-      "cload" -> printf "%5.3f" uC
-      "mload" -> printf "%5.3f" uM
-      "dload" -> printf "%5.3f" uD
-      "nload" -> printf "%5.3f" uN
-      "ptags" -> intercalate "," . map (uncurry (printf "%s=%d")) .
-                 Map.toList $ pTags t
-      "peermap" -> show $ peers t
-      _ -> T.unknownField
-    where
-      T.DynUtil { T.cpuWeight = uC, T.memWeight = uM,
-                  T.dskWeight = uD, T.netWeight = uN } = utilLoad t
+  case field of
+    "idx"  -> printf "%4d" $ idx t
+    "name" -> alias t
+    "fqdn" -> name t
+    "status" -> case () of
+                  _ | offline t -> "-"
+                    | failN1 t -> "*"
+                    | otherwise -> " "
+    "tmem" -> printf "%5.0f" $ tMem t
+    "nmem" -> printf "%5d" $ nMem t
+    "xmem" -> printf "%5d" $ xMem t
+    "fmem" -> printf "%5d" $ fMem t
+    "imem" -> printf "%5d" $ iMem t
+    "rmem" -> printf "%5d" $ rMem t
+    "amem" -> printf "%5d" $ fMem t - rMem t
+    "tdsk" -> printf "%5.0f" $ tDsk t / 1024
+    "fdsk" -> printf "%5d" $ fDsk t `div` 1024
+    "tcpu" -> printf "%4.0f" $ tCpu t
+    "ucpu" -> printf "%4d" $ uCpu t
+    "pcnt" -> printf "%3d" $ length (pList t)
+    "scnt" -> printf "%3d" $ length (sList t)
+    "plist" -> show $ pList t
+    "slist" -> show $ sList t
+    "pfmem" -> printf "%6.4f" $ pMem t
+    "pfdsk" -> printf "%6.4f" $ pDsk t
+    "rcpu"  -> printf "%5.2f" $ pCpu t
+    "cload" -> printf "%5.3f" uC
+    "mload" -> printf "%5.3f" uM
+    "dload" -> printf "%5.3f" uD
+    "nload" -> printf "%5.3f" uN
+    "ptags" -> intercalate "," . map (uncurry (printf "%s=%d")) .
+               Map.toList $ pTags t
+    "peermap" -> show $ peers t
+    "spindle_count" -> show $ spindleCount t
+    "hi_spindles" -> show $ hiSpindles t
+    "inst_spindles" -> show $ instSpindles t
+    _ -> T.unknownField
+  where
+    T.DynUtil { T.cpuWeight = uC, T.memWeight = uM,
+                T.dskWeight = uD, T.netWeight = uN } = utilLoad t
 
 -- | Returns the header and numeric propery of a field.
 showHeader :: String -> (String, Bool)
 showHeader field =
-    case field of
-      "idx" -> ("Index", True)
-      "name" -> ("Name", False)
-      "fqdn" -> ("Name", False)
-      "status" -> ("F", False)
-      "tmem" -> ("t_mem", True)
-      "nmem" -> ("n_mem", True)
-      "xmem" -> ("x_mem", True)
-      "fmem" -> ("f_mem", True)
-      "imem" -> ("i_mem", True)
-      "rmem" -> ("r_mem", True)
-      "amem" -> ("a_mem", True)
-      "tdsk" -> ("t_dsk", True)
-      "fdsk" -> ("f_dsk", True)
-      "tcpu" -> ("pcpu", True)
-      "ucpu" -> ("vcpu", True)
-      "pcnt" -> ("pcnt", True)
-      "scnt" -> ("scnt", True)
-      "plist" -> ("primaries", True)
-      "slist" -> ("secondaries", True)
-      "pfmem" -> ("p_fmem", True)
-      "pfdsk" -> ("p_fdsk", True)
-      "rcpu"  -> ("r_cpu", True)
-      "cload" -> ("lCpu", True)
-      "mload" -> ("lMem", True)
-      "dload" -> ("lDsk", True)
-      "nload" -> ("lNet", True)
-      "ptags" -> ("PrimaryTags", False)
-      "peermap" -> ("PeerMap", False)
-      -- TODO: add node fields (group.uuid, group)
-      _ -> (T.unknownField, False)
+  case field of
+    "idx" -> ("Index", True)
+    "name" -> ("Name", False)
+    "fqdn" -> ("Name", False)
+    "status" -> ("F", False)
+    "tmem" -> ("t_mem", True)
+    "nmem" -> ("n_mem", True)
+    "xmem" -> ("x_mem", True)
+    "fmem" -> ("f_mem", True)
+    "imem" -> ("i_mem", True)
+    "rmem" -> ("r_mem", True)
+    "amem" -> ("a_mem", True)
+    "tdsk" -> ("t_dsk", True)
+    "fdsk" -> ("f_dsk", True)
+    "tcpu" -> ("pcpu", True)
+    "ucpu" -> ("vcpu", True)
+    "pcnt" -> ("pcnt", True)
+    "scnt" -> ("scnt", True)
+    "plist" -> ("primaries", True)
+    "slist" -> ("secondaries", True)
+    "pfmem" -> ("p_fmem", True)
+    "pfdsk" -> ("p_fdsk", True)
+    "rcpu"  -> ("r_cpu", True)
+    "cload" -> ("lCpu", True)
+    "mload" -> ("lMem", True)
+    "dload" -> ("lDsk", True)
+    "nload" -> ("lNet", True)
+    "ptags" -> ("PrimaryTags", False)
+    "peermap" -> ("PeerMap", False)
+    "spindle_count" -> ("NodeSpindles", True)
+    "hi_spindles" -> ("MaxSpindles", True)
+    "inst_spindles" -> ("InstSpindles", True)
+    -- TODO: add node fields (group.uuid, group)
+    _ -> (T.unknownField, False)
 
 -- | String converter for the node list functionality.
 list :: [String] -> Node -> [String]
 list fields t = map (showField t) fields
 
-
 -- | Constant holding the fields we're displaying by default.
 defaultFields :: [String]
 defaultFields =
-    [ "status", "name", "tmem", "nmem", "imem", "xmem", "fmem"
-    , "rmem", "tdsk", "fdsk", "tcpu", "ucpu", "pcnt", "scnt"
-    , "pfmem", "pfdsk", "rcpu"
-    , "cload", "mload", "dload", "nload" ]
+  [ "status", "name", "tmem", "nmem", "imem", "xmem", "fmem"
+  , "rmem", "tdsk", "fdsk", "tcpu", "ucpu", "pcnt", "scnt"
+  , "pfmem", "pfdsk", "rcpu"
+  , "cload", "mload", "dload", "nload" ]
 
 -- | Split a list of nodes into a list of (node group UUID, list of
 -- associated nodes).
index 2d17d2a..f178578 100644 (file)
@@ -8,7 +8,7 @@ implementation should be easy in case it's needed.
 
 {-
 
-Copyright (C) 2009 Google Inc.
+Copyright (C) 2009, 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
@@ -28,16 +28,16 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 -}
 
 module Ganeti.HTools.PeerMap
-    ( PeerMap
-    , Key
-    , Elem
-    , empty
-    , accumArray
-    , Ganeti.HTools.PeerMap.find
-    , add
-    , remove
-    , maxElem
-    ) where
+  ( PeerMap
+  , Key
+  , Elem
+  , empty
+  , accumArray
+  , Ganeti.HTools.PeerMap.find
+  , add
+  , remove
+  , maxElem
+  ) where
 
 import Data.Maybe (fromMaybe)
 import Data.List
@@ -70,9 +70,9 @@ pmCompare a b = comparing snd b a
 -- | Add or update (via a custom function) an element.
 addWith :: (Elem -> Elem -> Elem) -> Key -> Elem -> PeerMap -> PeerMap
 addWith fn k v lst =
-    case lookup k lst of
-      Nothing -> insertBy pmCompare (k, v) lst
-      Just o -> insertBy pmCompare (k, fn o v) (remove k lst)
+  case lookup k lst of
+    Nothing -> insertBy pmCompare (k, v) lst
+    Just o -> insertBy pmCompare (k, fn o v) (remove k lst)
 
 -- | Create a PeerMap from an association list, with possible duplicates.
 accumArray :: (Elem -> Elem -> Elem) -- ^ function used to merge the elements
diff --git a/htools/Ganeti/HTools/Program.hs b/htools/Ganeti/HTools/Program.hs
new file mode 100644 (file)
index 0000000..75870e6
--- /dev/null
@@ -0,0 +1,47 @@
+{-| Small module holding program definitions.
+
+-}
+
+{-
+
+Copyright (C) 2011, 2012 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.Program
+  ( personalities
+  ) where
+
+import Ganeti.HTools.CLI (OptType, Options)
+
+import qualified Ganeti.HTools.Program.Hail as Hail
+import qualified Ganeti.HTools.Program.Hbal as Hbal
+import qualified Ganeti.HTools.Program.Hcheck as Hcheck
+import qualified Ganeti.HTools.Program.Hscan as Hscan
+import qualified Ganeti.HTools.Program.Hspace as Hspace
+import qualified Ganeti.HTools.Program.Hinfo as Hinfo
+
+-- | Supported binaries.
+personalities :: [(String, (Options -> [String] -> IO (), [OptType]))]
+personalities = [ ("hail",   (Hail.main,   Hail.options))
+                , ("hbal",   (Hbal.main,   Hbal.options))
+                , ("hcheck", (Hcheck.main, Hcheck.options))
+                , ("hscan",  (Hscan.main,  Hscan.options))
+                , ("hspace", (Hspace.main, Hspace.options))
+                , ("hinfo",  (Hinfo.main,  Hinfo.options))
+                ]
index 1fb9b80..4fc016a 100644 (file)
@@ -4,7 +4,7 @@
 
 {-
 
-Copyright (C) 2009, 2010, 2011 Google Inc.
+Copyright (C) 2009, 2010, 2011, 2012 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
@@ -23,42 +23,55 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 
 -}
 
-module Ganeti.HTools.Program.Hail (main) where
+module Ganeti.HTools.Program.Hail (main, options) where
 
 import Control.Monad
+import Data.Maybe (fromMaybe, isJust)
 import System.IO
-import qualified System
+import System.Exit
 
 import qualified Ganeti.HTools.Cluster as Cluster
 
 import Ganeti.HTools.CLI
 import Ganeti.HTools.IAlloc
 import Ganeti.HTools.Loader (Request(..), ClusterData(..))
-import Ganeti.HTools.ExtLoader (maybeSaveData)
+import Ganeti.HTools.ExtLoader (maybeSaveData, loadExternalData)
 
 -- | Options list and functions.
 options :: [OptType]
 options =
-    [ oPrintNodes
-    , oSaveCluster
-    , oDataFile
-    , oNodeSim
-    , oVerbose
-    , oShowVer
-    , oShowHelp
-    ]
+  [ oPrintNodes
+  , oSaveCluster
+  , oDataFile
+  , oNodeSim
+  , oVerbose
+  , oShowVer
+  , oShowHelp
+  ]
+
+wrapReadRequest :: Options -> [String] -> IO Request
+wrapReadRequest opts args = do
+  when (null args) $ do
+    hPutStrLn stderr "Error: this program needs an input file."
+    exitWith $ ExitFailure 1
+
+  r1 <- readRequest (head args)
+  if isJust (optDataFile opts) ||  (not . null . optNodeSim) opts
+    then do
+      cdata <- loadExternalData opts
+      let Request rqt _ = r1
+      return $ Request rqt cdata
+    else return r1
 
--- | Main function.
-main :: IO ()
-main = do
-  cmd_args <- System.getArgs
-  (opts, args) <- parseOpts cmd_args "hail" options
 
+-- | Main function.
+main :: Options -> [String] -> IO ()
+main opts args = do
   let shownodes = optShowNodes opts
       verbose = optVerbose opts
       savecluster = optSaveCluster opts
 
-  request <- readRequest opts args
+  request <- wrapReadRequest opts args
 
   let Request rq cdata = request
 
@@ -74,7 +87,7 @@ main = do
   maybeSaveData savecluster "pre-ialloc" "before iallocator run" cdata
 
   let (maybe_ni, resp) = runIAllocator request
-      (fin_nl, fin_il) = maybe (cdNodes cdata, cdInstances cdata) id maybe_ni
+      (fin_nl, fin_il) = fromMaybe (cdNodes cdata, cdInstances cdata) maybe_ni
   putStrLn resp
 
   maybePrintNodes shownodes "Final cluster" (Cluster.printNodes fin_nl)
index 77d3834..8c4136f 100644 (file)
@@ -4,7 +4,7 @@
 
 {-
 
-Copyright (C) 2009, 2010, 2011 Google Inc.
+Copyright (C) 2009, 2010, 2011, 2012 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
@@ -23,7 +23,11 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 
 -}
 
-module Ganeti.HTools.Program.Hbal (main) where
+module Ganeti.HTools.Program.Hbal
+    ( main
+    , options
+    , iterateDepth
+    ) where
 
 import Control.Concurrent (threadDelay)
 import Control.Exception (bracket)
@@ -31,11 +35,10 @@ import Control.Monad
 import Data.List
 import Data.Maybe (isJust, isNothing, fromJust)
 import Data.IORef
-import System (exitWith, ExitCode(..))
+import System.Exit
 import System.IO
 import System.Posix.Process
 import System.Posix.Signals
-import qualified System
 
 import Text.Printf (printf, hPrintf)
 
@@ -57,87 +60,103 @@ import Ganeti.Jobs
 -- | Options list and functions.
 options :: [OptType]
 options =
-    [ oPrintNodes
-    , oPrintInsts
-    , oPrintCommands
-    , oOneline
-    , oDataFile
-    , oEvacMode
-    , oRapiMaster
-    , oLuxiSocket
-    , oExecJobs
-    , oGroup
-    , oMaxSolLength
-    , oVerbose
-    , oQuiet
-    , oOfflineNode
-    , oMinScore
-    , oMaxCpu
-    , oMinDisk
-    , oMinGain
-    , oMinGainLim
-    , oDiskMoves
-    , oSelInst
-    , oInstMoves
-    , oDynuFile
-    , oExTags
-    , oExInst
-    , oSaveCluster
-    , oShowVer
-    , oShowHelp
-    ]
+  [ oPrintNodes
+  , oPrintInsts
+  , oPrintCommands
+  , oDataFile
+  , oEvacMode
+  , oRapiMaster
+  , oLuxiSocket
+  , oIAllocSrc
+  , oExecJobs
+  , oGroup
+  , oMaxSolLength
+  , oVerbose
+  , oQuiet
+  , oOfflineNode
+  , oMinScore
+  , oMaxCpu
+  , oMinDisk
+  , oMinGain
+  , oMinGainLim
+  , oDiskMoves
+  , oSelInst
+  , oInstMoves
+  , oDynuFile
+  , oExTags
+  , oExInst
+  , oSaveCluster
+  , oShowVer
+  , oShowHelp
+  ]
 
 {- | Start computing the solution at the given depth and recurse until
 we find a valid solution or we exceed the maximum depth.
 
 -}
-iterateDepth :: Cluster.Table    -- ^ The starting table
+iterateDepth :: Bool             -- ^ Whether to print moves
+             -> Cluster.Table    -- ^ The starting table
              -> Int              -- ^ Remaining length
              -> Bool             -- ^ Allow disk moves
              -> Bool             -- ^ Allow instance moves
              -> Int              -- ^ Max node name len
              -> Int              -- ^ Max instance name len
              -> [MoveJob]        -- ^ Current command list
-             -> Bool             -- ^ Whether to be silent
              -> Score            -- ^ Score at which to stop
              -> Score            -- ^ Min gain limit
              -> Score            -- ^ Min score gain
              -> Bool             -- ^ Enable evacuation mode
              -> IO (Cluster.Table, [MoveJob]) -- ^ The resulting table
                                               -- and commands
-iterateDepth ini_tbl max_rounds disk_moves inst_moves nmlen imlen
-             cmd_strs oneline min_score mg_limit min_gain evac_mode =
-    let Cluster.Table ini_nl ini_il _ _ = ini_tbl
-        allowed_next = Cluster.doNextBalance ini_tbl max_rounds min_score
-        m_fin_tbl = if allowed_next
+iterateDepth printmove ini_tbl max_rounds disk_moves inst_moves nmlen imlen
+             cmd_strs min_score mg_limit min_gain evac_mode =
+  let Cluster.Table ini_nl ini_il _ _ = ini_tbl
+      allowed_next = Cluster.doNextBalance ini_tbl max_rounds min_score
+      m_fin_tbl = if allowed_next
                     then Cluster.tryBalance ini_tbl disk_moves inst_moves
                          evac_mode mg_limit min_gain
                     else Nothing
-    in
-      case m_fin_tbl of
-        Just fin_tbl ->
-            do
-              let
-                  (Cluster.Table _ _ _ fin_plc) = fin_tbl
-                  fin_plc_len = length fin_plc
-                  cur_plc@(idx, _, _, move, _) = head fin_plc
-                  (sol_line, cmds) = Cluster.printSolutionLine ini_nl ini_il
-                                     nmlen imlen cur_plc fin_plc_len
-                  afn = Cluster.involvedNodes ini_il cur_plc
-                  upd_cmd_strs = (afn, idx, move, cmds):cmd_strs
-              unless oneline $ do
-                       putStrLn sol_line
-                       hFlush stdout
-              iterateDepth fin_tbl max_rounds disk_moves inst_moves
-                           nmlen imlen upd_cmd_strs oneline min_score
-                           mg_limit min_gain evac_mode
-        Nothing -> return (ini_tbl, cmd_strs)
-
--- | Formats the solution for the oneline display.
-formatOneline :: Double -> Int -> Double -> String
-formatOneline ini_cv plc_len fin_cv =
-    printf "%.8f %d %.8f %8.3f" ini_cv plc_len fin_cv
-               (if fin_cv == 0 then 1 else ini_cv / fin_cv)
+  in case m_fin_tbl of
+       Just fin_tbl ->
+         do
+           let (Cluster.Table _ _ _ fin_plc) = fin_tbl
+               fin_plc_len = length fin_plc
+               cur_plc@(idx, _, _, move, _) = head fin_plc
+               (sol_line, cmds) = Cluster.printSolutionLine ini_nl ini_il
+                                  nmlen imlen cur_plc fin_plc_len
+               afn = Cluster.involvedNodes ini_il cur_plc
+               upd_cmd_strs = (afn, idx, move, cmds):cmd_strs
+           when printmove $ do
+               putStrLn sol_line
+               hFlush stdout
+           iterateDepth printmove fin_tbl max_rounds disk_moves inst_moves
+                        nmlen imlen upd_cmd_strs min_score
+                        mg_limit min_gain evac_mode
+       Nothing -> return (ini_tbl, cmd_strs)
+
+-- | Displays the cluster stats.
+printStats :: Node.List -> Node.List -> IO ()
+printStats ini_nl fin_nl = do
+  let ini_cs = Cluster.totalResources ini_nl
+      fin_cs = Cluster.totalResources fin_nl
+  printf "Original: mem=%d disk=%d\n"
+             (Cluster.csFmem ini_cs) (Cluster.csFdsk ini_cs) :: IO ()
+  printf "Final:    mem=%d disk=%d\n"
+             (Cluster.csFmem fin_cs) (Cluster.csFdsk fin_cs)
+
+-- | Saves the rebalance commands to a text file.
+saveBalanceCommands :: Options -> String -> IO ()
+saveBalanceCommands opts cmd_data = do
+  let out_path = fromJust $ optShowCmds opts
+  putStrLn ""
+  if out_path == "-"
+    then printf "Commands to run to reach the above solution:\n%s"
+           (unlines . map ("  " ++) .
+            filter (/= "  check") .
+            lines $ cmd_data)
+    else do
+      writeFile out_path (shTemplate ++ cmd_data)
+      printf "The commands have been written to file '%s'\n" out_path
 
 -- | Polls a set of jobs at a fixed interval until all are finished
 -- one way or another.
@@ -159,16 +178,16 @@ checkJobsStatus = all (== JOB_STATUS_SUCCESS)
 
 -- | Wrapper over execJobSet checking for early termination.
 execWrapper :: String -> Node.List
-           -> Instance.List -> IORef Int -> [JobSet] -> IO Bool
+            -> Instance.List -> IORef Int -> [JobSet] -> IO Bool
 execWrapper _      _  _  _    [] = return True
 execWrapper master nl il cref alljss = do
   cancel <- readIORef cref
-  (if cancel > 0
-   then do
-     hPrintf stderr "Exiting early due to user request, %d\
-                    \ jobset(s) remaining." (length alljss)::IO ()
-     return False
-   else execJobSet master nl il cref alljss)
+  if cancel > 0
+    then do
+      hPrintf stderr "Exiting early due to user request, %d\
+                     \ jobset(s) remaining." (length alljss)::IO ()
+      return False
+    else execJobSet master nl il cref alljss
 
 -- | Execute an entire jobset.
 execJobSet :: String -> Node.List
@@ -189,17 +208,33 @@ execJobSet master nl il cref (js:jss) = do
                 putStrLn $ "Got job IDs " ++ commaJoin x
                 waitForJobs client x
          )
-  (case jrs of
-     Bad x -> do
-       hPutStrLn stderr $ "Cannot compute job status, aborting: " ++ show x
-       return False
-     Ok x -> if checkJobsStatus x
-             then execWrapper master nl il cref jss
-             else do
-               hPutStrLn stderr $ "Not all jobs completed successfully: " ++
-                         show x
-               hPutStrLn stderr "Aborting."
-               return False)
+  case jrs of
+    Bad x -> do
+      hPutStrLn stderr $ "Cannot compute job status, aborting: " ++ show x
+      return False
+    Ok x -> if checkJobsStatus x
+              then execWrapper master nl il cref jss
+              else do
+                hPutStrLn stderr $ "Not all jobs completed successfully: " ++
+                          show x
+                hPutStrLn stderr "Aborting."
+                return False
+
+-- | Executes the jobs, if possible and desired.
+maybeExecJobs :: Options
+              -> [a]
+              -> Node.List
+              -> Instance.List
+              -> [JobSet]
+              -> IO Bool
+maybeExecJobs opts ord_plc fin_nl il cmd_jobs =
+  if optExecJobs opts && not (null ord_plc)
+    then (case optLuxi opts of
+            Nothing -> do
+              hPutStrLn stderr "Execution of commands possible only on LUXI"
+              return False
+            Just master -> runJobSet master fin_nl il cmd_jobs)
+    else return True
 
 -- | Signal handler for graceful termination.
 hangleSigInt :: IORef Int -> IO ()
@@ -224,61 +259,10 @@ runJobSet master fin_nl il cmd_jobs = do
     [(hangleSigTerm, softwareTermination), (hangleSigInt, keyboardSignal)]
   execWrapper master fin_nl il cref cmd_jobs
 
--- | Main function.
-main :: IO ()
-main = do
-  cmd_args <- System.getArgs
-  (opts, args) <- parseOpts cmd_args "hbal" options
-
-  unless (null args) $ do
-         hPutStrLn stderr "Error: this program doesn't take any arguments."
-         exitWith $ ExitFailure 1
-
-  let oneline = optOneline opts
-      verbose = optVerbose opts
-      shownodes = optShowNodes opts
-      showinsts = optShowInsts opts
-
-  ini_cdata@(ClusterData gl fixed_nl ilf ctags) <- loadExternalData opts
-
-  let offline_passed = optOffline opts
-      all_nodes = Container.elems fixed_nl
-      offline_lkp = map (lookupName (map Node.name all_nodes)) offline_passed
-      offline_wrong = filter (not . goodLookupResult) offline_lkp
-      offline_names = map lrContent offline_lkp
-      offline_indices = map Node.idx $
-                        filter (\n -> Node.name n `elem` offline_names)
-                               all_nodes
-      m_cpu = optMcpu opts
-      m_dsk = optMdsk opts
-      csf = commonSuffix fixed_nl ilf
-
-  when (not (null offline_wrong)) $ do
-         hPrintf stderr "Error: Wrong node name(s) set as offline: %s\n"
-                     (commaJoin (map lrContent offline_wrong)) :: IO ()
-         exitWith $ ExitFailure 1
-
-  let nm = Container.map (\n -> if Node.idx n `elem` offline_indices
-                                then Node.setOffline n True
-                                else n) fixed_nl
-      nlf = Container.map (flip Node.setMdsk m_dsk . flip Node.setMcpu m_cpu)
-            nm
-
-  when (not oneline && verbose > 1) $
-       putStrLn $ "Loaded cluster tags: " ++ intercalate "," ctags
-
-  when (Container.size ilf == 0) $ do
-         (if oneline then putStrLn $ formatOneline 0 0 0
-          else printf "Cluster is empty, exiting.\n")
-         exitWith ExitSuccess
-
-  let split_insts = Cluster.findSplitInstances nlf ilf
-  unless (null split_insts) $ do
-    hPutStrLn stderr "Found instances belonging to multiple node groups:"
-    mapM_ (\i -> hPutStrLn stderr $ "  " ++ Instance.name i) split_insts
-    hPutStrLn stderr "Aborting."
-    exitWith $ ExitFailure 1
-
+-- | Select the target node group.
+selectGroup :: Options -> Group.List -> Node.List -> Instance.List
+            -> IO (String, (Node.List, Instance.List))
+selectGroup opts gl nlf ilf = do
   let ngroups = Cluster.splitCluster nlf ilf
   when (length ngroups > 1 && isNothing (optGroup opts)) $ do
     hPutStrLn stderr "Found multiple node groups:"
@@ -287,17 +271,11 @@ main = do
     hPutStrLn stderr "Aborting."
     exitWith $ ExitFailure 1
 
-  maybeSaveData (optSaveCluster opts) "original" "before balancing" ini_cdata
-
-  unless oneline $ printf "Loaded %d nodes, %d instances\n"
-             (Container.size nlf)
-             (Container.size ilf)
-
-  (gname, (nl, il)) <- case optGroup opts of
+  case optGroup opts of
     Nothing -> do
-         let (gidx, cdata) = head ngroups
-             grp = Container.find gidx gl
-         return (Group.name grp, cdata)
+      let (gidx, cdata) = head ngroups
+          grp = Container.find gidx gl
+      return (Group.name grp, cdata)
     Just g -> case Container.findByName gl g of
       Nothing -> do
         hPutStrLn stderr $ "Node group " ++ g ++
@@ -307,30 +285,89 @@ main = do
         exitWith $ ExitFailure 1
       Just grp ->
           case lookup (Group.idx grp) ngroups of
-            Nothing -> do
+            Nothing ->
               -- This will only happen if there are no nodes assigned
               -- to this group
               return (Group.name grp, (Container.empty, Container.empty))
             Just cdata -> return (Group.name grp, cdata)
 
-  unless oneline $ printf "Group size %d nodes, %d instances\n"
-             (Container.size nl)
-             (Container.size il)
+-- | Do a few checks on the cluster data.
+checkCluster :: Int -> Node.List -> Instance.List -> IO ()
+checkCluster verbose nl il = do
+  -- nothing to do on an empty cluster
+  when (Container.null il) $ do
+         printf "Cluster is empty, exiting.\n"::IO ()
+         exitWith ExitSuccess
 
-  putStrLn $ "Selected node group: " ++ gname
+  -- hbal doesn't currently handle split clusters
+  let split_insts = Cluster.findSplitInstances nl il
+  unless (null split_insts || verbose <= 1) $ do
+    hPutStrLn stderr "Found instances belonging to multiple node groups:"
+    mapM_ (\i -> hPutStrLn stderr $ "  " ++ Instance.name i) split_insts
+    hPutStrLn stderr "These instances will not be moved."
+
+  printf "Loaded %d nodes, %d instances\n"
+             (Container.size nl)
+             (Container.size il)::IO ()
 
-  when (length csf > 0 && not oneline && verbose > 1) $
+  let csf = commonSuffix nl il
+  when (not (null csf) && verbose > 1) $
        printf "Note: Stripping common suffix of '%s' from names\n" csf
 
+-- | Do a few checks on the selected group data.
+checkGroup :: Int -> String -> Node.List -> Instance.List -> IO ()
+checkGroup verbose gname nl il = do
+  printf "Group size %d nodes, %d instances\n"
+             (Container.size nl)
+             (Container.size il)::IO ()
+
+  putStrLn $ "Selected node group: " ++ gname
+
   let (bad_nodes, bad_instances) = Cluster.computeBadItems nl il
-  unless (oneline || verbose == 0) $ printf
+  unless (verbose == 0) $ printf
              "Initial check done: %d bad nodes, %d bad instances.\n"
              (length bad_nodes) (length bad_instances)
 
-  when (length bad_nodes > 0) $
+  when (not (null bad_nodes)) $
          putStrLn "Cluster is not N+1 happy, continuing but no guarantee \
                   \that the cluster will end N+1 happy."
 
+-- | Check that we actually need to rebalance.
+checkNeedRebalance :: Options -> Score -> IO ()
+checkNeedRebalance opts ini_cv = do
+  let min_cv = optMinScore opts
+  when (ini_cv < min_cv) $ do
+         printf "Cluster is already well balanced (initial score %.6g,\n\
+                \minimum score %.6g).\nNothing to do, exiting\n"
+                ini_cv min_cv:: IO ()
+         exitWith ExitSuccess
+
+-- | Main function.
+main :: Options -> [String] -> IO ()
+main opts args = do
+  unless (null args) $ do
+         hPutStrLn stderr "Error: this program doesn't take any arguments."
+         exitWith $ ExitFailure 1
+
+  let verbose = optVerbose opts
+      shownodes = optShowNodes opts
+      showinsts = optShowInsts opts
+
+  ini_cdata@(ClusterData gl fixed_nl ilf ctags ipol) <- loadExternalData opts
+
+  when (verbose > 1) $ do
+       putStrLn $ "Loaded cluster tags: " ++ intercalate "," ctags
+       putStrLn $ "Loaded cluster ipolicy: " ++ show ipol
+
+  nlf <- setNodeStatus opts fixed_nl
+  checkCluster verbose nlf ilf
+
+  maybeSaveData (optSaveCluster opts) "original" "before balancing" ini_cdata
+
+  (gname, (nl, il)) <- selectGroup opts gl nlf ilf
+
+  checkGroup verbose gname nl il
+
   maybePrintInsts showinsts "Initial" (Cluster.printInsts nl il)
 
   maybePrintNodes shownodes "Initial cluster" (Cluster.printNodes nl)
@@ -339,28 +376,21 @@ main = do
       ini_tbl = Cluster.Table nl il ini_cv []
       min_cv = optMinScore opts
 
-  when (ini_cv < min_cv) $ do
-         (if oneline then
-              putStrLn $ formatOneline ini_cv 0 ini_cv
-          else printf "Cluster is already well balanced (initial score %.6g,\n\
-                      \minimum score %.6g).\nNothing to do, exiting\n"
-                      ini_cv min_cv)
-         exitWith ExitSuccess
+  checkNeedRebalance opts ini_cv
 
-  unless oneline (if verbose > 2 then
-                      printf "Initial coefficients: overall %.8f, %s\n"
-                      ini_cv (Cluster.printStats nl)
-                  else
-                      printf "Initial score: %.8f\n" ini_cv)
+  if verbose > 2
+    then printf "Initial coefficients: overall %.8f\n%s"
+           ini_cv (Cluster.printStats "  " nl)::IO ()
+    else printf "Initial score: %.8f\n" ini_cv
 
-  unless oneline $ putStrLn "Trying to minimize the CV..."
+  putStrLn "Trying to minimize the CV..."
   let imlen = maximum . map (length . Instance.alias) $ Container.elems il
       nmlen = maximum . map (length . Node.alias) $ Container.elems nl
 
-  (fin_tbl, cmd_strs) <- iterateDepth ini_tbl (optMaxLength opts)
+  (fin_tbl, cmd_strs) <- iterateDepth True ini_tbl (optMaxLength opts)
                          (optDiskMoves opts)
                          (optInstMoves opts)
-                         nmlen imlen [] oneline min_cv
+                         nmlen imlen [] min_cv
                          (optMinGainLim opts) (optMinGain opts)
                          (optEvacMode opts)
   let (Cluster.Table fin_nl fin_il fin_cv fin_plc) = fin_tbl
@@ -368,56 +398,30 @@ main = do
       sol_msg = case () of
                   _ | null fin_plc -> printf "No solution found\n"
                     | verbose > 2 ->
-                        printf "Final coefficients:   overall %.8f, %s\n"
-                        fin_cv (Cluster.printStats fin_nl)
+                        printf "Final coefficients:   overall %.8f\n%s"
+                        fin_cv (Cluster.printStats "  " fin_nl)
                     | otherwise ->
                         printf "Cluster score improved from %.8f to %.8f\n"
                         ini_cv fin_cv ::String
 
-  unless oneline $ putStr sol_msg
+  putStr sol_msg
 
-  unless (oneline || verbose == 0) $
+  unless (verbose == 0) $
          printf "Solution length=%d\n" (length ord_plc)
 
   let cmd_jobs = Cluster.splitJobs cmd_strs
-      cmd_data = Cluster.formatCmds cmd_jobs
 
   when (isJust $ optShowCmds opts) $
-       do
-         let out_path = fromJust $ optShowCmds opts
-         putStrLn ""
-         (if out_path == "-" then
-              printf "Commands to run to reach the above solution:\n%s"
-                     (unlines . map ("  " ++) .
-                      filter (/= "  check") .
-                      lines $ cmd_data)
-          else do
-            writeFile out_path (shTemplate ++ cmd_data)
-            printf "The commands have been written to file '%s'\n" out_path)
+       saveBalanceCommands opts $ Cluster.formatCmds cmd_jobs
 
   maybeSaveData (optSaveCluster opts) "balanced" "after balancing"
-                (ClusterData gl fin_nl fin_il ctags)
+                ini_cdata { cdNodes = fin_nl, cdInstances = fin_il }
 
   maybePrintInsts showinsts "Final" (Cluster.printInsts fin_nl fin_il)
 
   maybePrintNodes shownodes "Final cluster" (Cluster.printNodes fin_nl)
 
-  when (verbose > 3) $ do
-         let ini_cs = Cluster.totalResources nl
-             fin_cs = Cluster.totalResources fin_nl
-         printf "Original: mem=%d disk=%d\n"
-                    (Cluster.csFmem ini_cs) (Cluster.csFdsk ini_cs) :: IO ()
-         printf "Final:    mem=%d disk=%d\n"
-                    (Cluster.csFmem fin_cs) (Cluster.csFdsk fin_cs)
-  when oneline $
-         putStrLn $ formatOneline ini_cv (length ord_plc) fin_cv
-
-  eval <-
-      if optExecJobs opts && not (null ord_plc)
-      then (case optLuxi opts of
-              Nothing -> do
-                hPutStrLn stderr "Execution of commands possible only on LUXI"
-                return False
-              Just master -> runJobSet master fin_nl il cmd_jobs)
-      else return True
+  when (verbose > 3) $ printStats nl fin_nl
+
+  eval <- maybeExecJobs opts ord_plc fin_nl il cmd_jobs
   unless eval (exitWith (ExitFailure 1))
diff --git a/htools/Ganeti/HTools/Program/Hcheck.hs b/htools/Ganeti/HTools/Program/Hcheck.hs
new file mode 100644 (file)
index 0000000..b322ce7
--- /dev/null
@@ -0,0 +1,338 @@
+{-| Cluster checker.
+
+-}
+
+{-
+
+Copyright (C) 2012 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 Gene52al 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.Program.Hcheck (main, options) where
+
+import Control.Monad
+import Data.List (transpose)
+import System.Exit
+import System.IO
+import Text.Printf (printf)
+
+import qualified Ganeti.HTools.Container as Container
+import qualified Ganeti.HTools.Cluster as Cluster
+import qualified Ganeti.HTools.Group as Group
+import qualified Ganeti.HTools.Node as Node
+import qualified Ganeti.HTools.Instance as Instance
+
+import qualified Ganeti.HTools.Program.Hbal as Hbal
+
+import Ganeti.HTools.CLI
+import Ganeti.HTools.ExtLoader
+import Ganeti.HTools.Loader
+import Ganeti.HTools.Types
+
+-- | Options list and functions.
+options :: [OptType]
+options =
+  [ oDataFile
+  , oDiskMoves
+  , oDynuFile
+  , oEvacMode
+  , oExInst
+  , oExTags
+  , oIAllocSrc
+  , oInstMoves
+  , oLuxiSocket
+  , oMachineReadable
+  , oMaxCpu
+  , oMaxSolLength
+  , oMinDisk
+  , oMinGain
+  , oMinGainLim
+  , oMinScore
+  , oNoSimulation
+  , oOfflineNode
+  , oQuiet
+  , oRapiMaster
+  , oSelInst
+  , oShowHelp
+  , oShowVer
+  , oVerbose
+  ]
+
+-- | Check phase - are we before (initial) or after rebalance.
+data Phase = Initial
+           | Rebalanced
+
+-- | Level of presented statistics.
+data Level = GroupLvl
+           | ClusterLvl
+
+-- | A type alias for a group index and node\/instance lists.
+type GroupInfo = (Gdx, (Node.List, Instance.List))
+
+-- | A type alias for group stats.
+type GroupStats = ((Group.Group, Double), [Int])
+
+-- | Prefix for machine readable names.
+htcPrefix :: String
+htcPrefix = "HCHECK"
+
+-- | Data showed both per group and per cluster.
+commonData :: [(String, String)]
+commonData =[ ("N1_FAIL", "Nodes not N+1 happy")
+            , ("CONFLICT_TAGS", "Nodes with conflicting instances")
+            , ("OFFLINE_PRI", "Instances having the primary node offline")
+            , ("OFFLINE_SEC", "Instances having a secondary node offline")
+            ]
+
+-- | Data showed per group.
+groupData :: [(String, String)]
+groupData = commonData ++ [("SCORE", "Group score")]
+
+-- | Data showed per cluster.
+clusterData :: [(String, String)]
+clusterData = commonData ++
+              [ ("NEED_REBALANCE", "Cluster is not healthy") ]
+
+-- | Phase-specific prefix for machine readable version.
+phasePrefix :: Phase -> String
+phasePrefix Initial = "INIT"
+phasePrefix Rebalanced = "FINAL"
+
+-- | Level-specific prefix for machine readable version.
+levelPrefix :: Level -> String
+levelPrefix GroupLvl = "GROUP"
+levelPrefix ClusterLvl = "CLUSTER"
+
+-- | Machine-readable keys to show depending on given level.
+keysData :: Level -> [String]
+keysData GroupLvl = map fst groupData
+keysData ClusterLvl = map fst clusterData
+
+-- | Description of phases for human readable version.
+phaseDescr :: Phase -> String
+phaseDescr Initial = "initially"
+phaseDescr Rebalanced = "after rebalancing"
+
+-- | Description to show depending on given level.
+descrData :: Level -> [String]
+descrData GroupLvl = map snd groupData
+descrData ClusterLvl = map snd clusterData
+
+-- | Human readable prefix for statistics.
+phaseLevelDescr :: Phase -> Level -> Maybe String -> String
+phaseLevelDescr phase GroupLvl (Just name) =
+    printf "Statistics for group %s %s\n" name $ phaseDescr phase
+phaseLevelDescr phase GroupLvl Nothing =
+    printf "Statistics for group %s\n" $ phaseDescr phase
+phaseLevelDescr phase ClusterLvl _ =
+    printf "Cluster statistics %s\n" $ phaseDescr phase
+
+-- | Format a list of key, value as a shell fragment.
+printKeysHTC :: [(String, String)] -> IO ()
+printKeysHTC = printKeys htcPrefix
+
+-- | Prepare string from boolean value.
+printBool :: Bool    -- ^ Whether the result should be machine readable
+          -> Bool    -- ^ Value to be converted to string
+          -> String
+printBool True True = "1"
+printBool True False = "0"
+printBool False b = show b
+
+-- | Print mapping from group idx to group uuid (only in machine
+-- readable mode).
+printGroupsMappings :: Group.List -> IO ()
+printGroupsMappings gl = do
+    let extract_vals = \g -> (printf "GROUP_UUID_%d" $ Group.idx g :: String,
+                              Group.uuid g)
+        printpairs = map extract_vals (Container.elems gl)
+    printKeysHTC printpairs
+
+-- | Prepare a single key given a certain level and phase of simulation.
+prepareKey :: Level -> Phase -> Maybe String -> String -> String
+prepareKey level phase Nothing suffix =
+  printf "%s_%s_%s" (phasePrefix phase) (levelPrefix level) suffix
+prepareKey level phase (Just idx) suffix =
+  printf "%s_%s_%s_%s" (phasePrefix phase) (levelPrefix level) idx suffix
+
+-- | Print all the statistics for given level and phase.
+printStats :: Int            -- ^ Verbosity level
+           -> Bool           -- ^ If the output should be machine readable
+           -> Level          -- ^ Level on which we are printing
+           -> Phase          -- ^ Current phase of simulation
+           -> [String]       -- ^ Values to print
+           -> Maybe String   -- ^ Additional data for groups
+           -> IO ()
+printStats _ True level phase values gidx = do
+  let keys = map (prepareKey level phase gidx) (keysData level)
+  printKeysHTC $ zip keys values
+
+printStats verbose False level phase values name = do
+  let prefix = phaseLevelDescr phase level name
+      descr = descrData level
+  unless (verbose == 0) $ do
+    putStrLn ""
+    putStr prefix
+    mapM_ (\(a,b) -> printf "    %s: %s\n" a b) (zip descr values)
+
+-- | Extract name or idx from group.
+extractGroupData :: Bool -> Group.Group -> String
+extractGroupData True grp = show $ Group.idx grp
+extractGroupData False grp = Group.name grp
+
+-- | Prepare values for group.
+prepareGroupValues :: [Int] -> Double -> [String]
+prepareGroupValues stats score =
+  map show stats ++ [printf "%.8f" score]
+
+-- | Prepare values for cluster.
+prepareClusterValues :: Bool -> [Int] -> [Bool] -> [String]
+prepareClusterValues machineread stats bstats =
+  map show stats ++ map (printBool machineread) bstats
+
+-- | Print all the statistics on a group level.
+printGroupStats :: Int -> Bool -> Phase -> GroupStats -> IO ()
+printGroupStats verbose machineread phase ((grp, score), stats) = do
+  let values = prepareGroupValues stats score
+      extradata = extractGroupData machineread grp
+  printStats verbose machineread GroupLvl phase values (Just extradata)
+
+-- | Print all the statistics on a cluster (global) level.
+printClusterStats :: Int -> Bool -> Phase -> [Int] -> Bool -> IO ()
+printClusterStats verbose machineread phase stats needhbal = do
+  let values = prepareClusterValues machineread stats [needhbal]
+  printStats verbose machineread ClusterLvl phase values Nothing
+
+-- | Check if any of cluster metrics is non-zero.
+clusterNeedsRebalance :: [Int] -> Bool
+clusterNeedsRebalance stats = sum stats > 0
+
+{- | Check group for N+1 hapiness, conflicts of primaries on nodes and
+instances residing on offline nodes.
+
+-}
+perGroupChecks :: Group.List -> GroupInfo -> GroupStats
+perGroupChecks gl (gidx, (nl, il)) =
+  let grp = Container.find gidx gl
+      offnl = filter Node.offline (Container.elems nl)
+      n1violated = length $ fst $ Cluster.computeBadItems nl il
+      conflicttags = length $ filter (>0)
+                     (map Node.conflictingPrimaries (Container.elems nl))
+      offline_pri = sum . map length $ map Node.pList offnl
+      offline_sec = length $ map Node.sList offnl
+      score = Cluster.compCV nl
+      groupstats = [ n1violated
+                   , conflicttags
+                   , offline_pri
+                   , offline_sec
+                   ]
+  in ((grp, score), groupstats)
+
+-- | Use Hbal's iterateDepth to simulate group rebalance.
+executeSimulation :: Options -> Cluster.Table -> Double
+                  -> Gdx -> Node.List -> Instance.List
+                  -> IO GroupInfo
+executeSimulation opts ini_tbl min_cv gidx nl il = do
+  let imlen = maximum . map (length . Instance.alias) $ Container.elems il
+      nmlen = maximum . map (length . Node.alias) $ Container.elems nl
+
+  (fin_tbl, _) <- Hbal.iterateDepth False ini_tbl
+                                    (optMaxLength opts)
+                                    (optDiskMoves opts)
+                                    (optInstMoves opts)
+                                    nmlen imlen [] min_cv
+                                    (optMinGainLim opts) (optMinGain opts)
+                                    (optEvacMode opts)
+
+  let (Cluster.Table fin_nl fin_il _ _) = fin_tbl
+  return (gidx, (fin_nl, fin_il))
+
+-- | Simulate group rebalance if group's score is not good
+maybeSimulateGroupRebalance :: Options -> GroupInfo -> IO GroupInfo
+maybeSimulateGroupRebalance opts (gidx, (nl, il)) = do
+  let ini_cv = Cluster.compCV nl
+      ini_tbl = Cluster.Table nl il ini_cv []
+      min_cv = optMinScore opts
+  if ini_cv < min_cv
+    then return (gidx, (nl, il))
+    else executeSimulation opts ini_tbl min_cv gidx nl il
+
+-- | Decide whether to simulate rebalance.
+maybeSimulateRebalance :: Bool             -- ^ Whether to simulate rebalance
+                       -> Options          -- ^ Command line options
+                       -> [GroupInfo]      -- ^ Group data
+                       -> IO [GroupInfo]
+maybeSimulateRebalance True opts cluster =
+    mapM (maybeSimulateGroupRebalance opts) cluster
+maybeSimulateRebalance False _ cluster = return cluster
+
+-- | Prints the final @OK@ marker in machine readable output.
+printFinalHTC :: Bool -> IO ()
+printFinalHTC = printFinal htcPrefix
+
+-- | Main function.
+main :: Options -> [String] -> IO ()
+main opts args = do
+  unless (null args) $ do
+         hPutStrLn stderr "Error: this program doesn't take any arguments."
+         exitWith $ ExitFailure 1
+
+  let verbose = optVerbose opts
+      machineread = optMachineReadable opts
+      nosimulation = optNoSimulation opts
+
+  (ClusterData gl fixed_nl ilf _ _) <- loadExternalData opts
+  nlf <- setNodeStatus opts fixed_nl
+
+  let splitcluster = Cluster.splitCluster nlf ilf
+
+  when machineread $ printGroupsMappings gl
+
+  let groupsstats = map (perGroupChecks gl) splitcluster
+      clusterstats = map sum . transpose . map snd $ groupsstats
+      needrebalance = clusterNeedsRebalance clusterstats
+
+  unless (verbose == 0 || machineread) $
+    if nosimulation
+      then putStrLn "Running in no-simulation mode."
+      else if needrebalance
+             then putStrLn "Cluster needs rebalancing."
+             else putStrLn "No need to rebalance cluster, no problems found."
+
+  mapM_ (printGroupStats verbose machineread Initial) groupsstats
+
+  printClusterStats verbose machineread Initial clusterstats needrebalance
+
+  let exitOK = nosimulation || not needrebalance
+      simulate = not nosimulation && needrebalance
+
+  rebalancedcluster <- maybeSimulateRebalance simulate opts splitcluster
+
+  when (simulate || machineread) $ do
+    let newgroupstats = map (perGroupChecks gl) rebalancedcluster
+        newclusterstats = map sum . transpose . map snd $ newgroupstats
+        newneedrebalance = clusterNeedsRebalance clusterstats
+
+    mapM_ (printGroupStats verbose machineread Rebalanced) newgroupstats
+
+    printClusterStats verbose machineread Rebalanced newclusterstats
+                           newneedrebalance
+
+  printFinalHTC machineread
+
+  unless exitOK $ exitWith $ ExitFailure 1
diff --git a/htools/Ganeti/HTools/Program/Hinfo.hs b/htools/Ganeti/HTools/Program/Hinfo.hs
new file mode 100644 (file)
index 0000000..548443b
--- /dev/null
@@ -0,0 +1,164 @@
+{-| Cluster information printer.
+
+-}
+
+{-
+
+Copyright (C) 2012 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.Program.Hinfo (main, options) where
+
+import Control.Monad
+import Data.List
+import System.Exit
+import System.IO
+
+import Text.Printf (printf)
+
+import qualified Ganeti.HTools.Container as Container
+import qualified Ganeti.HTools.Cluster as Cluster
+import qualified Ganeti.HTools.Node as Node
+import qualified Ganeti.HTools.Group as Group
+import qualified Ganeti.HTools.Instance as Instance
+
+import Ganeti.HTools.Utils
+import Ganeti.HTools.CLI
+import Ganeti.HTools.ExtLoader
+import Ganeti.HTools.Loader
+
+-- | Options list and functions.
+options :: [OptType]
+options =
+  [ oPrintNodes
+  , oPrintInsts
+  , oDataFile
+  , oRapiMaster
+  , oLuxiSocket
+  , oIAllocSrc
+  , oVerbose
+  , oQuiet
+  , oOfflineNode
+  , oShowVer
+  , oShowHelp
+  ]
+
+-- | Node group statistics.
+calcGroupInfo :: Group.Group
+              -> Node.List
+              -> Instance.List
+              -> (String, (Int, Int), (Int, Int), Bool)
+calcGroupInfo g nl il =
+  let nl_size                    = Container.size nl
+      il_size                    = Container.size il
+      (bad_nodes, bad_instances) = Cluster.computeBadItems nl il
+      bn_size                    = length bad_nodes
+      bi_size                    = length bad_instances
+      n1h                        = bn_size == 0
+  in (Group.name g, (nl_size, il_size), (bn_size, bi_size), n1h)
+
+-- | Helper to format one group row result.
+groupRowFormatHelper :: (String, (Int, Int), (Int, Int), Bool) -> [String]
+groupRowFormatHelper (gname, (nl_size, il_size), (bn_size, bi_size), n1h) =
+  [ gname
+  , printf "%d" nl_size
+  , printf "%d" il_size
+  , printf "%d" bn_size
+  , printf "%d" bi_size
+  , show n1h ]
+
+-- | Print node group information.
+showGroupInfo :: Int -> Group.List -> Node.List -> Instance.List -> IO ()
+showGroupInfo verbose gl nl il = do
+  let cgrs   = map (\(gdx, (gnl, gil)) ->
+                 calcGroupInfo (Container.find gdx gl) gnl gil) $
+                 Cluster.splitCluster nl il
+      cn1h   = all (\(_, _, _, n1h) -> n1h) cgrs
+      grs    = map groupRowFormatHelper cgrs
+      header = ["Group", "Nodes", "Instances", "Bad_Nodes", "Bad_Instances",
+                "N+1"]
+
+  when (verbose > 1) $
+    printf "Node group information:\n%s"
+           (printTable "  " header grs [False, True, True, True, True, False])
+
+  printf "Cluster is N+1 %s\n" $ if cn1h then "happy" else "unhappy"
+
+-- | Gather and print split instances.
+splitInstancesInfo :: Int -> Node.List -> Instance.List -> IO ()
+splitInstancesInfo verbose nl il = do
+  let split_insts = Cluster.findSplitInstances nl il
+  if (null split_insts)
+    then
+      when (verbose > 1) $ do
+        putStrLn "No split instances found"::IO ()
+    else do
+      putStrLn "Found instances belonging to multiple node groups:"
+      mapM_ (\i -> hPutStrLn stderr $ "  " ++ Instance.name i) split_insts
+
+-- | Print common (interesting) information.
+commonInfo :: Int -> Group.List -> Node.List -> Instance.List -> IO ()
+commonInfo verbose gl nl il = do
+  when (Container.null il && verbose > 1) $ do
+         printf "Cluster is empty.\n"::IO ()
+
+  let nl_size = (Container.size nl)
+      il_size = (Container.size il)
+      gl_size = (Container.size gl)
+  printf "Loaded %d %s, %d %s, %d %s\n"
+             nl_size (plural nl_size "node" "nodes")
+             il_size (plural il_size "instance" "instances")
+             gl_size (plural gl_size "node group" "node groups")::IO ()
+
+  let csf = commonSuffix nl il
+  when (not (null csf) && verbose > 2) $
+       printf "Note: Stripping common suffix of '%s' from names\n" csf
+
+-- | Main function.
+main :: Options -> [String] -> IO ()
+main opts args = do
+  unless (null args) $ do
+         hPutStrLn stderr "Error: this program doesn't take any arguments."
+         exitWith $ ExitFailure 1
+
+  let verbose = optVerbose opts
+      shownodes = optShowNodes opts
+      showinsts = optShowInsts opts
+
+  (ClusterData gl fixed_nl ilf ctags ipol) <- loadExternalData opts
+
+  putStrLn $ "Loaded cluster tags: " ++ intercalate "," ctags
+
+  when (verbose > 2) $ do
+       putStrLn $ "Loaded cluster ipolicy: " ++ show ipol
+
+  nlf <- setNodeStatus opts fixed_nl
+
+  commonInfo verbose gl nlf ilf
+
+  splitInstancesInfo verbose nlf ilf
+
+  showGroupInfo verbose gl nlf ilf
+
+  maybePrintInsts showinsts "Instances" (Cluster.printInsts nlf ilf)
+
+  maybePrintNodes shownodes "Cluster" (Cluster.printNodes nlf)
+
+  printf "Cluster coefficients:\n%s" (Cluster.printStats "  " nlf)::IO ()
+  printf "Cluster score: %.8f\n" (Cluster.compCV nlf)
index 0dbcf6e..9a8feda 100644 (file)
@@ -4,7 +4,7 @@
 
 {-
 
-Copyright (C) 2009, 2010, 2011 Google Inc.
+Copyright (C) 2009, 2010, 2011, 2012 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
@@ -23,14 +23,13 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 
 -}
 
-module Ganeti.HTools.Program.Hscan (main) where
+module Ganeti.HTools.Program.Hscan (main, options) where
 
 import Control.Monad
 import Data.Maybe (isJust, fromJust, fromMaybe)
-import System (exitWith, ExitCode(..))
+import System.Exit
 import System.IO
 import System.FilePath
-import qualified System
 
 import Text.Printf (printf)
 
@@ -49,45 +48,40 @@ import Ganeti.HTools.Types
 -- | Options list and functions.
 options :: [OptType]
 options =
-    [ oPrintNodes
-    , oOutputDir
-    , oLuxiSocket
-    , oVerbose
-    , oNoHeaders
-    , oShowVer
-    , oShowHelp
-    ]
+  [ oPrintNodes
+  , oOutputDir
+  , oLuxiSocket
+  , oVerbose
+  , oNoHeaders
+  , oShowVer
+  , oShowHelp
+  ]
 
 -- | Return a one-line summary of cluster state.
 printCluster :: Node.List -> Instance.List
              -> String
 printCluster nl il =
-    let (bad_nodes, bad_instances) = Cluster.computeBadItems nl il
-        ccv = Cluster.compCV nl
-        nodes = Container.elems nl
-        insts = Container.elems il
-        t_ram = sum . map Node.tMem $ nodes
-        t_dsk = sum . map Node.tDsk $ nodes
-        f_ram = sum . map Node.fMem $ nodes
-        f_dsk = sum . map Node.fDsk $ nodes
-    in
-      printf "%5d %5d %5d %5d %6.0f %6d %6.0f %6d %.8f"
-                 (length nodes) (length insts)
-                 (length bad_nodes) (length bad_instances)
-                 t_ram f_ram
-                 (t_dsk / 1024) (f_dsk `div` 1024)
-                 ccv
-
+  let (bad_nodes, bad_instances) = Cluster.computeBadItems nl il
+      ccv = Cluster.compCV nl
+      nodes = Container.elems nl
+      insts = Container.elems il
+      t_ram = sum . map Node.tMem $ nodes
+      t_dsk = sum . map Node.tDsk $ nodes
+      f_ram = sum . map Node.fMem $ nodes
+      f_dsk = sum . map Node.fDsk $ nodes
+  in printf "%5d %5d %5d %5d %6.0f %6d %6.0f %6d %.8f"
+       (length nodes) (length insts)
+       (length bad_nodes) (length bad_instances)
+       t_ram f_ram (t_dsk / 1024) (f_dsk `div` 1024) ccv
 
 -- | Replace slashes with underscore for saving to filesystem.
 fixSlash :: String -> String
 fixSlash = map (\x -> if x == '/' then '_' else x)
 
-
 -- | Generates serialized data from loader input.
 processData :: ClusterData -> Result ClusterData
 processData input_data = do
-  cdata@(ClusterData _ nl il _) <- mergeData [] [] [] [] input_data
+  cdata@(ClusterData _ nl il _ _) <- mergeData [] [] [] [] input_data
   let (_, fix_nl) = checkData nl il
   return cdata { cdNodes = fix_nl }
 
@@ -116,7 +110,7 @@ writeDataInner :: Int
                -> ClusterData
                -> IO Bool
 writeDataInner nlen name opts cdata fixdata = do
-  let (ClusterData _ nl il _) = fixdata
+  let (ClusterData _ nl il _ _) = fixdata
   printf "%-*s " nlen name :: IO ()
   hFlush stdout
   let shownodes = optShowNodes opts
@@ -130,10 +124,8 @@ writeDataInner nlen name opts cdata fixdata = do
   return True
 
 -- | Main function.
-main :: IO ()
-main = do
-  cmd_args <- System.getArgs
-  (opts, clusters) <- parseOpts cmd_args "hscan" options
+main :: Options -> [String] -> IO ()
+main opts clusters = do
   let local = "LOCAL"
 
   let nlen = if null clusters
index f78262f..2c20ab1 100644 (file)
@@ -4,7 +4,7 @@
 
 {-
 
-Copyright (C) 2009, 2010, 2011 Google Inc.
+Copyright (C) 2009, 2010, 2011, 2012 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
@@ -23,17 +23,15 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 
 -}
 
-module Ganeti.HTools.Program.Hspace (main) where
+module Ganeti.HTools.Program.Hspace (main, options) where
 
 import Control.Monad
-import Data.Char (toUpper, isAlphaNum)
+import Data.Char (toUpper, toLower)
 import Data.Function (on)
 import Data.List
-import Data.Maybe (isJust, fromJust)
+import Data.Maybe (fromMaybe)
 import Data.Ord (comparing)
-import System (exitWith, ExitCode(..))
 import System.IO
-import qualified System
 
 import Text.Printf (printf, hPrintf)
 
@@ -51,26 +49,27 @@ import Ganeti.HTools.Loader
 -- | Options list and functions.
 options :: [OptType]
 options =
-    [ oPrintNodes
-    , oDataFile
-    , oDiskTemplate
-    , oNodeSim
-    , oRapiMaster
-    , oLuxiSocket
-    , oVerbose
-    , oQuiet
-    , oOfflineNode
-    , oIMem
-    , oIDisk
-    , oIVcpus
-    , oMachineReadable
-    , oMaxCpu
-    , oMinDisk
-    , oTieredSpec
-    , oSaveCluster
-    , oShowVer
-    , oShowHelp
-    ]
+  [ oPrintNodes
+  , oDataFile
+  , oDiskTemplate
+  , oSpindleUse
+  , oNodeSim
+  , oRapiMaster
+  , oLuxiSocket
+  , oIAllocSrc
+  , oVerbose
+  , oQuiet
+  , oOfflineNode
+  , oMachineReadable
+  , oMaxCpu
+  , oMaxSolLength
+  , oMinDisk
+  , oStdSpec
+  , oTieredSpec
+  , oSaveCluster
+  , oShowVer
+  , oShowHelp
+  ]
 
 -- | The allocation phase we're in (initial, after tiered allocs, or
 -- after regular allocation).
@@ -82,6 +81,10 @@ data Phase = PInitial
 data SpecType = SpecNormal
               | SpecTiered
 
+-- | Prefix for machine readable names
+htsPrefix :: String
+htsPrefix = "HTS"
+
 -- | What we prefix a spec with.
 specPrefix :: SpecType -> String
 specPrefix SpecNormal = "SPEC"
@@ -89,7 +92,7 @@ specPrefix SpecTiered = "TSPEC_INI"
 
 -- | The description of a spec.
 specDescription :: SpecType -> String
-specDescription SpecNormal = "Normal (fixed-size)"
+specDescription SpecNormal = "Standard (fixed-size)"
 specDescription SpecTiered = "Tiered (initial size)"
 
 -- | Efficiency generic function.
@@ -159,6 +162,13 @@ printStats ph cs =
                  PFinal -> "FIN"
                  PTiered -> "TRL"
 
+-- | Print failure reason and scores
+printFRScores :: Node.List -> Node.List -> [(FailMode, Int)] -> IO ()
+printFRScores ini_nl fin_nl sreason = do
+  printf "  - most likely failure reason: %s\n" $ failureReason sreason::IO ()
+  printClusterScores ini_nl fin_nl
+  printClusterEff (Cluster.totalResources fin_nl)
+
 -- | Print final stats and related metrics.
 printResults :: Bool -> Node.List -> Node.List -> Int -> Int
              -> [(FailMode, Int)] -> IO ()
@@ -166,84 +176,69 @@ printResults True _ fin_nl num_instances allocs sreason = do
   let fin_stats = Cluster.totalResources fin_nl
       fin_instances = num_instances + allocs
 
-  when (num_instances + allocs /= Cluster.csNinst fin_stats) $
-       do
-         hPrintf stderr "ERROR: internal inconsistency, allocated (%d)\
-                        \ != counted (%d)\n" (num_instances + allocs)
-                                 (Cluster.csNinst fin_stats) :: IO ()
-         exitWith $ ExitFailure 1
-
-  printKeys $ printStats PFinal fin_stats
-  printKeys [ ("ALLOC_USAGE", printf "%.8f"
-                                ((fromIntegral num_instances::Double) /
-                                 fromIntegral fin_instances))
-            , ("ALLOC_INSTANCES", printf "%d" allocs)
-            , ("ALLOC_FAIL_REASON", map toUpper . show . fst $ head sreason)
-            ]
-  printKeys $ map (\(x, y) -> (printf "ALLOC_%s_CNT" (show x),
-                               printf "%d" y)) sreason
+  exitWhen (num_instances + allocs /= Cluster.csNinst fin_stats) $
+           printf "internal inconsistency, allocated (%d)\
+                  \ != counted (%d)\n" (num_instances + allocs)
+           (Cluster.csNinst fin_stats)
+
+  printKeysHTS $ printStats PFinal fin_stats
+  printKeysHTS [ ("ALLOC_USAGE", printf "%.8f"
+                                   ((fromIntegral num_instances::Double) /
+                                   fromIntegral fin_instances))
+               , ("ALLOC_INSTANCES", printf "%d" allocs)
+               , ("ALLOC_FAIL_REASON", map toUpper . show . fst $ head sreason)
+               ]
+  printKeysHTS $ map (\(x, y) -> (printf "ALLOC_%s_CNT" (show x),
+                                  printf "%d" y)) sreason
 
 printResults False ini_nl fin_nl _ allocs sreason = do
   putStrLn "Normal (fixed-size) allocation results:"
   printf "  - %3d instances allocated\n" allocs :: IO ()
-  printf "  - most likely failure reason: %s\n" $ failureReason sreason::IO ()
-  printClusterScores ini_nl fin_nl
-  printClusterEff (Cluster.totalResources fin_nl)
+  printFRScores ini_nl fin_nl sreason
 
 -- | Prints the final @OK@ marker in machine readable output.
-printFinal :: Bool -> IO ()
-printFinal True =
-  -- this should be the final entry
-  printKeys [("OK", "1")]
-
-printFinal False = return ()
+printFinalHTS :: Bool -> IO ()
+printFinalHTS = printFinal htsPrefix
 
 -- | Compute the tiered spec counts from a list of allocated
 -- instances.
 tieredSpecMap :: [Instance.Instance]
               -> [(RSpec, Int)]
 tieredSpecMap trl_ixes =
-    let fin_trl_ixes = reverse trl_ixes
-        ix_byspec = groupBy ((==) `on` Instance.specOf) fin_trl_ixes
-        spec_map = map (\ixs -> (Instance.specOf $ head ixs, length ixs))
-                   ix_byspec
-    in spec_map
+  let fin_trl_ixes = reverse trl_ixes
+      ix_byspec = groupBy ((==) `on` Instance.specOf) fin_trl_ixes
+      spec_map = map (\ixs -> (Instance.specOf $ head ixs, length ixs))
+                 ix_byspec
+  in spec_map
 
 -- | Formats a spec map to strings.
 formatSpecMap :: [(RSpec, Int)] -> [String]
 formatSpecMap =
-    map (\(spec, cnt) -> printf "%d,%d,%d=%d" (rspecMem spec)
-                         (rspecDsk spec) (rspecCpu spec) cnt)
+  map (\(spec, cnt) -> printf "%d,%d,%d=%d" (rspecMem spec)
+                       (rspecDsk spec) (rspecCpu spec) cnt)
 
 -- | Formats \"key-metrics\" values.
-formatRSpec :: Double -> String -> RSpec -> [(String, String)]
-formatRSpec m_cpu s r =
-    [ ("KM_" ++ s ++ "_CPU", show $ rspecCpu r)
-    , ("KM_" ++ s ++ "_NPU", show $ fromIntegral (rspecCpu r) / m_cpu)
-    , ("KM_" ++ s ++ "_MEM", show $ rspecMem r)
-    , ("KM_" ++ s ++ "_DSK", show $ rspecDsk r)
-    ]
+formatRSpec :: String -> AllocInfo -> [(String, String)]
+formatRSpec s r =
+  [ ("KM_" ++ s ++ "_CPU", show $ allocInfoVCpus r)
+  , ("KM_" ++ s ++ "_NPU", show $ allocInfoNCpus r)
+  , ("KM_" ++ s ++ "_MEM", show $ allocInfoMem r)
+  , ("KM_" ++ s ++ "_DSK", show $ allocInfoDisk r)
+  ]
 
 -- | Shows allocations stats.
-printAllocationStats :: Double -> Node.List -> Node.List -> IO ()
-printAllocationStats m_cpu ini_nl fin_nl = do
+printAllocationStats :: Node.List -> Node.List -> IO ()
+printAllocationStats ini_nl fin_nl = do
   let ini_stats = Cluster.totalResources ini_nl
       fin_stats = Cluster.totalResources fin_nl
       (rini, ralo, runa) = Cluster.computeAllocationDelta ini_stats fin_stats
-  printKeys $ formatRSpec m_cpu  "USED" rini
-  printKeys $ formatRSpec m_cpu "POOL"ralo
-  printKeys $ formatRSpec m_cpu "UNAV" runa
-
--- | Ensure a value is quoted if needed.
-ensureQuoted :: String -> String
-ensureQuoted v = if not (all (\c -> isAlphaNum c || c == '.') v)
-                 then '\'':v ++ "'"
-                 else v
+  printKeysHTS $ formatRSpec "USED" rini
+  printKeysHTS $ formatRSpec "POOL" ralo
+  printKeysHTS $ formatRSpec "UNAV" runa
 
 -- | Format a list of key\/values as a shell fragment.
-printKeys :: [(String, String)] -> IO ()
-printKeys = mapM_ (\(k, v) ->
-                   printf "HTS_%s=%s\n" (map toUpper k) (ensureQuoted v))
+printKeysHTS :: [(String, String)] -> IO ()
+printKeysHTS = printKeys htsPrefix
 
 -- | Converts instance data to a list of strings.
 printInstance :: Node.List -> Instance.Instance -> [String]
@@ -262,8 +257,8 @@ printAllocationMap :: Int -> String
                    -> Node.List -> [Instance.Instance] -> IO ()
 printAllocationMap verbose msg nl ixes =
   when (verbose > 1) $ do
-    hPutStrLn stderr msg
-    hPutStr stderr . unlines . map ((:) ' ' .  intercalate " ") $
+    hPutStrLn stderr (msg ++ " map")
+    hPutStr stderr . unlines . map ((:) ' ' .  unwords) $
             formatTable (map (printInstance nl) (reverse ixes))
                         -- This is the numberic-or-not field
                         -- specification; the first three fields are
@@ -278,9 +273,9 @@ formatResources res =
 -- | Print the cluster resources.
 printCluster :: Bool -> Cluster.CStats -> Int -> IO ()
 printCluster True ini_stats node_count = do
-  printKeys $ map (\(a, fn) -> ("CLUSTER_" ++ a, fn ini_stats)) clusterData
-  printKeys [("CLUSTER_NODES", printf "%d" node_count)]
-  printKeys $ printStats PInitial ini_stats
+  printKeysHTS $ map (\(a, fn) -> ("CLUSTER_" ++ a, fn ini_stats)) clusterData
+  printKeysHTS [("CLUSTER_NODES", printf "%d" node_count)]
+  printKeysHTS $ printStats PInitial ini_stats
 
 printCluster False ini_stats node_count = do
   printf "The cluster has %d nodes and the following resources:\n  %s.\n"
@@ -292,9 +287,10 @@ printCluster False ini_stats node_count = do
 -- | Prints the normal instance spec.
 printISpec :: Bool -> RSpec -> SpecType -> DiskTemplate -> IO ()
 printISpec True ispec spec disk_template = do
-  printKeys $ map (\(a, fn) -> (prefix ++ "_" ++ a, fn ispec)) specData
-  printKeys [ (prefix ++ "_RQN", printf "%d" req_nodes) ]
-  printKeys [ (prefix ++ "_DISK_TEMPLATE", dtToString disk_template) ]
+  printKeysHTS $ map (\(a, fn) -> (prefix ++ "_" ++ a, fn ispec)) specData
+  printKeysHTS [ (prefix ++ "_RQN", printf "%d" req_nodes) ]
+  printKeysHTS [ (prefix ++ "_DISK_TEMPLATE",
+                  diskTemplateToRaw disk_template) ]
       where req_nodes = Instance.requiredNodes disk_template
             prefix = specPrefix spec
 
@@ -302,24 +298,24 @@ printISpec False ispec spec disk_template =
   printf "%s instance spec is:\n  %s, using disk\
          \ template '%s'.\n"
          (specDescription spec)
-         (formatResources ispec specData) (dtToString disk_template)
+         (formatResources ispec specData) (diskTemplateToRaw disk_template)
 
 -- | Prints the tiered results.
-printTiered :: Bool -> [(RSpec, Int)] -> Double
+printTiered :: Bool -> [(RSpec, Int)]
             -> Node.List -> Node.List -> [(FailMode, Int)] -> IO ()
-printTiered True spec_map m_cpu nl trl_nl _ = do
-  printKeys $ printStats PTiered (Cluster.totalResources trl_nl)
-  printKeys [("TSPEC", intercalate " " (formatSpecMap spec_map))]
-  printAllocationStats m_cpu nl trl_nl
+printTiered True spec_map nl trl_nl _ = do
+  printKeysHTS $ printStats PTiered (Cluster.totalResources trl_nl)
+  printKeysHTS [("TSPEC", unwords (formatSpecMap spec_map))]
+  printAllocationStats nl trl_nl
 
-printTiered False spec_map _ ini_nl fin_nl sreason = do
+printTiered False spec_map ini_nl fin_nl sreason = do
   _ <- printf "Tiered allocation results:\n"
-  mapM_ (\(ispec, cnt) ->
-             printf "  - %3d instances of spec %s\n" cnt
-                        (formatResources ispec specData)) spec_map
-  printf "  - most likely failure reason: %s\n" $ failureReason sreason::IO ()
-  printClusterScores ini_nl fin_nl
-  printClusterEff (Cluster.totalResources fin_nl)
+  if null spec_map
+    then putStrLn "  - no instances allocated"
+    else mapM_ (\(ispec, cnt) ->
+                  printf "  - %3d instances of spec %s\n" cnt
+                           (formatResources ispec specData)) spec_map
+  printFRScores ini_nl fin_nl sreason
 
 -- | Displays the initial/final cluster scores.
 printClusterScores :: Node.List -> Node.List -> IO ()
@@ -330,8 +326,8 @@ printClusterScores ini_nl fin_nl = do
 -- | Displays the cluster efficiency.
 printClusterEff :: Cluster.CStats -> IO ()
 printClusterEff cs =
-    mapM_ (\(s, fn) ->
-               printf "  - %s usage efficiency: %5.2f%%\n" s (fn cs * 100))
+  mapM_ (\(s, fn) ->
+           printf "  - %s usage efficiency: %5.2f%%\n" s (fn cs * 100))
           [("memory", memEff),
            ("  disk", dskEff),
            ("  vcpu", cpuEff)]
@@ -344,136 +340,114 @@ failureReason = show . fst . head
 sortReasons :: [(FailMode, Int)] -> [(FailMode, Int)]
 sortReasons = reverse . sortBy (comparing snd)
 
--- | Main function.
-main :: IO ()
-main = do
-  cmd_args <- System.getArgs
-  (opts, args) <- parseOpts cmd_args "hspace" options
+-- | Runs an allocation algorithm and saves cluster state.
+runAllocation :: ClusterData                -- ^ Cluster data
+              -> Maybe Cluster.AllocResult  -- ^ Optional stop-allocation
+              -> Result Cluster.AllocResult -- ^ Allocation result
+              -> RSpec                      -- ^ Requested instance spec
+              -> DiskTemplate               -- ^ Requested disk template
+              -> SpecType                   -- ^ Allocation type
+              -> Options                    -- ^ CLI options
+              -> IO (FailStats, Node.List, Int, [(RSpec, Int)])
+runAllocation cdata stop_allocation actual_result spec dt mode opts = do
+  (reasons, new_nl, new_il, new_ixes, _) <-
+      case stop_allocation of
+        Just result_noalloc -> return result_noalloc
+        Nothing -> exitIfBad "failure during allocation" actual_result
 
-  unless (null args) $ do
-         hPutStrLn stderr "Error: this program doesn't take any arguments."
-         exitWith $ ExitFailure 1
+  let name = head . words . specDescription $ mode
+      descr = name ++ " allocation"
+      ldescr = "after " ++ map toLower descr
 
-  let verbose = optVerbose opts
-      ispec = optISpec opts
-      shownodes = optShowNodes opts
-      disk_template = optDiskTemplate opts
-      req_nodes = Instance.requiredNodes disk_template
-      machine_r = optMachineReadable opts
+  printISpec (optMachineReadable opts) spec mode dt
 
-  (ClusterData gl fixed_nl il ctags) <- loadExternalData opts
+  printAllocationMap (optVerbose opts) descr new_nl new_ixes
 
-  let num_instances = length $ Container.elems il
+  maybePrintNodes (optShowNodes opts) descr (Cluster.printNodes new_nl)
 
-  let offline_passed = optOffline opts
-      all_nodes = Container.elems fixed_nl
-      offline_lkp = map (lookupName (map Node.name all_nodes)) offline_passed
-      offline_wrong = filter (not . goodLookupResult) offline_lkp
-      offline_names = map lrContent offline_lkp
-      offline_indices = map Node.idx $
-                        filter (\n -> Node.name n `elem` offline_names)
-                               all_nodes
-      m_cpu = optMcpu opts
-      m_dsk = optMdsk opts
-
-  when (not (null offline_wrong)) $ do
-         hPrintf stderr "Error: Wrong node name(s) set as offline: %s\n"
-                     (commaJoin (map lrContent offline_wrong)) :: IO ()
-         exitWith $ ExitFailure 1
-
-  when (req_nodes /= 1 && req_nodes /= 2) $ do
-         hPrintf stderr "Error: Invalid required nodes (%d)\n"
-                                            req_nodes :: IO ()
-         exitWith $ ExitFailure 1
-
-  let nm = Container.map (\n -> if Node.idx n `elem` offline_indices
-                                then Node.setOffline n True
-                                else n) fixed_nl
-      nl = Container.map (flip Node.setMdsk m_dsk . flip Node.setMcpu m_cpu)
-           nm
-      csf = commonSuffix fixed_nl il
+  maybeSaveData (optSaveCluster opts) (map toLower name) ldescr
+                    (cdata { cdNodes = new_nl, cdInstances = new_il})
 
-  when (length csf > 0 && verbose > 1) $
-       hPrintf stderr "Note: Stripping common suffix of '%s' from names\n" csf
+  return (sortReasons reasons, new_nl, length new_ixes, tieredSpecMap new_ixes)
 
-  when (isJust shownodes) $
-       do
-         hPutStrLn stderr "Initial cluster status:"
-         hPutStrLn stderr $ Cluster.printNodes nl (fromJust shownodes)
+-- | Create an instance from a given spec.
+instFromSpec :: RSpec -> DiskTemplate -> Int -> Instance.Instance
+instFromSpec spx disk_template su =
+  Instance.create "new" (rspecMem spx) (rspecDsk spx)
+    (rspecCpu spx) Running [] True (-1) (-1) disk_template su
 
-  let ini_cv = Cluster.compCV nl
-      ini_stats = Cluster.totalResources nl
-
-  when (verbose > 2) $
-         hPrintf stderr "Initial coefficients: overall %.8f, %s\n"
-                 ini_cv (Cluster.printStats nl)
+-- | Main function.
+main :: Options -> [String] -> IO ()
+main opts args = do
+  exitUnless (null args) "this program doesn't take any arguments"
 
-  printCluster machine_r ini_stats (length all_nodes)
+  let verbose = optVerbose opts
+      machine_r = optMachineReadable opts
 
-  printISpec machine_r ispec SpecNormal disk_template
+  orig_cdata@(ClusterData gl fixed_nl il _ ipol) <- loadExternalData opts
+  nl <- setNodeStatus opts fixed_nl
 
-  let bad_nodes = fst $ Cluster.computeBadItems nl il
-      stop_allocation = length bad_nodes > 0
-      result_noalloc = ([(FailN1, 1)]::FailStats, nl, il, [], [])
+  cluster_disk_template <-
+    case iPolicyDiskTemplates ipol of
+      first_templ:_ -> return first_templ
+      _ -> exitErr "null list of disk templates received from cluster"
 
-  -- utility functions
-  let iofspec spx = Instance.create "new" (rspecMem spx) (rspecDsk spx)
-                    (rspecCpu spx) "running" [] True (-1) (-1) disk_template
-      exitifbad val = (case val of
-                         Bad s -> do
-                           hPrintf stderr "Failure: %s\n" s :: IO ()
-                           exitWith $ ExitFailure 1
-                         Ok x -> return x)
+  let num_instances = Container.size il
+      all_nodes = Container.elems fixed_nl
+      cdata = orig_cdata { cdNodes = fixed_nl }
+      disk_template = fromMaybe cluster_disk_template (optDiskTemplate opts)
+      req_nodes = Instance.requiredNodes disk_template
+      csf = commonSuffix fixed_nl il
+      su = fromMaybe (iSpecSpindleUse $ iPolicyStdSpec ipol)
+                     (optSpindleUse opts)
 
+  when (not (null csf) && verbose > 1) $
+       hPrintf stderr "Note: Stripping common suffix of '%s' from names\n" csf
 
-  let reqinst = iofspec ispec
+  maybePrintNodes (optShowNodes opts) "Initial cluster" (Cluster.printNodes nl)
 
-  allocnodes <- exitifbad $ Cluster.genAllocNodes gl nl req_nodes True
+  when (verbose > 2) $
+         hPrintf stderr "Initial coefficients: overall %.8f\n%s"
+                 (Cluster.compCV nl) (Cluster.printStats "  " nl)
 
-  -- Run the tiered allocation, if enabled
+  printCluster machine_r (Cluster.totalResources nl) (length all_nodes)
 
-  (case optTieredSpec opts of
-     Nothing -> return ()
-     Just tspec -> do
-       (treason, trl_nl, trl_il, trl_ixes, _) <-
-           if stop_allocation
-           then return result_noalloc
-           else exitifbad (Cluster.tieredAlloc nl il Nothing (iofspec tspec)
-                                  allocnodes [] [])
-       let spec_map' = tieredSpecMap trl_ixes
-           treason' = sortReasons treason
+  let stop_allocation = case Cluster.computeBadItems nl il of
+                          ([], _) -> Nothing
+                          _ -> Just ([(FailN1, 1)]::FailStats, nl, il, [], [])
+      alloclimit = if optMaxLength opts == -1
+                   then Nothing
+                   else Just (optMaxLength opts)
 
-       printAllocationMap verbose "Tiered allocation map" trl_nl trl_ixes
+  allocnodes <- exitIfBad "failure during allocation" $
+                Cluster.genAllocNodes gl nl req_nodes True
 
-       maybePrintNodes shownodes "Tiered allocation"
-                           (Cluster.printNodes trl_nl)
+  -- Run the tiered allocation
 
-       maybeSaveData (optSaveCluster opts) "tiered" "after tiered allocation"
-                     (ClusterData gl trl_nl trl_il ctags)
+  let tspec = fromMaybe (rspecFromISpec (iPolicyMaxSpec ipol))
+              (optTieredSpec opts)
 
-       printISpec machine_r tspec SpecTiered disk_template
+  (treason, trl_nl, _, spec_map) <-
+    runAllocation cdata stop_allocation
+       (Cluster.tieredAlloc nl il alloclimit
+        (instFromSpec tspec disk_template su) allocnodes [] [])
+       tspec disk_template SpecTiered opts
 
-       printTiered machine_r spec_map' m_cpu nl trl_nl treason'
-       )
+  printTiered machine_r spec_map nl trl_nl treason
 
   -- Run the standard (avg-mode) allocation
 
-  (ereason, fin_nl, fin_il, ixes, _) <-
-      if stop_allocation
-      then return result_noalloc
-      else exitifbad (Cluster.iterateAlloc nl il Nothing
-                      reqinst allocnodes [] [])
+  let ispec = fromMaybe (rspecFromISpec (iPolicyStdSpec ipol))
+              (optStdSpec opts)
 
-  let allocs = length ixes
-      sreason = sortReasons ereason
-
-  printAllocationMap verbose "Standard allocation map" fin_nl ixes
-
-  maybePrintNodes shownodes "Standard allocation" (Cluster.printNodes fin_nl)
-
-  maybeSaveData (optSaveCluster opts) "alloc" "after standard allocation"
-       (ClusterData gl fin_nl fin_il ctags)
+  (sreason, fin_nl, allocs, _) <-
+      runAllocation cdata stop_allocation
+            (Cluster.iterateAlloc nl il alloclimit
+             (instFromSpec ispec disk_template su) allocnodes [] [])
+            ispec disk_template SpecNormal opts
 
   printResults machine_r nl fin_nl num_instances allocs sreason
 
-  printFinal machine_r
+  -- Print final result
+
+  printFinalHTS machine_r
index 80551eb..c00e22d 100644 (file)
@@ -1,10 +1,12 @@
+{-# LANGUAGE TemplateHaskell #-}
+
 {-| Unittests for ganeti-htools.
 
 -}
 
 {-
 
-Copyright (C) 2009, 2010, 2011 Google Inc.
+Copyright (C) 2009, 2010, 2011, 2012 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
@@ -24,55 +26,68 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 -}
 
 module Ganeti.HTools.QC
-    ( testUtils
-    , testPeerMap
-    , testContainer
-    , testInstance
-    , testNode
-    , testText
-    , testOpCodes
-    , testJobs
-    , testCluster
-    , testLoader
-    , testTypes
-    ) where
+  ( testUtils
+  , testPeerMap
+  , testContainer
+  , testInstance
+  , testNode
+  , testText
+  , testSimu
+  , testOpCodes
+  , testJobs
+  , testCluster
+  , testLoader
+  , testTypes
+  , testCLI
+  , testJSON
+  , testLUXI
+  , testSsconf
+  ) where
 
 import Test.QuickCheck
+import Text.Printf (printf)
 import Data.List (findIndex, intercalate, nub, isPrefixOf)
+import qualified Data.Set as Set
 import Data.Maybe
 import Control.Monad
+import Control.Applicative
+import qualified System.Console.GetOpt as GetOpt
 import qualified Text.JSON as J
 import qualified Data.Map
 import qualified Data.IntMap as IntMap
+
+import qualified Ganeti.BasicTypes as BasicTypes
 import qualified Ganeti.OpCodes as OpCodes
 import qualified Ganeti.Jobs as Jobs
-import qualified Ganeti.Luxi
+import qualified Ganeti.Luxi as Luxi
+import qualified Ganeti.Ssconf as Ssconf
 import qualified Ganeti.HTools.CLI as CLI
 import qualified Ganeti.HTools.Cluster as Cluster
 import qualified Ganeti.HTools.Container as Container
 import qualified Ganeti.HTools.ExtLoader
 import qualified Ganeti.HTools.IAlloc as IAlloc
 import qualified Ganeti.HTools.Instance as Instance
+import qualified Ganeti.HTools.JSON as JSON
 import qualified Ganeti.HTools.Loader as Loader
-import qualified Ganeti.HTools.Luxi
+import qualified Ganeti.HTools.Luxi as HTools.Luxi
 import qualified Ganeti.HTools.Node as Node
 import qualified Ganeti.HTools.Group as Group
 import qualified Ganeti.HTools.PeerMap as PeerMap
 import qualified Ganeti.HTools.Rapi
-import qualified Ganeti.HTools.Simu
+import qualified Ganeti.HTools.Simu as Simu
 import qualified Ganeti.HTools.Text as Text
 import qualified Ganeti.HTools.Types as Types
 import qualified Ganeti.HTools.Utils as Utils
 import qualified Ganeti.HTools.Version
 import qualified Ganeti.Constants as C
 
+import qualified Ganeti.HTools.Program as Program
 import qualified Ganeti.HTools.Program.Hail
 import qualified Ganeti.HTools.Program.Hbal
 import qualified Ganeti.HTools.Program.Hscan
 import qualified Ganeti.HTools.Program.Hspace
 
-run :: Testable prop => prop -> Args -> IO Result
-run = flip quickCheckWithResult
+import Ganeti.HTools.QCHelper (testSuite)
 
 -- * Constants
 
@@ -88,10 +103,61 @@ maxDsk = 1024 * 1024 * 8
 maxCpu :: Int
 maxCpu = 1024
 
+-- | Max vcpu ratio (random value).
+maxVcpuRatio :: Double
+maxVcpuRatio = 1024.0
+
+-- | Max spindle ratio (random value).
+maxSpindleRatio :: Double
+maxSpindleRatio = 1024.0
+
+-- | Max nodes, used just to limit arbitrary instances for smaller
+-- opcode definitions (e.g. list of nodes in OpTestDelay).
+maxNodes :: Int
+maxNodes = 32
+
+-- | Max opcodes or jobs in a submit job and submit many jobs.
+maxOpCodes :: Int
+maxOpCodes = 16
+
+-- | All disk templates (used later)
+allDiskTemplates :: [Types.DiskTemplate]
+allDiskTemplates = [minBound..maxBound]
+
+-- | Null iPolicy, and by null we mean very liberal.
+nullIPolicy = Types.IPolicy
+  { Types.iPolicyMinSpec = Types.ISpec { Types.iSpecMemorySize = 0
+                                       , Types.iSpecCpuCount   = 0
+                                       , Types.iSpecDiskSize   = 0
+                                       , Types.iSpecDiskCount  = 0
+                                       , Types.iSpecNicCount   = 0
+                                       , Types.iSpecSpindleUse = 0
+                                       }
+  , Types.iPolicyMaxSpec = Types.ISpec { Types.iSpecMemorySize = maxBound
+                                       , Types.iSpecCpuCount   = maxBound
+                                       , Types.iSpecDiskSize   = maxBound
+                                       , Types.iSpecDiskCount  = C.maxDisks
+                                       , Types.iSpecNicCount   = C.maxNics
+                                       , Types.iSpecSpindleUse = maxBound
+                                       }
+  , Types.iPolicyStdSpec = Types.ISpec { Types.iSpecMemorySize = Types.unitMem
+                                       , Types.iSpecCpuCount   = Types.unitCpu
+                                       , Types.iSpecDiskSize   = Types.unitDsk
+                                       , Types.iSpecDiskCount  = 1
+                                       , Types.iSpecNicCount   = 1
+                                       , Types.iSpecSpindleUse = 1
+                                       }
+  , Types.iPolicyDiskTemplates = [minBound..maxBound]
+  , Types.iPolicyVcpuRatio = maxVcpuRatio -- somewhat random value, high
+                                          -- enough to not impact us
+  , Types.iPolicySpindleRatio = maxSpindleRatio
+  }
+
+
 defGroup :: Group.Group
 defGroup = flip Group.setIdx 0 $
-               Group.create "default" Utils.defaultGroupID
-                    Types.AllocPreferred
+             Group.create "default" Types.defaultGroupID Types.AllocPreferred
+                  nullIPolicy
 
 defGroupList :: Group.List
 defGroupList = Container.fromList [(Group.idx defGroup, defGroup)]
@@ -106,29 +172,52 @@ isFailure :: Types.OpResult a -> Bool
 isFailure (Types.OpFail _) = True
 isFailure _ = False
 
+-- | Checks for equality with proper annotation.
+(==?) :: (Show a, Eq a) => a -> a -> Property
+(==?) x y = printTestCase
+            ("Expected equality, but '" ++
+             show x ++ "' /= '" ++ show y ++ "'") (x == y)
+infix 3 ==?
+
+-- | Show a message and fail the test.
+failTest :: String -> Property
+failTest msg = printTestCase msg False
+
 -- | Update an instance to be smaller than a node.
 setInstanceSmallerThanNode node inst =
-    inst { Instance.mem = Node.availMem node `div` 2
-         , Instance.dsk = Node.availDisk node `div` 2
-         , Instance.vcpus = Node.availCpu node `div` 2
-         }
+  inst { Instance.mem = Node.availMem node `div` 2
+       , Instance.dsk = Node.availDisk node `div` 2
+       , Instance.vcpus = Node.availCpu node `div` 2
+       }
 
 -- | Create an instance given its spec.
 createInstance mem dsk vcpus =
-    Instance.create "inst-unnamed" mem dsk vcpus "running" [] True (-1) (-1)
-                    Types.DTDrbd8
+  Instance.create "inst-unnamed" mem dsk vcpus Types.Running [] True (-1) (-1)
+    Types.DTDrbd8 1
 
 -- | Create a small cluster by repeating a node spec.
 makeSmallCluster :: Node.Node -> Int -> Node.List
 makeSmallCluster node count =
-    let fn = Node.buildPeers node Container.empty
-        namelst = map (\n -> (Node.name n, n)) (replicate count fn)
-        (_, nlst) = Loader.assignIndices namelst
-    in nlst
+  let origname = Node.name node
+      origalias = Node.alias node
+      nodes = map (\idx -> node { Node.name = origname ++ "-" ++ show idx
+                                , Node.alias = origalias ++ "-" ++ show idx })
+              [1..count]
+      fn = flip Node.buildPeers Container.empty
+      namelst = map (\n -> (Node.name n, fn n)) nodes
+      (_, nlst) = Loader.assignIndices namelst
+  in nlst
+
+-- | Make a small cluster, both nodes and instances.
+makeSmallEmptyCluster :: Node.Node -> Int -> Instance.Instance
+                      -> (Node.List, Instance.List, Instance.Instance)
+makeSmallEmptyCluster node count inst =
+  (makeSmallCluster node count, Container.empty,
+   setInstanceSmallerThanNode node inst)
 
 -- | Checks if a node is "big" enough.
-isNodeBig :: Node.Node -> Int -> Bool
-isNodeBig node size = Node.availDisk node > size * Types.unitDsk
+isNodeBig :: Int -> Node.Node -> Bool
+isNodeBig size node = Node.availDisk node > size * Types.unitDsk
                       && Node.availMem node > size * Types.unitMem
                       && Node.availCpu node > size * Types.unitCpu
 
@@ -144,8 +233,8 @@ assignInstance nl il inst pdx sdx =
   let pnode = Container.find pdx nl
       snode = Container.find sdx nl
       maxiidx = if Container.null il
-                then 0
-                else fst (Container.findMax il) + 1
+                  then 0
+                  else fst (Container.findMax il) + 1
       inst' = inst { Instance.idx = maxiidx,
                      Instance.pNode = pdx, Instance.sNode = sdx }
       pnode' = Node.setPri pnode inst'
@@ -154,50 +243,120 @@ assignInstance nl il inst pdx sdx =
       il' = Container.add maxiidx inst' il
   in (nl', il')
 
+-- | Generates a list of a given size with non-duplicate elements.
+genUniquesList :: (Eq a, Arbitrary a) => Int -> Gen [a]
+genUniquesList cnt =
+  foldM (\lst _ -> do
+           newelem <- arbitrary `suchThat` (`notElem` lst)
+           return (newelem:lst)) [] [1..cnt]
+
+-- | Checks if an instance is mirrored.
+isMirrored :: Instance.Instance -> Bool
+isMirrored = (/= Types.MirrorNone) . Instance.mirrorType
+
+-- | Returns the possible change node types for a disk template.
+evacModeOptions :: Types.MirrorType -> [Types.EvacMode]
+evacModeOptions Types.MirrorNone     = []
+evacModeOptions Types.MirrorInternal = [minBound..maxBound] -- DRBD can do all
+evacModeOptions Types.MirrorExternal = [Types.ChangePrimary, Types.ChangeAll]
+
 -- * Arbitrary instances
 
 -- | Defines a DNS name.
 newtype DNSChar = DNSChar { dnsGetChar::Char }
 
 instance Arbitrary DNSChar where
-    arbitrary = do
-      x <- elements (['a'..'z'] ++ ['0'..'9'] ++ "_-")
-      return (DNSChar x)
+  arbitrary = do
+    x <- elements (['a'..'z'] ++ ['0'..'9'] ++ "_-")
+    return (DNSChar x)
 
+-- | Generates a single name component.
 getName :: Gen String
 getName = do
   n <- choose (1, 64)
-  dn <- vector n::Gen [DNSChar]
+  dn <- vector n
   return (map dnsGetChar dn)
 
-
+-- | Generates an entire FQDN.
 getFQDN :: Gen String
 getFQDN = do
-  felem <- getName
   ncomps <- choose (1, 4)
-  frest <- vector ncomps::Gen [[DNSChar]]
-  let frest' = map (map dnsGetChar) frest
-  return (felem ++ "." ++ intercalate "." frest')
+  names <- vectorOf ncomps getName
+  return $ intercalate "." names
+
+-- | Combinator that generates a 'Maybe' using a sub-combinator.
+getMaybe :: Gen a -> Gen (Maybe a)
+getMaybe subgen = do
+  bool <- arbitrary
+  if bool
+    then Just <$> subgen
+    else return Nothing
+
+-- | Generates a fields list. This uses the same character set as a
+-- DNS name (just for simplicity).
+getFields :: Gen [String]
+getFields = do
+  n <- choose (1, 32)
+  vectorOf n getName
+
+-- | Defines a tag type.
+newtype TagChar = TagChar { tagGetChar :: Char }
+
+-- | All valid tag chars. This doesn't need to match _exactly_
+-- Ganeti's own tag regex, just enough for it to be close.
+tagChar :: [Char]
+tagChar = ['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9'] ++ ".+*/:@-"
+
+instance Arbitrary TagChar where
+  arbitrary = do
+    c <- elements tagChar
+    return (TagChar c)
+
+-- | Generates a tag
+genTag :: Gen [TagChar]
+genTag = do
+  -- the correct value would be C.maxTagLen, but that's way too
+  -- verbose in unittests, and at the moment I don't see any possible
+  -- bugs with longer tags and the way we use tags in htools
+  n <- choose (1, 10)
+  vector n
+
+-- | Generates a list of tags (correctly upper bounded).
+genTags :: Gen [String]
+genTags = do
+  -- the correct value would be C.maxTagsPerObj, but per the comment
+  -- in genTag, we don't use tags enough in htools to warrant testing
+  -- such big values
+  n <- choose (0, 10::Int)
+  tags <- mapM (const genTag) [1..n]
+  return $ map (map tagGetChar) tags
+
+instance Arbitrary Types.InstanceStatus where
+    arbitrary = elements [minBound..maxBound]
+
+-- | Generates a random instance with maximum disk/mem/cpu values.
+genInstanceSmallerThan :: Int -> Int -> Int -> Gen Instance.Instance
+genInstanceSmallerThan lim_mem lim_dsk lim_cpu = do
+  name <- getFQDN
+  mem <- choose (0, lim_mem)
+  dsk <- choose (0, lim_dsk)
+  run_st <- arbitrary
+  pn <- arbitrary
+  sn <- arbitrary
+  vcpus <- choose (0, lim_cpu)
+  dt <- arbitrary
+  return $ Instance.create name mem dsk vcpus run_st [] True pn sn dt 1
+
+-- | Generates an instance smaller than a node.
+genInstanceSmallerThanNode :: Node.Node -> Gen Instance.Instance
+genInstanceSmallerThanNode node =
+  genInstanceSmallerThan (Node.availMem node `div` 2)
+                         (Node.availDisk node `div` 2)
+                         (Node.availCpu node `div` 2)
 
 -- let's generate a random instance
 instance Arbitrary Instance.Instance where
-    arbitrary = do
-      name <- getFQDN
-      mem <- choose (0, maxMem)
-      dsk <- choose (0, maxDsk)
-      run_st <- elements [ C.inststErrorup
-                         , C.inststErrordown
-                         , C.inststAdmindown
-                         , C.inststNodedown
-                         , C.inststNodeoffline
-                         , C.inststRunning
-                         , "no_such_status1"
-                         , "no_such_status2"]
-      pn <- arbitrary
-      sn <- arbitrary
-      vcpus <- choose (0, maxCpu)
-      return $ Instance.create name mem dsk vcpus run_st [] True pn sn
-                               Types.DTDrbd8
+  arbitrary = genInstanceSmallerThan maxMem maxDsk maxCpu
 
 -- | Generas an arbitrary node based on sizing information.
 genNode :: Maybe Int -- ^ Minimum node size in terms of units
@@ -206,17 +365,17 @@ genNode :: Maybe Int -- ^ Minimum node size in terms of units
         -> Gen Node.Node
 genNode min_multiplier max_multiplier = do
   let (base_mem, base_dsk, base_cpu) =
-          case min_multiplier of
-            Just mm -> (mm * Types.unitMem,
-                        mm * Types.unitDsk,
-                        mm * Types.unitCpu)
-            Nothing -> (0, 0, 0)
+        case min_multiplier of
+          Just mm -> (mm * Types.unitMem,
+                      mm * Types.unitDsk,
+                      mm * Types.unitCpu)
+          Nothing -> (0, 0, 0)
       (top_mem, top_dsk, top_cpu)  =
-          case max_multiplier of
-            Just mm -> (mm * Types.unitMem,
-                        mm * Types.unitDsk,
-                        mm * Types.unitCpu)
-            Nothing -> (maxMem, maxDsk, maxCpu)
+        case max_multiplier of
+          Just mm -> (mm * Types.unitMem,
+                      mm * Types.unitDsk,
+                      mm * Types.unitCpu)
+          Nothing -> (maxMem, maxDsk, maxCpu)
   name  <- getFQDN
   mem_t <- choose (base_mem, top_mem)
   mem_f <- choose (base_mem, mem_t)
@@ -226,20 +385,26 @@ genNode min_multiplier max_multiplier = do
   cpu_t <- choose (base_cpu, top_cpu)
   offl  <- arbitrary
   let n = Node.create name (fromIntegral mem_t) mem_n mem_f
-          (fromIntegral dsk_t) dsk_f (fromIntegral cpu_t) offl 0
-  return $ Node.buildPeers n Container.empty
+          (fromIntegral dsk_t) dsk_f (fromIntegral cpu_t) offl 1 0
+      n' = Node.setPolicy nullIPolicy n
+  return $ Node.buildPeers n' Container.empty
+
+-- | Helper function to generate a sane node.
+genOnlineNode :: Gen Node.Node
+genOnlineNode = do
+  arbitrary `suchThat` (\n -> not (Node.offline n) &&
+                              not (Node.failN1 n) &&
+                              Node.availDisk n > 0 &&
+                              Node.availMem n > 0 &&
+                              Node.availCpu n > 0)
 
 -- and a random node
 instance Arbitrary Node.Node where
-    arbitrary = genNode Nothing Nothing
+  arbitrary = genNode Nothing Nothing
 
 -- replace disks
 instance Arbitrary OpCodes.ReplaceDisksMode where
-  arbitrary = elements [ OpCodes.ReplaceOnPrimary
-                       , OpCodes.ReplaceOnSecondary
-                       , OpCodes.ReplaceNewSecondary
-                       , OpCodes.ReplaceAuto
-                       ]
+  arbitrary = elements [minBound..maxBound]
 
 instance Arbitrary OpCodes.OpCode where
   arbitrary = do
@@ -248,20 +413,20 @@ instance Arbitrary OpCodes.OpCode where
                       , "OP_INSTANCE_FAILOVER"
                       , "OP_INSTANCE_MIGRATE"
                       ]
-    (case op_id of
-        "OP_TEST_DELAY" ->
-          liftM3 OpCodes.OpTestDelay arbitrary arbitrary arbitrary
-        "OP_INSTANCE_REPLACE_DISKS" ->
-          liftM5 OpCodes.OpInstanceReplaceDisks arbitrary arbitrary
-          arbitrary arbitrary arbitrary
-        "OP_INSTANCE_FAILOVER" ->
-          liftM3 OpCodes.OpInstanceFailover arbitrary arbitrary
-                 arbitrary
-        "OP_INSTANCE_MIGRATE" ->
-          liftM5 OpCodes.OpInstanceMigrate arbitrary arbitrary
-                 arbitrary arbitrary
-          arbitrary
-        _ -> fail "Wrong opcode")
+    case op_id of
+      "OP_TEST_DELAY" ->
+        OpCodes.OpTestDelay <$> arbitrary <*> arbitrary
+                 <*> resize maxNodes (listOf getFQDN)
+      "OP_INSTANCE_REPLACE_DISKS" ->
+        OpCodes.OpInstanceReplaceDisks <$> getFQDN <*> getMaybe getFQDN <*>
+          arbitrary <*> resize C.maxDisks arbitrary <*> getMaybe getName
+      "OP_INSTANCE_FAILOVER" ->
+        OpCodes.OpInstanceFailover <$> getFQDN <*> arbitrary <*>
+          getMaybe getFQDN
+      "OP_INSTANCE_MIGRATE" ->
+        OpCodes.OpInstanceMigrate <$> getFQDN <*> arbitrary <*>
+          arbitrary <*> arbitrary <*> getMaybe getFQDN
+      _ -> fail "Wrong opcode"
 
 instance Arbitrary Jobs.OpStatus where
   arbitrary = elements [minBound..maxBound]
@@ -271,9 +436,9 @@ instance Arbitrary Jobs.JobStatus where
 
 newtype SmallRatio = SmallRatio Double deriving Show
 instance Arbitrary SmallRatio where
-    arbitrary = do
-      v <- choose (0, 1)
-      return $ SmallRatio v
+  arbitrary = do
+    v <- choose (0, 1)
+    return $ SmallRatio v
 
 instance Arbitrary Types.AllocPolicy where
   arbitrary = elements [minBound..maxBound]
@@ -282,104 +447,165 @@ instance Arbitrary Types.DiskTemplate where
   arbitrary = elements [minBound..maxBound]
 
 instance Arbitrary Types.FailMode where
-    arbitrary = elements [minBound..maxBound]
+  arbitrary = elements [minBound..maxBound]
+
+instance Arbitrary Types.EvacMode where
+  arbitrary = elements [minBound..maxBound]
 
 instance Arbitrary a => Arbitrary (Types.OpResult a) where
-    arbitrary = arbitrary >>= \c ->
-                case c of
-                  False -> liftM Types.OpFail arbitrary
-                  True -> liftM Types.OpGood arbitrary
+  arbitrary = arbitrary >>= \c ->
+              if c
+                then Types.OpGood <$> arbitrary
+                else Types.OpFail <$> arbitrary
+
+instance Arbitrary Types.ISpec where
+  arbitrary = do
+    mem_s <- arbitrary::Gen (NonNegative Int)
+    dsk_c <- arbitrary::Gen (NonNegative Int)
+    dsk_s <- arbitrary::Gen (NonNegative Int)
+    cpu_c <- arbitrary::Gen (NonNegative Int)
+    nic_c <- arbitrary::Gen (NonNegative Int)
+    su    <- arbitrary::Gen (NonNegative Int)
+    return Types.ISpec { Types.iSpecMemorySize = fromIntegral mem_s
+                       , Types.iSpecCpuCount   = fromIntegral cpu_c
+                       , Types.iSpecDiskSize   = fromIntegral dsk_s
+                       , Types.iSpecDiskCount  = fromIntegral dsk_c
+                       , Types.iSpecNicCount   = fromIntegral nic_c
+                       , Types.iSpecSpindleUse = fromIntegral su
+                       }
+
+-- | Generates an ispec bigger than the given one.
+genBiggerISpec :: Types.ISpec -> Gen Types.ISpec
+genBiggerISpec imin = do
+  mem_s <- choose (Types.iSpecMemorySize imin, maxBound)
+  dsk_c <- choose (Types.iSpecDiskCount imin, maxBound)
+  dsk_s <- choose (Types.iSpecDiskSize imin, maxBound)
+  cpu_c <- choose (Types.iSpecCpuCount imin, maxBound)
+  nic_c <- choose (Types.iSpecNicCount imin, maxBound)
+  su    <- choose (Types.iSpecSpindleUse imin, maxBound)
+  return Types.ISpec { Types.iSpecMemorySize = fromIntegral mem_s
+                     , Types.iSpecCpuCount   = fromIntegral cpu_c
+                     , Types.iSpecDiskSize   = fromIntegral dsk_s
+                     , Types.iSpecDiskCount  = fromIntegral dsk_c
+                     , Types.iSpecNicCount   = fromIntegral nic_c
+                     , Types.iSpecSpindleUse = fromIntegral su
+                     }
+
+instance Arbitrary Types.IPolicy where
+  arbitrary = do
+    imin <- arbitrary
+    istd <- genBiggerISpec imin
+    imax <- genBiggerISpec istd
+    num_tmpl <- choose (0, length allDiskTemplates)
+    dts  <- genUniquesList num_tmpl
+    vcpu_ratio <- choose (1.0, maxVcpuRatio)
+    spindle_ratio <- choose (1.0, maxSpindleRatio)
+    return Types.IPolicy { Types.iPolicyMinSpec = imin
+                         , Types.iPolicyStdSpec = istd
+                         , Types.iPolicyMaxSpec = imax
+                         , Types.iPolicyDiskTemplates = dts
+                         , Types.iPolicyVcpuRatio = vcpu_ratio
+                         , Types.iPolicySpindleRatio = spindle_ratio
+                         }
 
 -- * Actual tests
 
 -- ** Utils tests
 
+-- | Helper to generate a small string that doesn't contain commas.
+genNonCommaString = do
+  size <- choose (0, 20) -- arbitrary max size
+  vectorOf size (arbitrary `suchThat` ((/=) ','))
+
 -- | If the list is not just an empty element, and if the elements do
 -- not contain commas, then join+split should be idempotent.
 prop_Utils_commaJoinSplit =
-    forAll (arbitrary `suchThat`
-            (\l -> l /= [""] && all (not . elem ',') l )) $ \lst ->
-    Utils.sepSplit ',' (Utils.commaJoin lst) == lst
+  forAll (choose (0, 20)) $ \llen ->
+  forAll (vectorOf llen genNonCommaString `suchThat` ((/=) [""])) $ \lst ->
+  Utils.sepSplit ',' (Utils.commaJoin lst) ==? lst
 
 -- | Split and join should always be idempotent.
-prop_Utils_commaSplitJoin s = Utils.commaJoin (Utils.sepSplit ',' s) == s
+prop_Utils_commaSplitJoin s =
+  Utils.commaJoin (Utils.sepSplit ',' s) ==? s
 
 -- | fromObjWithDefault, we test using the Maybe monad and an integer
 -- value.
 prop_Utils_fromObjWithDefault def_value random_key =
-    -- a missing key will be returned with the default
-    Utils.fromObjWithDefault [] random_key def_value == Just def_value &&
-    -- a found key will be returned as is, not with default
-    Utils.fromObjWithDefault [(random_key, J.showJSON def_value)]
-         random_key (def_value+1) == Just def_value
-        where _types = def_value :: Integer
+  -- a missing key will be returned with the default
+  JSON.fromObjWithDefault [] random_key def_value == Just def_value &&
+  -- a found key will be returned as is, not with default
+  JSON.fromObjWithDefault [(random_key, J.showJSON def_value)]
+       random_key (def_value+1) == Just def_value
+    where _types = def_value :: Integer
 
 -- | Test that functional if' behaves like the syntactic sugar if.
-prop_Utils_if'if :: Bool -> Int -> Int -> Bool
-prop_Utils_if'if cnd a b = Utils.if' cnd a b == if cnd then a else b
+prop_Utils_if'if :: Bool -> Int -> Int -> Gen Prop
+prop_Utils_if'if cnd a b =
+  Utils.if' cnd a b ==? if cnd then a else b
 
 -- | Test basic select functionality
-prop_Utils_select :: Int   -- ^ Default result
-                  -> [Int] -- ^ List of False values
-                  -> [Int] -- ^ List of True values
-                  -> Bool  -- ^ Test result
+prop_Utils_select :: Int      -- ^ Default result
+                  -> [Int]    -- ^ List of False values
+                  -> [Int]    -- ^ List of True values
+                  -> Gen Prop -- ^ Test result
 prop_Utils_select def lst1 lst2 =
-  Utils.select def cndlist == expectedresult
-  where expectedresult = Utils.if' (null lst2) def (head lst2)
-        flist = map (\e -> (False, e)) lst1
-        tlist = map (\e -> (True, e)) lst2
-        cndlist = flist ++ tlist
+  Utils.select def (flist ++ tlist) ==? expectedresult
+    where expectedresult = Utils.if' (null lst2) def (head lst2)
+          flist = zip (repeat False) lst1
+          tlist = zip (repeat True)  lst2
 
 -- | Test basic select functionality with undefined default
-prop_Utils_select_undefd :: [Int] -- ^ List of False values
+prop_Utils_select_undefd :: [Int]            -- ^ List of False values
                          -> NonEmptyList Int -- ^ List of True values
-                         -> Bool  -- ^ Test result
+                         -> Gen Prop         -- ^ Test result
 prop_Utils_select_undefd lst1 (NonEmpty lst2) =
-  Utils.select undefined cndlist == head lst2
-  where flist = map (\e -> (False, e)) lst1
-        tlist = map (\e -> (True, e)) lst2
-        cndlist = flist ++ tlist
+  Utils.select undefined (flist ++ tlist) ==? head lst2
+    where flist = zip (repeat False) lst1
+          tlist = zip (repeat True)  lst2
 
 -- | Test basic select functionality with undefined list values
-prop_Utils_select_undefv :: [Int] -- ^ List of False values
+prop_Utils_select_undefv :: [Int]            -- ^ List of False values
                          -> NonEmptyList Int -- ^ List of True values
-                         -> Bool  -- ^ Test result
+                         -> Gen Prop         -- ^ Test result
 prop_Utils_select_undefv lst1 (NonEmpty lst2) =
-  Utils.select undefined cndlist == head lst2
-  where flist = map (\e -> (False, e)) lst1
-        tlist = map (\e -> (True, e)) lst2
-        cndlist = flist ++ tlist ++ [undefined]
+  Utils.select undefined cndlist ==? head lst2
+    where flist = zip (repeat False) lst1
+          tlist = zip (repeat True)  lst2
+          cndlist = flist ++ tlist ++ [undefined]
 
 prop_Utils_parseUnit (NonNegative n) =
-    Utils.parseUnit (show n) == Types.Ok n &&
-    Utils.parseUnit (show n ++ "m") == Types.Ok n &&
-    (case Utils.parseUnit (show n ++ "M") of
-      Types.Ok m -> if n > 0
-                    then m < n  -- for positive values, X MB is less than X MiB
-                    else m == 0 -- but for 0, 0 MB == 0 MiB
-      Types.Bad _ -> False) &&
-    Utils.parseUnit (show n ++ "g") == Types.Ok (n*1024) &&
-    Utils.parseUnit (show n ++ "t") == Types.Ok (n*1048576) &&
-    Types.isBad (Utils.parseUnit (show n ++ "x")::Types.Result Int)
-    where _types = n::Int
+  Utils.parseUnit (show n) ==? Types.Ok n .&&.
+  Utils.parseUnit (show n ++ "m") ==? Types.Ok n .&&.
+  Utils.parseUnit (show n ++ "M") ==? Types.Ok (truncate n_mb::Int) .&&.
+  Utils.parseUnit (show n ++ "g") ==? Types.Ok (n*1024) .&&.
+  Utils.parseUnit (show n ++ "G") ==? Types.Ok (truncate n_gb::Int) .&&.
+  Utils.parseUnit (show n ++ "t") ==? Types.Ok (n*1048576) .&&.
+  Utils.parseUnit (show n ++ "T") ==? Types.Ok (truncate n_tb::Int) .&&.
+  printTestCase "Internal error/overflow?"
+    (n_mb >=0 && n_gb >= 0 && n_tb >= 0) .&&.
+  property (Types.isBad (Utils.parseUnit (show n ++ "x")::Types.Result Int))
+  where _types = (n::Int)
+        n_mb = (fromIntegral n::Rational) * 1000 * 1000 / 1024 / 1024
+        n_gb = n_mb * 1000
+        n_tb = n_gb * 1000
 
 -- | Test list for the Utils module.
-testUtils =
-  [ run prop_Utils_commaJoinSplit
-  , run prop_Utils_commaSplitJoin
-  , run prop_Utils_fromObjWithDefault
-  , run prop_Utils_if'if
-  , run prop_Utils_select
-  , run prop_Utils_select_undefd
-  , run prop_Utils_select_undefv
-  , run prop_Utils_parseUnit
-  ]
+testSuite "Utils"
+            [ 'prop_Utils_commaJoinSplit
+            , 'prop_Utils_commaSplitJoin
+            , 'prop_Utils_fromObjWithDefault
+            , 'prop_Utils_if'if
+            , 'prop_Utils_select
+            , 'prop_Utils_select_undefd
+            , 'prop_Utils_select_undefv
+            , 'prop_Utils_parseUnit
+            ]
 
 -- ** PeerMap tests
 
 -- | Make sure add is idempotent.
 prop_PeerMap_addIdempotent pmap key em =
-    fn puniq == fn (fn puniq)
+  fn puniq ==? fn (fn puniq)
     where _types = (pmap::PeerMap.PeerMap,
                     key::PeerMap.Key, em::PeerMap.Elem)
           fn = PeerMap.add key em
@@ -387,45 +613,47 @@ prop_PeerMap_addIdempotent pmap key em =
 
 -- | Make sure remove is idempotent.
 prop_PeerMap_removeIdempotent pmap key =
-    fn puniq == fn (fn puniq)
+  fn puniq ==? fn (fn puniq)
     where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
           fn = PeerMap.remove key
           puniq = PeerMap.accumArray const pmap
 
 -- | Make sure a missing item returns 0.
 prop_PeerMap_findMissing pmap key =
-    PeerMap.find key (PeerMap.remove key puniq) == 0
+  PeerMap.find key (PeerMap.remove key puniq) ==? 0
     where _types = (pmap::PeerMap.PeerMap, key::PeerMap.Key)
           puniq = PeerMap.accumArray const pmap
 
 -- | Make sure an added item is found.
 prop_PeerMap_addFind pmap key em =
-    PeerMap.find key (PeerMap.add key em puniq) == em
+  PeerMap.find key (PeerMap.add key em puniq) ==? em
     where _types = (pmap::PeerMap.PeerMap,
                     key::PeerMap.Key, em::PeerMap.Elem)
           puniq = PeerMap.accumArray const pmap
 
 -- | Manual check that maxElem returns the maximum indeed, or 0 for null.
 prop_PeerMap_maxElem pmap =
-    PeerMap.maxElem puniq == if null puniq then 0
-                             else (maximum . snd . unzip) puniq
+  PeerMap.maxElem puniq ==? if null puniq then 0
+                              else (maximum . snd . unzip) puniq
     where _types = pmap::PeerMap.PeerMap
           puniq = PeerMap.accumArray const pmap
 
 -- | List of tests for the PeerMap module.
-testPeerMap =
-    [ run prop_PeerMap_addIdempotent
-    , run prop_PeerMap_removeIdempotent
-    , run prop_PeerMap_maxElem
-    , run prop_PeerMap_addFind
-    , run prop_PeerMap_findMissing
-    ]
+testSuite "PeerMap"
+            [ 'prop_PeerMap_addIdempotent
+            , 'prop_PeerMap_removeIdempotent
+            , 'prop_PeerMap_maxElem
+            , 'prop_PeerMap_addFind
+            , 'prop_PeerMap_findMissing
+            ]
 
 -- ** Container tests
 
+-- we silence the following due to hlint bug fixed in later versions
+{-# ANN prop_Container_addTwo "HLint: ignore Avoid lambda" #-}
 prop_Container_addTwo cdata i1 i2 =
-    fn i1 i2 cont == fn i2 i1 cont &&
-       fn i1 i2 cont == fn i1 i2 (fn i1 i2 cont)
+  fn i1 i2 cont == fn i2 i1 cont &&
+  fn i1 i2 cont == fn i1 i2 (fn i1 i2 cont)
     where _types = (cdata::[Int],
                     i1::Int, i2::Int)
           cont = foldl (\c x -> Container.add x x c) Container.empty cdata
@@ -434,19 +662,19 @@ prop_Container_addTwo cdata i1 i2 =
 prop_Container_nameOf node =
   let nl = makeSmallCluster node 1
       fnode = head (Container.elems nl)
-  in Container.nameOf nl (Node.idx fnode) == Node.name fnode
+  in Container.nameOf nl (Node.idx fnode) ==? Node.name fnode
 
 -- | We test that in a cluster, given a random node, we can find it by
 -- its name and alias, as long as all names and aliases are unique,
 -- and that we fail to find a non-existing name.
-prop_Container_findByName node othername =
+prop_Container_findByName =
+  forAll (genNode (Just 1) Nothing) $ \node ->
   forAll (choose (1, 20)) $ \ cnt ->
   forAll (choose (0, cnt - 1)) $ \ fidx ->
-  forAll (vector cnt) $ \ names ->
-  (length . nub) (map fst names ++ map snd names) ==
-  length names * 2 &&
-  not (othername `elem` (map fst names ++ map snd names)) ==>
-  let nl = makeSmallCluster node cnt
+  forAll (genUniquesList (cnt * 2)) $ \ allnames ->
+  forAll (arbitrary `suchThat` (`notElem` allnames)) $ \ othername ->
+  let names = zip (take cnt allnames) (drop cnt allnames)
+      nl = makeSmallCluster node cnt
       nodes = Container.elems nl
       nodes' = map (\((name, alias), nn) -> (Node.idx nn,
                                              nn { Node.name = name,
@@ -454,343 +682,468 @@ prop_Container_findByName node othername =
                $ zip names nodes
       nl' = Container.fromList nodes'
       target = snd (nodes' !! fidx)
-  in Container.findByName nl' (Node.name target) == Just target &&
-     Container.findByName nl' (Node.alias target) == Just target &&
-     Container.findByName nl' othername == Nothing
+  in Container.findByName nl' (Node.name target) ==? Just target .&&.
+     Container.findByName nl' (Node.alias target) ==? Just target .&&.
+     printTestCase "Found non-existing name"
+       (isNothing (Container.findByName nl' othername))
 
-testContainer =
-    [ run prop_Container_addTwo
-    , run prop_Container_nameOf
-    , run prop_Container_findByName
-    ]
+testSuite "Container"
+            [ 'prop_Container_addTwo
+            , 'prop_Container_nameOf
+            , 'prop_Container_findByName
+            ]
 
 -- ** Instance tests
 
 -- Simple instance tests, we only have setter/getters
 
 prop_Instance_creat inst =
-    Instance.name inst == Instance.alias inst
+  Instance.name inst ==? Instance.alias inst
 
 prop_Instance_setIdx inst idx =
-    Instance.idx (Instance.setIdx inst idx) == idx
+  Instance.idx (Instance.setIdx inst idx) ==? idx
     where _types = (inst::Instance.Instance, idx::Types.Idx)
 
 prop_Instance_setName inst name =
-    Instance.name newinst == name &&
-    Instance.alias newinst == name
+  Instance.name newinst == name &&
+  Instance.alias newinst == name
     where _types = (inst::Instance.Instance, name::String)
           newinst = Instance.setName inst name
 
 prop_Instance_setAlias inst name =
-    Instance.name newinst == Instance.name inst &&
-    Instance.alias newinst == name
+  Instance.name newinst == Instance.name inst &&
+  Instance.alias newinst == name
     where _types = (inst::Instance.Instance, name::String)
           newinst = Instance.setAlias inst name
 
 prop_Instance_setPri inst pdx =
-    Instance.pNode (Instance.setPri inst pdx) == pdx
+  Instance.pNode (Instance.setPri inst pdx) ==? pdx
     where _types = (inst::Instance.Instance, pdx::Types.Ndx)
 
 prop_Instance_setSec inst sdx =
-    Instance.sNode (Instance.setSec inst sdx) == sdx
+  Instance.sNode (Instance.setSec inst sdx) ==? sdx
     where _types = (inst::Instance.Instance, sdx::Types.Ndx)
 
 prop_Instance_setBoth inst pdx sdx =
-    Instance.pNode si == pdx && Instance.sNode si == sdx
+  Instance.pNode si == pdx && Instance.sNode si == sdx
     where _types = (inst::Instance.Instance, pdx::Types.Ndx, sdx::Types.Ndx)
           si = Instance.setBoth inst pdx sdx
 
-prop_Instance_runStatus_True =
-    forAll (arbitrary `suchThat`
-            ((`elem` Instance.runningStates) . Instance.runSt))
-    Instance.running
-
-prop_Instance_runStatus_False inst =
-    let run_st = Instance.running inst
-        run_tx = Instance.runSt inst
-    in
-      run_tx `notElem` Instance.runningStates ==> not run_st
-
 prop_Instance_shrinkMG inst =
-    Instance.mem inst >= 2 * Types.unitMem ==>
-        case Instance.shrinkByType inst Types.FailMem of
-          Types.Ok inst' ->
-              Instance.mem inst' == Instance.mem inst - Types.unitMem
-          _ -> False
+  Instance.mem inst >= 2 * Types.unitMem ==>
+    case Instance.shrinkByType inst Types.FailMem of
+      Types.Ok inst' -> Instance.mem inst' == Instance.mem inst - Types.unitMem
+      _ -> False
 
 prop_Instance_shrinkMF inst =
-    forAll (choose (0, 2 * Types.unitMem - 1)) $ \mem ->
+  forAll (choose (0, 2 * Types.unitMem - 1)) $ \mem ->
     let inst' = inst { Instance.mem = mem}
     in Types.isBad $ Instance.shrinkByType inst' Types.FailMem
 
 prop_Instance_shrinkCG inst =
-    Instance.vcpus inst >= 2 * Types.unitCpu ==>
-        case Instance.shrinkByType inst Types.FailCPU of
-          Types.Ok inst' ->
-              Instance.vcpus inst' == Instance.vcpus inst - Types.unitCpu
-          _ -> False
+  Instance.vcpus inst >= 2 * Types.unitCpu ==>
+    case Instance.shrinkByType inst Types.FailCPU of
+      Types.Ok inst' ->
+        Instance.vcpus inst' == Instance.vcpus inst - Types.unitCpu
+      _ -> False
 
 prop_Instance_shrinkCF inst =
-    forAll (choose (0, 2 * Types.unitCpu - 1)) $ \vcpus ->
+  forAll (choose (0, 2 * Types.unitCpu - 1)) $ \vcpus ->
     let inst' = inst { Instance.vcpus = vcpus }
     in Types.isBad $ Instance.shrinkByType inst' Types.FailCPU
 
 prop_Instance_shrinkDG inst =
-    Instance.dsk inst >= 2 * Types.unitDsk ==>
-        case Instance.shrinkByType inst Types.FailDisk of
-          Types.Ok inst' ->
-              Instance.dsk inst' == Instance.dsk inst - Types.unitDsk
-          _ -> False
+  Instance.dsk inst >= 2 * Types.unitDsk ==>
+    case Instance.shrinkByType inst Types.FailDisk of
+      Types.Ok inst' ->
+        Instance.dsk inst' == Instance.dsk inst - Types.unitDsk
+      _ -> False
 
 prop_Instance_shrinkDF inst =
-    forAll (choose (0, 2 * Types.unitDsk - 1)) $ \dsk ->
+  forAll (choose (0, 2 * Types.unitDsk - 1)) $ \dsk ->
     let inst' = inst { Instance.dsk = dsk }
     in Types.isBad $ Instance.shrinkByType inst' Types.FailDisk
 
 prop_Instance_setMovable inst m =
-    Instance.movable inst' == m
+  Instance.movable inst' ==? m
     where inst' = Instance.setMovable inst m
 
-testInstance =
-    [ run prop_Instance_creat
-    , run prop_Instance_setIdx
-    , run prop_Instance_setName
-    , run prop_Instance_setAlias
-    , run prop_Instance_setPri
-    , run prop_Instance_setSec
-    , run prop_Instance_setBoth
-    , run prop_Instance_runStatus_True
-    , run prop_Instance_runStatus_False
-    , run prop_Instance_shrinkMG
-    , run prop_Instance_shrinkMF
-    , run prop_Instance_shrinkCG
-    , run prop_Instance_shrinkCF
-    , run prop_Instance_shrinkDG
-    , run prop_Instance_shrinkDF
-    , run prop_Instance_setMovable
-    ]
-
--- ** Text backend tests
+testSuite "Instance"
+            [ 'prop_Instance_creat
+            , 'prop_Instance_setIdx
+            , 'prop_Instance_setName
+            , 'prop_Instance_setAlias
+            , 'prop_Instance_setPri
+            , 'prop_Instance_setSec
+            , 'prop_Instance_setBoth
+            , 'prop_Instance_shrinkMG
+            , 'prop_Instance_shrinkMF
+            , 'prop_Instance_shrinkCG
+            , 'prop_Instance_shrinkCF
+            , 'prop_Instance_shrinkDG
+            , 'prop_Instance_shrinkDF
+            , 'prop_Instance_setMovable
+            ]
+
+-- ** Backends
+
+-- *** Text backend tests
 
 -- Instance text loader tests
 
 prop_Text_Load_Instance name mem dsk vcpus status
                         (NonEmpty pnode) snode
-                        (NonNegative pdx) (NonNegative sdx) autobal dt =
-    pnode /= snode && pdx /= sdx ==>
-    let vcpus_s = show vcpus
-        dsk_s = show dsk
-        mem_s = show mem
-        ndx = if null snode
+                        (NonNegative pdx) (NonNegative sdx) autobal dt su =
+  pnode /= snode && pdx /= sdx ==>
+  let vcpus_s = show vcpus
+      dsk_s = show dsk
+      mem_s = show mem
+      su_s = show su
+      status_s = Types.instanceStatusToRaw status
+      ndx = if null snode
               then [(pnode, pdx)]
               else [(pnode, pdx), (snode, sdx)]
-        nl = Data.Map.fromList ndx
-        tags = ""
-        sbal = if autobal then "Y" else "N"
-        sdt = Types.dtToString dt
-        inst = Text.loadInst nl
-               [name, mem_s, dsk_s, vcpus_s, status,
-                sbal, pnode, snode, sdt, tags]
-        fail1 = Text.loadInst nl
-               [name, mem_s, dsk_s, vcpus_s, status,
-                sbal, pnode, pnode, tags]
-        _types = ( name::String, mem::Int, dsk::Int
-                 , vcpus::Int, status::String
-                 , snode::String
-                 , autobal::Bool)
-    in
-      case inst of
-        Types.Bad msg -> printTestCase ("Failed to load instance: " ++ msg)
-                         False
-        Types.Ok (_, i) -> printTestCase "Mismatch in some field while\
-                                         \ loading the instance" $
-            Instance.name i == name &&
-            Instance.vcpus i == vcpus &&
-            Instance.mem i == mem &&
-            Instance.pNode i == pdx &&
-            Instance.sNode i == (if null snode
-                                 then Node.noSecondary
-                                 else sdx) &&
-            Instance.autoBalance i == autobal &&
-            Types.isBad fail1
+      nl = Data.Map.fromList ndx
+      tags = ""
+      sbal = if autobal then "Y" else "N"
+      sdt = Types.diskTemplateToRaw dt
+      inst = Text.loadInst nl
+             [name, mem_s, dsk_s, vcpus_s, status_s,
+              sbal, pnode, snode, sdt, tags, su_s]
+      fail1 = Text.loadInst nl
+              [name, mem_s, dsk_s, vcpus_s, status_s,
+               sbal, pnode, pnode, tags]
+      _types = ( name::String, mem::Int, dsk::Int
+               , vcpus::Int, status::Types.InstanceStatus
+               , snode::String
+               , autobal::Bool)
+  in case inst of
+       Types.Bad msg -> failTest $ "Failed to load instance: " ++ msg
+       Types.Ok (_, i) -> printTestCase "Mismatch in some field while\
+                                        \ loading the instance" $
+               Instance.name i == name &&
+               Instance.vcpus i == vcpus &&
+               Instance.mem i == mem &&
+               Instance.pNode i == pdx &&
+               Instance.sNode i == (if null snode
+                                      then Node.noSecondary
+                                      else sdx) &&
+               Instance.autoBalance i == autobal &&
+               Instance.spindleUse i == su &&
+               Types.isBad fail1
 
 prop_Text_Load_InstanceFail ktn fields =
-    length fields /= 10 ==>
+  length fields /= 10 && length fields /= 11 ==>
     case Text.loadInst nl fields of
-      Types.Ok _ -> printTestCase "Managed to load instance from invalid\
-                                  \ data" False
+      Types.Ok _ -> failTest "Managed to load instance from invalid data"
       Types.Bad msg -> printTestCase ("Unrecognised error message: " ++ msg) $
                        "Invalid/incomplete instance data: '" `isPrefixOf` msg
     where nl = Data.Map.fromList ktn
 
 prop_Text_Load_Node name tm nm fm td fd tc fo =
-    let conv v = if v < 0
-                    then "?"
-                    else show v
-        tm_s = conv tm
-        nm_s = conv nm
-        fm_s = conv fm
-        td_s = conv td
-        fd_s = conv fd
-        tc_s = conv tc
-        fo_s = if fo
+  let conv v = if v < 0
+                 then "?"
+                 else show v
+      tm_s = conv tm
+      nm_s = conv nm
+      fm_s = conv fm
+      td_s = conv td
+      fd_s = conv fd
+      tc_s = conv tc
+      fo_s = if fo
                then "Y"
                else "N"
-        any_broken = any (< 0) [tm, nm, fm, td, fd, tc]
-        gid = Group.uuid defGroup
-    in case Text.loadNode defGroupAssoc
-           [name, tm_s, nm_s, fm_s, td_s, fd_s, tc_s, fo_s, gid] of
-         Nothing -> False
-         Just (name', node) ->
-             if fo || any_broken
-             then Node.offline node
-             else Node.name node == name' && name' == name &&
-                  Node.alias node == name &&
-                  Node.tMem node == fromIntegral tm &&
-                  Node.nMem node == nm &&
-                  Node.fMem node == fm &&
-                  Node.tDsk node == fromIntegral td &&
-                  Node.fDsk node == fd &&
-                  Node.tCpu node == fromIntegral tc
+      any_broken = any (< 0) [tm, nm, fm, td, fd, tc]
+      gid = Group.uuid defGroup
+  in case Text.loadNode defGroupAssoc
+       [name, tm_s, nm_s, fm_s, td_s, fd_s, tc_s, fo_s, gid] of
+       Nothing -> False
+       Just (name', node) ->
+         if fo || any_broken
+           then Node.offline node
+           else Node.name node == name' && name' == name &&
+                Node.alias node == name &&
+                Node.tMem node == fromIntegral tm &&
+                Node.nMem node == nm &&
+                Node.fMem node == fm &&
+                Node.tDsk node == fromIntegral td &&
+                Node.fDsk node == fd &&
+                Node.tCpu node == fromIntegral tc
 
 prop_Text_Load_NodeFail fields =
-    length fields /= 8 ==> isNothing $ Text.loadNode Data.Map.empty fields
-
-prop_Text_NodeLSIdempotent node =
+  length fields /= 8 ==> isNothing $ Text.loadNode Data.Map.empty fields
+
+prop_Text_NodeLSIdempotent =
+  forAll (genNode (Just 1) Nothing) $ \node ->
+  -- override failN1 to what loadNode returns by default
+  let n = Node.setPolicy Types.defIPolicy $
+          node { Node.failN1 = True, Node.offline = False }
+  in
     (Text.loadNode defGroupAssoc.
-         Utils.sepSplit '|' . Text.serializeNode defGroupList) n ==
+         Utils.sepSplit '|' . Text.serializeNode defGroupList) n ==?
     Just (Node.name n, n)
-    -- override failN1 to what loadNode returns by default
-    where n = node { Node.failN1 = True, Node.offline = False }
 
-testText =
-    [ run prop_Text_Load_Instance
-    , run prop_Text_Load_InstanceFail
-    , run prop_Text_Load_Node
-    , run prop_Text_Load_NodeFail
-    , run prop_Text_NodeLSIdempotent
-    ]
+prop_Text_ISpecIdempotent ispec =
+  case Text.loadISpec "dummy" . Utils.sepSplit ',' .
+       Text.serializeISpec $ ispec of
+    Types.Bad msg -> failTest $ "Failed to load ispec: " ++ msg
+    Types.Ok ispec' -> ispec ==? ispec'
+
+prop_Text_IPolicyIdempotent ipol =
+  case Text.loadIPolicy . Utils.sepSplit '|' $
+       Text.serializeIPolicy owner ipol of
+    Types.Bad msg -> failTest $ "Failed to load ispec: " ++ msg
+    Types.Ok res -> (owner, ipol) ==? res
+  where owner = "dummy"
+
+-- | This property, while being in the text tests, does more than just
+-- test end-to-end the serialisation and loading back workflow; it
+-- also tests the Loader.mergeData and the actuall
+-- Cluster.iterateAlloc (for well-behaving w.r.t. instance
+-- allocations, not for the business logic). As such, it's a quite
+-- complex and slow test, and that's the reason we restrict it to
+-- small cluster sizes.
+prop_Text_CreateSerialise =
+  forAll genTags $ \ctags ->
+  forAll (choose (1, 20)) $ \maxiter ->
+  forAll (choose (2, 10)) $ \count ->
+  forAll genOnlineNode $ \node ->
+  forAll (genInstanceSmallerThanNode node) $ \inst ->
+  let nl = makeSmallCluster node count
+      reqnodes = Instance.requiredNodes $ Instance.diskTemplate inst
+  in case Cluster.genAllocNodes defGroupList nl reqnodes True >>= \allocn ->
+     Cluster.iterateAlloc nl Container.empty (Just maxiter) inst allocn [] []
+     of
+       Types.Bad msg -> failTest $ "Failed to allocate: " ++ msg
+       Types.Ok (_, _, _, [], _) -> printTestCase
+                                    "Failed to allocate: no allocations" False
+       Types.Ok (_, nl', il', _, _) ->
+         let cdata = Loader.ClusterData defGroupList nl' il' ctags
+                     Types.defIPolicy
+             saved = Text.serializeCluster cdata
+         in case Text.parseData saved >>= Loader.mergeData [] [] [] [] of
+              Types.Bad msg -> failTest $ "Failed to load/merge: " ++ msg
+              Types.Ok (Loader.ClusterData gl2 nl2 il2 ctags2 cpol2) ->
+                ctags ==? ctags2 .&&.
+                Types.defIPolicy ==? cpol2 .&&.
+                il' ==? il2 .&&.
+                defGroupList ==? gl2 .&&.
+                nl' ==? nl2
+
+testSuite "Text"
+            [ 'prop_Text_Load_Instance
+            , 'prop_Text_Load_InstanceFail
+            , 'prop_Text_Load_Node
+            , 'prop_Text_Load_NodeFail
+            , 'prop_Text_NodeLSIdempotent
+            , 'prop_Text_ISpecIdempotent
+            , 'prop_Text_IPolicyIdempotent
+            , 'prop_Text_CreateSerialise
+            ]
+
+-- *** Simu backend
+
+-- | Generates a tuple of specs for simulation.
+genSimuSpec :: Gen (String, Int, Int, Int, Int)
+genSimuSpec = do
+  pol <- elements [C.allocPolicyPreferred,
+                   C.allocPolicyLastResort, C.allocPolicyUnallocable,
+                  "p", "a", "u"]
+ -- should be reasonable (nodes/group), bigger values only complicate
+ -- the display of failed tests, and we don't care (in this particular
+ -- test) about big node groups
+  nodes <- choose (0, 20)
+  dsk <- choose (0, maxDsk)
+  mem <- choose (0, maxMem)
+  cpu <- choose (0, maxCpu)
+  return (pol, nodes, dsk, mem, cpu)
+
+-- | Checks that given a set of corrects specs, we can load them
+-- successfully, and that at high-level the values look right.
+prop_SimuLoad =
+  forAll (choose (0, 10)) $ \ngroups ->
+  forAll (replicateM ngroups genSimuSpec) $ \specs ->
+  let strspecs = map (\(p, n, d, m, c) -> printf "%s,%d,%d,%d,%d"
+                                          p n d m c::String) specs
+      totnodes = sum $ map (\(_, n, _, _, _) -> n) specs
+      mdc_in = concatMap (\(_, n, d, m, c) ->
+                            replicate n (fromIntegral m, fromIntegral d,
+                                         fromIntegral c,
+                                         fromIntegral m, fromIntegral d)) specs
+  in case Simu.parseData strspecs of
+       Types.Bad msg -> failTest $ "Failed to load specs: " ++ msg
+       Types.Ok (Loader.ClusterData gl nl il tags ipol) ->
+         let nodes = map snd $ IntMap.toAscList nl
+             nidx = map Node.idx nodes
+             mdc_out = map (\n -> (Node.tMem n, Node.tDsk n, Node.tCpu n,
+                                   Node.fMem n, Node.fDsk n)) nodes
+         in
+         Container.size gl ==? ngroups .&&.
+         Container.size nl ==? totnodes .&&.
+         Container.size il ==? 0 .&&.
+         length tags ==? 0 .&&.
+         ipol ==? Types.defIPolicy .&&.
+         nidx ==? [1..totnodes] .&&.
+         mdc_in ==? mdc_out .&&.
+         map Group.iPolicy (Container.elems gl) ==?
+             replicate ngroups Types.defIPolicy
+
+testSuite "Simu"
+            [ 'prop_SimuLoad
+            ]
 
 -- ** Node tests
 
 prop_Node_setAlias node name =
-    Node.name newnode == Node.name node &&
-    Node.alias newnode == name
+  Node.name newnode == Node.name node &&
+  Node.alias newnode == name
     where _types = (node::Node.Node, name::String)
           newnode = Node.setAlias node name
 
 prop_Node_setOffline node status =
-    Node.offline newnode == status
+  Node.offline newnode ==? status
     where newnode = Node.setOffline node status
 
 prop_Node_setXmem node xm =
-    Node.xMem newnode == xm
+  Node.xMem newnode ==? xm
     where newnode = Node.setXmem node xm
 
 prop_Node_setMcpu node mc =
-    Node.mCpu newnode == mc
+  Types.iPolicyVcpuRatio (Node.iPolicy newnode) ==? mc
     where newnode = Node.setMcpu node mc
 
 -- | Check that an instance add with too high memory or disk will be
 -- rejected.
-prop_Node_addPriFM node inst = Instance.mem inst >= Node.fMem node &&
-                               not (Node.failN1 node)
-                               ==>
-                               case Node.addPri node inst'' of
-                                 Types.OpFail Types.FailMem -> True
-                                 _ -> False
-    where _types = (node::Node.Node, inst::Instance.Instance)
-          inst' = setInstanceSmallerThanNode node inst
-          inst'' = inst' { Instance.mem = Instance.mem inst }
-
-prop_Node_addPriFD node inst = Instance.dsk inst >= Node.fDsk node &&
-                               not (Node.failN1 node)
-                               ==>
-                               case Node.addPri node inst'' of
-                                 Types.OpFail Types.FailDisk -> True
-                                 _ -> False
-    where _types = (node::Node.Node, inst::Instance.Instance)
-          inst' = setInstanceSmallerThanNode node inst
-          inst'' = inst' { Instance.dsk = Instance.dsk inst }
-
-prop_Node_addPriFC node inst (Positive extra) =
-    not (Node.failN1 node) ==>
-        case Node.addPri node inst'' of
-          Types.OpFail Types.FailCPU -> True
-          _ -> False
-    where _types = (node::Node.Node, inst::Instance.Instance)
-          inst' = setInstanceSmallerThanNode node inst
-          inst'' = inst' { Instance.vcpus = Node.availCpu node + extra }
+prop_Node_addPriFM node inst =
+  Instance.mem inst >= Node.fMem node && not (Node.failN1 node) &&
+  not (Instance.isOffline inst) ==>
+  case Node.addPri node inst'' of
+    Types.OpFail Types.FailMem -> True
+    _ -> False
+  where _types = (node::Node.Node, inst::Instance.Instance)
+        inst' = setInstanceSmallerThanNode node inst
+        inst'' = inst' { Instance.mem = Instance.mem inst }
+
+-- | Check that adding a primary instance with too much disk fails
+-- with type FailDisk.
+prop_Node_addPriFD node inst =
+  forAll (elements Instance.localStorageTemplates) $ \dt ->
+  Instance.dsk inst >= Node.fDsk node && not (Node.failN1 node) ==>
+  let inst' = setInstanceSmallerThanNode node inst
+      inst'' = inst' { Instance.dsk = Instance.dsk inst
+                     , Instance.diskTemplate = dt }
+  in case Node.addPri node inst'' of
+       Types.OpFail Types.FailDisk -> True
+       _ -> False
+
+-- | Check that adding a primary instance with too many VCPUs fails
+-- with type FailCPU.
+prop_Node_addPriFC =
+  forAll (choose (1, maxCpu)) $ \extra ->
+  forAll genOnlineNode $ \node ->
+  forAll (arbitrary `suchThat` Instance.notOffline) $ \inst ->
+  let inst' = setInstanceSmallerThanNode node inst
+      inst'' = inst' { Instance.vcpus = Node.availCpu node + extra }
+  in case Node.addPri node inst'' of
+       Types.OpFail Types.FailCPU -> property True
+       v -> failTest $ "Expected OpFail FailCPU, but got " ++ show v
 
 -- | Check that an instance add with too high memory or disk will be
 -- rejected.
 prop_Node_addSec node inst pdx =
-    (Instance.mem inst >= (Node.fMem node - Node.rMem node) ||
-     Instance.dsk inst >= Node.fDsk node) &&
-    not (Node.failN1 node)
-    ==> isFailure (Node.addSec node inst pdx)
+  ((Instance.mem inst >= (Node.fMem node - Node.rMem node) &&
+    not (Instance.isOffline inst)) ||
+   Instance.dsk inst >= Node.fDsk node) &&
+  not (Node.failN1 node) ==>
+      isFailure (Node.addSec node inst pdx)
         where _types = (node::Node.Node, inst::Instance.Instance, pdx::Int)
 
+-- | Check that an offline instance with reasonable disk size but
+-- extra mem/cpu can always be added.
+prop_Node_addOfflinePri (NonNegative extra_mem) (NonNegative extra_cpu) =
+  forAll genOnlineNode $ \node ->
+  forAll (genInstanceSmallerThanNode node) $ \inst ->
+  let inst' = inst { Instance.runSt = Types.AdminOffline
+                   , Instance.mem = Node.availMem node + extra_mem
+                   , Instance.vcpus = Node.availCpu node + extra_cpu }
+  in case Node.addPri node inst' of
+       Types.OpGood _ -> property True
+       v -> failTest $ "Expected OpGood, but got: " ++ show v
+
+-- | Check that an offline instance with reasonable disk size but
+-- extra mem/cpu can always be added.
+prop_Node_addOfflineSec (NonNegative extra_mem) (NonNegative extra_cpu) pdx =
+  forAll genOnlineNode $ \node ->
+  forAll (genInstanceSmallerThanNode node) $ \inst ->
+  let inst' = inst { Instance.runSt = Types.AdminOffline
+                   , Instance.mem = Node.availMem node + extra_mem
+                   , Instance.vcpus = Node.availCpu node + extra_cpu
+                   , Instance.diskTemplate = Types.DTDrbd8 }
+  in case Node.addSec node inst' pdx of
+       Types.OpGood _ -> property True
+       v -> failTest $ "Expected OpGood/OpGood, but got: " ++ show v
+
 -- | Checks for memory reservation changes.
 prop_Node_rMem inst =
-    forAll (arbitrary `suchThat` ((> Types.unitMem) . Node.fMem)) $ \node ->
-    -- ab = auto_balance, nb = non-auto_balance
-    -- we use -1 as the primary node of the instance
-    let inst' = inst { Instance.pNode = -1, Instance.autoBalance = True }
-        inst_ab = setInstanceSmallerThanNode node inst'
-        inst_nb = inst_ab { Instance.autoBalance = False }
-        -- now we have the two instances, identical except the
-        -- autoBalance attribute
-        orig_rmem = Node.rMem node
-        inst_idx = Instance.idx inst_ab
-        node_add_ab = Node.addSec node inst_ab (-1)
-        node_add_nb = Node.addSec node inst_nb (-1)
-        node_del_ab = liftM (`Node.removeSec` inst_ab) node_add_ab
-        node_del_nb = liftM (`Node.removeSec` inst_nb) node_add_nb
-    in case (node_add_ab, node_add_nb, node_del_ab, node_del_nb) of
-         (Types.OpGood a_ab, Types.OpGood a_nb,
-          Types.OpGood d_ab, Types.OpGood d_nb) ->
-             printTestCase "Consistency checks failed" $
-             Node.rMem a_ab >  orig_rmem &&
-             Node.rMem a_ab - orig_rmem == Instance.mem inst_ab &&
-             Node.rMem a_nb == orig_rmem &&
-             Node.rMem d_ab == orig_rmem &&
-             Node.rMem d_nb == orig_rmem &&
-             -- this is not related to rMem, but as good a place to
-             -- test as any
-             inst_idx `elem` Node.sList a_ab &&
-             not (inst_idx `elem` Node.sList d_ab)
-         x -> printTestCase ("Failed to add/remove instances: " ++ show x)
-              False
+  not (Instance.isOffline inst) ==>
+  forAll (genOnlineNode `suchThat` ((> Types.unitMem) . Node.fMem)) $ \node ->
+  -- ab = auto_balance, nb = non-auto_balance
+  -- we use -1 as the primary node of the instance
+  let inst' = inst { Instance.pNode = -1, Instance.autoBalance = True
+                   , Instance.diskTemplate = Types.DTDrbd8 }
+      inst_ab = setInstanceSmallerThanNode node inst'
+      inst_nb = inst_ab { Instance.autoBalance = False }
+      -- now we have the two instances, identical except the
+      -- autoBalance attribute
+      orig_rmem = Node.rMem node
+      inst_idx = Instance.idx inst_ab
+      node_add_ab = Node.addSec node inst_ab (-1)
+      node_add_nb = Node.addSec node inst_nb (-1)
+      node_del_ab = liftM (`Node.removeSec` inst_ab) node_add_ab
+      node_del_nb = liftM (`Node.removeSec` inst_nb) node_add_nb
+  in case (node_add_ab, node_add_nb, node_del_ab, node_del_nb) of
+       (Types.OpGood a_ab, Types.OpGood a_nb,
+        Types.OpGood d_ab, Types.OpGood d_nb) ->
+         printTestCase "Consistency checks failed" $
+           Node.rMem a_ab >  orig_rmem &&
+           Node.rMem a_ab - orig_rmem == Instance.mem inst_ab &&
+           Node.rMem a_nb == orig_rmem &&
+           Node.rMem d_ab == orig_rmem &&
+           Node.rMem d_nb == orig_rmem &&
+           -- this is not related to rMem, but as good a place to
+           -- test as any
+           inst_idx `elem` Node.sList a_ab &&
+           inst_idx `notElem` Node.sList d_ab
+       x -> failTest $ "Failed to add/remove instances: " ++ show x
 
 -- | Check mdsk setting.
 prop_Node_setMdsk node mx =
-    Node.loDsk node' >= 0 &&
-    fromIntegral (Node.loDsk node') <= Node.tDsk node &&
-    Node.availDisk node' >= 0 &&
-    Node.availDisk node' <= Node.fDsk node' &&
-    fromIntegral (Node.availDisk node') <= Node.tDsk node' &&
-    Node.mDsk node' == mx'
+  Node.loDsk node' >= 0 &&
+  fromIntegral (Node.loDsk node') <= Node.tDsk node &&
+  Node.availDisk node' >= 0 &&
+  Node.availDisk node' <= Node.fDsk node' &&
+  fromIntegral (Node.availDisk node') <= Node.tDsk node' &&
+  Node.mDsk node' == mx'
     where _types = (node::Node.Node, mx::SmallRatio)
           node' = Node.setMdsk node mx'
           SmallRatio mx' = mx
 
 -- Check tag maps
-prop_Node_tagMaps_idempotent tags =
-    Node.delTags (Node.addTags m tags) tags == m
+prop_Node_tagMaps_idempotent =
+  forAll genTags $ \tags ->
+  Node.delTags (Node.addTags m tags) tags ==? m
     where m = Data.Map.empty
 
-prop_Node_tagMaps_reject tags =
-    not (null tags) ==>
-    any (\t -> Node.rejectAddTags m [t]) tags
-    where m = Node.addTags Data.Map.empty tags
+prop_Node_tagMaps_reject =
+  forAll (genTags `suchThat` (not . null)) $ \tags ->
+  let m = Node.addTags Data.Map.empty tags
+  in all (\t -> Node.rejectAddTags m [t]) tags
 
 prop_Node_showField node =
   forAll (elements Node.defaultFields) $ \ field ->
   fst (Node.showHeader field) /= Types.unknownField &&
   Node.showField node field /= Types.unknownField
 
-
 prop_Node_computeGroups nodes =
   let ng = Node.computeGroups nodes
       onlyuuid = map fst ng
@@ -799,139 +1152,231 @@ prop_Node_computeGroups nodes =
      length (nub onlyuuid) == length onlyuuid &&
      (null nodes || not (null ng))
 
-testNode =
-    [ run prop_Node_setAlias
-    , run prop_Node_setOffline
-    , run prop_Node_setMcpu
-    , run prop_Node_setXmem
-    , run prop_Node_addPriFM
-    , run prop_Node_addPriFD
-    , run prop_Node_addPriFC
-    , run prop_Node_addSec
-    , run prop_Node_rMem
-    , run prop_Node_setMdsk
-    , run prop_Node_tagMaps_idempotent
-    , run prop_Node_tagMaps_reject
-    , run prop_Node_showField
-    , run prop_Node_computeGroups
-    ]
-
+-- Check idempotence of add/remove operations
+prop_Node_addPri_idempotent =
+  forAll genOnlineNode $ \node ->
+  forAll (genInstanceSmallerThanNode node) $ \inst ->
+  case Node.addPri node inst of
+    Types.OpGood node' -> Node.removePri node' inst ==? node
+    _ -> failTest "Can't add instance"
+
+prop_Node_addSec_idempotent =
+  forAll genOnlineNode $ \node ->
+  forAll (genInstanceSmallerThanNode node) $ \inst ->
+  let pdx = Node.idx node + 1
+      inst' = Instance.setPri inst pdx
+      inst'' = inst' { Instance.diskTemplate = Types.DTDrbd8 }
+  in case Node.addSec node inst'' pdx of
+       Types.OpGood node' -> Node.removeSec node' inst'' ==? node
+       _ -> failTest "Can't add instance"
+
+testSuite "Node"
+            [ 'prop_Node_setAlias
+            , 'prop_Node_setOffline
+            , 'prop_Node_setMcpu
+            , 'prop_Node_setXmem
+            , 'prop_Node_addPriFM
+            , 'prop_Node_addPriFD
+            , 'prop_Node_addPriFC
+            , 'prop_Node_addSec
+            , 'prop_Node_addOfflinePri
+            , 'prop_Node_addOfflineSec
+            , 'prop_Node_rMem
+            , 'prop_Node_setMdsk
+            , 'prop_Node_tagMaps_idempotent
+            , 'prop_Node_tagMaps_reject
+            , 'prop_Node_showField
+            , 'prop_Node_computeGroups
+            , 'prop_Node_addPri_idempotent
+            , 'prop_Node_addSec_idempotent
+            ]
 
 -- ** Cluster tests
 
 -- | Check that the cluster score is close to zero for a homogeneous
 -- cluster.
 prop_Score_Zero node =
-    forAll (choose (1, 1024)) $ \count ->
+  forAll (choose (1, 1024)) $ \count ->
     (not (Node.offline node) && not (Node.failN1 node) && (count > 0) &&
      (Node.tDsk node > 0) && (Node.tMem node > 0)) ==>
-    let fn = Node.buildPeers node Container.empty
-        nlst = replicate count fn
-        score = Cluster.compCVNodes nlst
-    -- we can't say == 0 here as the floating point errors accumulate;
-    -- this should be much lower than the default score in CLI.hs
-    in score <= 1e-12
+  let fn = Node.buildPeers node Container.empty
+      nlst = replicate count fn
+      score = Cluster.compCVNodes nlst
+  -- we can't say == 0 here as the floating point errors accumulate;
+  -- this should be much lower than the default score in CLI.hs
+  in score <= 1e-12
 
 -- | Check that cluster stats are sane.
-prop_CStats_sane node =
-    forAll (choose (1, 1024)) $ \count ->
-    (not (Node.offline node) && not (Node.failN1 node) &&
-     (Node.availDisk node > 0) && (Node.availMem node > 0)) ==>
-    let fn = Node.buildPeers node Container.empty
-        nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
-        nl = Container.fromList nlst
-        cstats = Cluster.totalResources nl
-    in Cluster.csAdsk cstats >= 0 &&
-       Cluster.csAdsk cstats <= Cluster.csFdsk cstats
+prop_CStats_sane =
+  forAll (choose (1, 1024)) $ \count ->
+  forAll genOnlineNode $ \node ->
+  let fn = Node.buildPeers node Container.empty
+      nlst = zip [1..] $ replicate count fn::[(Types.Ndx, Node.Node)]
+      nl = Container.fromList nlst
+      cstats = Cluster.totalResources nl
+  in Cluster.csAdsk cstats >= 0 &&
+     Cluster.csAdsk cstats <= Cluster.csFdsk cstats
 
 -- | Check that one instance is allocated correctly, without
 -- rebalances needed.
-prop_ClusterAlloc_sane node inst =
-    forAll (choose (5, 20)) $ \count ->
-    not (Node.offline node)
-            && not (Node.failN1 node)
-            && Node.availDisk node > 0
-            && Node.availMem node > 0
-            ==>
-    let nl = makeSmallCluster node count
-        il = Container.empty
-        inst' = setInstanceSmallerThanNode node inst
-    in case Cluster.genAllocNodes defGroupList nl 2 True >>=
-       Cluster.tryAlloc nl il inst' of
-         Types.Bad _ -> False
-         Types.Ok as ->
-             case Cluster.asSolutions as of
-               [] -> False
-               (xnl, xi, _, cv):[] ->
-                   let il' = Container.add (Instance.idx xi) xi il
-                       tbl = Cluster.Table xnl il' cv []
-                   in not (canBalance tbl True True False)
-               _ -> False
+prop_ClusterAlloc_sane inst =
+  forAll (choose (5, 20)) $ \count ->
+  forAll genOnlineNode $ \node ->
+  let (nl, il, inst') = makeSmallEmptyCluster node count inst
+      reqnodes = Instance.requiredNodes $ Instance.diskTemplate inst
+  in case Cluster.genAllocNodes defGroupList nl reqnodes True >>=
+     Cluster.tryAlloc nl il inst' of
+       Types.Bad _ -> False
+       Types.Ok as ->
+         case Cluster.asSolution as of
+           Nothing -> False
+           Just (xnl, xi, _, cv) ->
+             let il' = Container.add (Instance.idx xi) xi il
+                 tbl = Cluster.Table xnl il' cv []
+             in not (canBalance tbl True True False)
 
 -- | Checks that on a 2-5 node cluster, we can allocate a random
 -- instance spec via tiered allocation (whatever the original instance
--- spec), on either one or two nodes.
-prop_ClusterCanTieredAlloc node inst =
-    forAll (choose (2, 5)) $ \count ->
-    forAll (choose (1, 2)) $ \rqnodes ->
-    not (Node.offline node)
-            && not (Node.failN1 node)
-            && isNodeBig node 4
-            ==>
-    let nl = makeSmallCluster node count
-        il = Container.empty
-        allocnodes = Cluster.genAllocNodes defGroupList nl rqnodes True
-    in case allocnodes >>= \allocnodes' ->
-        Cluster.tieredAlloc nl il (Just 1) inst allocnodes' [] [] of
-         Types.Bad _ -> False
-         Types.Ok (_, _, il', ixes, cstats) -> not (null ixes) &&
-                                      IntMap.size il' == length ixes &&
-                                      length ixes == length cstats
+-- spec), on either one or two nodes. Furthermore, we test that
+-- computed allocation statistics are correct.
+prop_ClusterCanTieredAlloc inst =
+  forAll (choose (2, 5)) $ \count ->
+  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
+  let nl = makeSmallCluster node count
+      il = Container.empty
+      rqnodes = Instance.requiredNodes $ Instance.diskTemplate inst
+      allocnodes = Cluster.genAllocNodes defGroupList nl rqnodes True
+  in case allocnodes >>= \allocnodes' ->
+    Cluster.tieredAlloc nl il (Just 1) inst allocnodes' [] [] of
+       Types.Bad msg -> failTest $ "Failed to tiered alloc: " ++ msg
+       Types.Ok (_, nl', il', ixes, cstats) ->
+         let (ai_alloc, ai_pool, ai_unav) =
+               Cluster.computeAllocationDelta
+                (Cluster.totalResources nl)
+                (Cluster.totalResources nl')
+             all_nodes = Container.elems nl
+         in property (not (null ixes)) .&&.
+            IntMap.size il' ==? length ixes .&&.
+            length ixes ==? length cstats .&&.
+            sum (map Types.allocInfoVCpus [ai_alloc, ai_pool, ai_unav]) ==?
+              sum (map Node.hiCpu all_nodes) .&&.
+            sum (map Types.allocInfoNCpus [ai_alloc, ai_pool, ai_unav]) ==?
+              sum (map Node.tCpu all_nodes) .&&.
+            sum (map Types.allocInfoMem [ai_alloc, ai_pool, ai_unav]) ==?
+              truncate (sum (map Node.tMem all_nodes)) .&&.
+            sum (map Types.allocInfoDisk [ai_alloc, ai_pool, ai_unav]) ==?
+              truncate (sum (map Node.tDsk all_nodes))
+
+-- | Helper function to create a cluster with the given range of nodes
+-- and allocate an instance on it.
+genClusterAlloc count node inst =
+  let nl = makeSmallCluster node count
+      reqnodes = Instance.requiredNodes $ Instance.diskTemplate inst
+  in case Cluster.genAllocNodes defGroupList nl reqnodes True >>=
+     Cluster.tryAlloc nl Container.empty inst of
+       Types.Bad _ -> Types.Bad "Can't allocate"
+       Types.Ok as ->
+         case Cluster.asSolution as of
+           Nothing -> Types.Bad "Empty solution?"
+           Just (xnl, xi, _, _) ->
+             let xil = Container.add (Instance.idx xi) xi Container.empty
+             in Types.Ok (xnl, xil, xi)
 
 -- | Checks that on a 4-8 node cluster, once we allocate an instance,
--- we can also evacuate it.
-prop_ClusterAllocEvac node inst =
-    forAll (choose (4, 8)) $ \count ->
-    not (Node.offline node)
-            && not (Node.failN1 node)
-            && isNodeBig node 4
-            ==>
-    let nl = makeSmallCluster node count
-        il = Container.empty
-        inst' = setInstanceSmallerThanNode node inst
-    in case Cluster.genAllocNodes defGroupList nl 2 True >>=
-       Cluster.tryAlloc nl il inst' of
-         Types.Bad _ -> False
-         Types.Ok as ->
-             case Cluster.asSolutions as of
-               [] -> False
-               (xnl, xi, _, _):[] ->
-                   let sdx = Instance.sNode xi
-                       il' = Container.add (Instance.idx xi) xi il
-                   in case Cluster.tryEvac xnl il' [Instance.idx xi] [sdx] of
-                        Just _ -> True
-                        _ -> False
-               _ -> False
+-- we can also relocate it.
+prop_ClusterAllocRelocate =
+  forAll (choose (4, 8)) $ \count ->
+  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
+  forAll (genInstanceSmallerThanNode node `suchThat` isMirrored) $ \inst ->
+  case genClusterAlloc count node inst of
+    Types.Bad msg -> failTest msg
+    Types.Ok (nl, il, inst') ->
+      case IAlloc.processRelocate defGroupList nl il
+             (Instance.idx inst) 1
+             [(if Instance.diskTemplate inst' == Types.DTDrbd8
+                 then Instance.sNode
+                 else Instance.pNode) inst'] of
+        Types.Ok _ -> property True
+        Types.Bad msg -> failTest $ "Failed to relocate: " ++ msg
+
+-- | Helper property checker for the result of a nodeEvac or
+-- changeGroup operation.
+check_EvacMode grp inst result =
+  case result of
+    Types.Bad msg -> failTest $ "Couldn't evacuate/change group:" ++ msg
+    Types.Ok (_, _, es) ->
+      let moved = Cluster.esMoved es
+          failed = Cluster.esFailed es
+          opcodes = not . null $ Cluster.esOpCodes es
+      in failmsg ("'failed' not empty: " ++ show failed) (null failed) .&&.
+         failmsg "'opcodes' is null" opcodes .&&.
+         case moved of
+           [(idx', gdx, _)] -> failmsg "invalid instance moved" (idx == idx')
+                               .&&.
+                               failmsg "wrong target group"
+                                         (gdx == Group.idx grp)
+           v -> failmsg  ("invalid solution: " ++ show v) False
+  where failmsg = \msg -> printTestCase ("Failed to evacuate: " ++ msg)
+        idx = Instance.idx inst
+
+-- | Checks that on a 4-8 node cluster, once we allocate an instance,
+-- we can also node-evacuate it.
+prop_ClusterAllocEvacuate =
+  forAll (choose (4, 8)) $ \count ->
+  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
+  forAll (genInstanceSmallerThanNode node `suchThat` isMirrored) $ \inst ->
+  case genClusterAlloc count node inst of
+    Types.Bad msg -> failTest msg
+    Types.Ok (nl, il, inst') ->
+      conjoin $ map (\mode -> check_EvacMode defGroup inst' $
+                              Cluster.tryNodeEvac defGroupList nl il mode
+                                [Instance.idx inst']) .
+                              evacModeOptions .
+                              Instance.mirrorType $ inst'
+
+-- | Checks that on a 4-8 node cluster with two node groups, once we
+-- allocate an instance on the first node group, we can also change
+-- its group.
+prop_ClusterAllocChangeGroup =
+  forAll (choose (4, 8)) $ \count ->
+  forAll (genOnlineNode `suchThat` (isNodeBig 4)) $ \node ->
+  forAll (genInstanceSmallerThanNode node `suchThat` isMirrored) $ \inst ->
+  case genClusterAlloc count node inst of
+    Types.Bad msg -> failTest msg
+    Types.Ok (nl, il, inst') ->
+      -- we need to add a second node group and nodes to the cluster
+      let nl2 = Container.elems $ makeSmallCluster node count
+          grp2 = Group.setIdx defGroup (Group.idx defGroup + 1)
+          maxndx = maximum . map Node.idx $ nl2
+          nl3 = map (\n -> n { Node.group = Group.idx grp2
+                             , Node.idx = Node.idx n + maxndx }) nl2
+          nl4 = Container.fromList . map (\n -> (Node.idx n, n)) $ nl3
+          gl' = Container.add (Group.idx grp2) grp2 defGroupList
+          nl' = IntMap.union nl nl4
+      in check_EvacMode grp2 inst' $
+         Cluster.tryChangeGroup gl' nl' il [] [Instance.idx inst']
 
 -- | Check that allocating multiple instances on a cluster, then
 -- adding an empty node, results in a valid rebalance.
 prop_ClusterAllocBalance =
-    forAll (genNode (Just 5) (Just 128)) $ \node ->
-    forAll (choose (3, 5)) $ \count ->
-    not (Node.offline node) && not (Node.failN1 node) ==>
-    let nl = makeSmallCluster node count
-        (hnode, nl') = IntMap.deleteFindMax nl
-        il = Container.empty
-        allocnodes = Cluster.genAllocNodes defGroupList nl' 2 True
-        i_templ = createInstance Types.unitMem Types.unitDsk Types.unitCpu
-    in case allocnodes >>= \allocnodes' ->
-        Cluster.iterateAlloc nl' il (Just 5) i_templ allocnodes' [] [] of
-         Types.Bad _ -> False
-         Types.Ok (_, xnl, il', _, _) ->
-                   let ynl = Container.add (Node.idx hnode) hnode xnl
-                       cv = Cluster.compCV ynl
-                       tbl = Cluster.Table ynl il' cv []
-                   in canBalance tbl True True False
+  forAll (genNode (Just 5) (Just 128)) $ \node ->
+  forAll (choose (3, 5)) $ \count ->
+  not (Node.offline node) && not (Node.failN1 node) ==>
+  let nl = makeSmallCluster node count
+      (hnode, nl') = IntMap.deleteFindMax nl
+      il = Container.empty
+      allocnodes = Cluster.genAllocNodes defGroupList nl' 2 True
+      i_templ = createInstance Types.unitMem Types.unitDsk Types.unitCpu
+  in case allocnodes >>= \allocnodes' ->
+    Cluster.iterateAlloc nl' il (Just 5) i_templ allocnodes' [] [] of
+       Types.Bad msg -> failTest $ "Failed to allocate: " ++ msg
+       Types.Ok (_, _, _, [], _) -> failTest "Failed to allocate: no instances"
+       Types.Ok (_, xnl, il', _, _) ->
+         let ynl = Container.add (Node.idx hnode) hnode xnl
+             cv = Cluster.compCV ynl
+             tbl = Cluster.Table ynl il' cv []
+         in printTestCase "Failed to rebalance" $
+            canBalance tbl True True False
 
 -- | Checks consistency.
 prop_ClusterCheckConsistency node inst =
@@ -958,67 +1403,104 @@ prop_ClusterSplitCluster node inst =
      all (\(guuid, (nl'', _)) -> all ((== guuid) . Node.group)
                                  (Container.elems nl'')) gni
 
-testCluster =
-    [ run prop_Score_Zero
-    , run prop_CStats_sane
-    , run prop_ClusterAlloc_sane
-    , run prop_ClusterCanTieredAlloc
-    , run prop_ClusterAllocEvac
-    , run prop_ClusterAllocBalance
-    , run prop_ClusterCheckConsistency
-    , run prop_ClusterSplitCluster
-    ]
+-- | Helper function to check if we can allocate an instance on a
+-- given node list.
+canAllocOn :: Node.List -> Int -> Instance.Instance -> Bool
+canAllocOn nl reqnodes inst =
+  case Cluster.genAllocNodes defGroupList nl reqnodes True >>=
+       Cluster.tryAlloc nl (Container.empty) inst of
+       Types.Bad _ -> False
+       Types.Ok as ->
+         case Cluster.asSolution as of
+           Nothing -> False
+           Just _ -> True
+
+-- | Checks that allocation obeys minimum and maximum instance
+-- policies. The unittest generates a random node, duplicates it count
+-- times, and generates a random instance that can be allocated on
+-- this mini-cluster; it then checks that after applying a policy that
+-- the instance doesn't fits, the allocation fails.
+prop_ClusterAllocPolicy node =
+  -- rqn is the required nodes (1 or 2)
+  forAll (choose (1, 2)) $ \rqn ->
+  forAll (choose (5, 20)) $ \count ->
+  forAll (arbitrary `suchThat` (canAllocOn (makeSmallCluster node count) rqn))
+         $ \inst ->
+  forAll (arbitrary `suchThat` (isFailure .
+                                Instance.instMatchesPolicy inst)) $ \ipol ->
+  let node' = Node.setPolicy ipol node
+      nl = makeSmallCluster node' count
+  in not $ canAllocOn nl rqn inst
+
+testSuite "Cluster"
+            [ 'prop_Score_Zero
+            , 'prop_CStats_sane
+            , 'prop_ClusterAlloc_sane
+            , 'prop_ClusterCanTieredAlloc
+            , 'prop_ClusterAllocRelocate
+            , 'prop_ClusterAllocEvacuate
+            , 'prop_ClusterAllocChangeGroup
+            , 'prop_ClusterAllocBalance
+            , 'prop_ClusterCheckConsistency
+            , 'prop_ClusterSplitCluster
+            , 'prop_ClusterAllocPolicy
+            ]
 
 -- ** OpCodes tests
 
 -- | Check that opcode serialization is idempotent.
 prop_OpCodes_serialization op =
   case J.readJSON (J.showJSON op) of
-    J.Error _ -> False
-    J.Ok op' -> op == op'
+    J.Error e -> failTest $ "Cannot deserialise: " ++ e
+    J.Ok op' -> op ==? op'
   where _types = op::OpCodes.OpCode
 
-testOpCodes =
-  [ run prop_OpCodes_serialization
-  ]
+testSuite "OpCodes"
+            [ 'prop_OpCodes_serialization ]
 
 -- ** Jobs tests
 
 -- | Check that (queued) job\/opcode status serialization is idempotent.
 prop_OpStatus_serialization os =
   case J.readJSON (J.showJSON os) of
-    J.Error _ -> False
-    J.Ok os' -> os == os'
+    J.Error e -> failTest $ "Cannot deserialise: " ++ e
+    J.Ok os' -> os ==? os'
   where _types = os::Jobs.OpStatus
 
 prop_JobStatus_serialization js =
   case J.readJSON (J.showJSON js) of
-    J.Error _ -> False
-    J.Ok js' -> js == js'
+    J.Error e -> failTest $ "Cannot deserialise: " ++ e
+    J.Ok js' -> js ==? js'
   where _types = js::Jobs.JobStatus
 
-testJobs =
-  [ run prop_OpStatus_serialization
-  , run prop_JobStatus_serialization
-  ]
+testSuite "Jobs"
+            [ 'prop_OpStatus_serialization
+            , 'prop_JobStatus_serialization
+            ]
 
 -- ** Loader tests
 
 prop_Loader_lookupNode ktn inst node =
-  Loader.lookupNode nl inst node == Data.Map.lookup node nl
-  where nl = Data.Map.fromList ktn
+  Loader.lookupNode nl inst node ==? Data.Map.lookup node nl
+    where nl = Data.Map.fromList ktn
 
 prop_Loader_lookupInstance kti inst =
-  Loader.lookupInstance il inst == Data.Map.lookup inst il
-  where il = Data.Map.fromList kti
-
-prop_Loader_assignIndices nodes =
-  Data.Map.size nassoc == length nodes &&
-  Container.size kt == length nodes &&
-  (if not (null nodes)
-   then maximum (IntMap.keys kt) == length nodes - 1
-   else True)
-  where (nassoc, kt) = Loader.assignIndices (map (\n -> (Node.name n, n)) nodes)
+  Loader.lookupInstance il inst ==? Data.Map.lookup inst il
+    where il = Data.Map.fromList kti
+
+prop_Loader_assignIndices =
+  -- generate nodes with unique names
+  forAll (arbitrary `suchThat`
+          (\nodes ->
+             let names = map Node.name nodes
+             in length names == length (nub names))) $ \nodes ->
+  let (nassoc, kt) =
+        Loader.assignIndices (map (\n -> (Node.name n, n)) nodes)
+  in Data.Map.size nassoc == length nodes &&
+     Container.size kt == length nodes &&
+     if not (null nodes)
+       then maximum (IntMap.keys kt) == length nodes - 1
+       else True
 
 -- | Checks that the number of primary instances recorded on the nodes
 -- is zero.
@@ -1027,7 +1509,7 @@ prop_Loader_mergeData ns =
   in case Loader.mergeData [] [] [] []
          (Loader.emptyCluster {Loader.cdNodes = na}) of
     Types.Bad _ -> False
-    Types.Ok (Loader.ClusterData _ nl il _) ->
+    Types.Ok (Loader.ClusterData _ nl il _ _) ->
       let nodes = Container.elems nl
           instances = Container.elems il
       in (sum . map (length . Node.pList)) nodes == 0 &&
@@ -1036,62 +1518,236 @@ prop_Loader_mergeData ns =
 -- | Check that compareNameComponent on equal strings works.
 prop_Loader_compareNameComponent_equal :: String -> Bool
 prop_Loader_compareNameComponent_equal s =
-  Loader.compareNameComponent s s ==
-    Loader.LookupResult Loader.ExactMatch s
+  BasicTypes.compareNameComponent s s ==
+    BasicTypes.LookupResult BasicTypes.ExactMatch s
 
 -- | Check that compareNameComponent on prefix strings works.
 prop_Loader_compareNameComponent_prefix :: NonEmptyList Char -> String -> Bool
 prop_Loader_compareNameComponent_prefix (NonEmpty s1) s2 =
-  Loader.compareNameComponent (s1 ++ "." ++ s2) s1 ==
-    Loader.LookupResult Loader.PartialMatch s1
-
-testLoader =
-  [ run prop_Loader_lookupNode
-  , run prop_Loader_lookupInstance
-  , run prop_Loader_assignIndices
-  , run prop_Loader_mergeData
-  , run prop_Loader_compareNameComponent_equal
-  , run prop_Loader_compareNameComponent_prefix
-  ]
+  BasicTypes.compareNameComponent (s1 ++ "." ++ s2) s1 ==
+    BasicTypes.LookupResult BasicTypes.PartialMatch s1
+
+testSuite "Loader"
+            [ 'prop_Loader_lookupNode
+            , 'prop_Loader_lookupInstance
+            , 'prop_Loader_assignIndices
+            , 'prop_Loader_mergeData
+            , 'prop_Loader_compareNameComponent_equal
+            , 'prop_Loader_compareNameComponent_prefix
+            ]
 
 -- ** Types tests
 
 prop_Types_AllocPolicy_serialisation apol =
-    case J.readJSON (J.showJSON apol) of
-      J.Ok p -> printTestCase ("invalid deserialisation " ++ show p) $
-                p == apol
-      J.Error s -> printTestCase ("failed to deserialise: " ++ s) False
-    where _types = apol::Types.AllocPolicy
+  case J.readJSON (J.showJSON apol) of
+    J.Ok p -> p ==? apol
+    J.Error s -> failTest $ "Failed to deserialise: " ++ s
+      where _types = apol::Types.AllocPolicy
 
 prop_Types_DiskTemplate_serialisation dt =
-    case J.readJSON (J.showJSON dt) of
-      J.Ok p -> printTestCase ("invalid deserialisation " ++ show p) $
-                p == dt
-      J.Error s -> printTestCase ("failed to deserialise: " ++ s)
-                   False
-    where _types = dt::Types.DiskTemplate
+  case J.readJSON (J.showJSON dt) of
+    J.Ok p -> p ==? dt
+    J.Error s -> failTest $ "Failed to deserialise: " ++ s
+      where _types = dt::Types.DiskTemplate
+
+prop_Types_ISpec_serialisation ispec =
+  case J.readJSON (J.showJSON ispec) of
+    J.Ok p -> p ==? ispec
+    J.Error s -> failTest $ "Failed to deserialise: " ++ s
+      where _types = ispec::Types.ISpec
+
+prop_Types_IPolicy_serialisation ipol =
+  case J.readJSON (J.showJSON ipol) of
+    J.Ok p -> p ==? ipol
+    J.Error s -> failTest $ "Failed to deserialise: " ++ s
+      where _types = ipol::Types.IPolicy
+
+prop_Types_EvacMode_serialisation em =
+  case J.readJSON (J.showJSON em) of
+    J.Ok p -> p ==? em
+    J.Error s -> failTest $ "Failed to deserialise: " ++ s
+      where _types = em::Types.EvacMode
 
 prop_Types_opToResult op =
-    case op of
-      Types.OpFail _ -> Types.isBad r
-      Types.OpGood v -> case r of
-                          Types.Bad _ -> False
-                          Types.Ok v' -> v == v'
-    where r = Types.opToResult op
-          _types = op::Types.OpResult Int
+  case op of
+    Types.OpFail _ -> Types.isBad r
+    Types.OpGood v -> case r of
+                        Types.Bad _ -> False
+                        Types.Ok v' -> v == v'
+  where r = Types.opToResult op
+        _types = op::Types.OpResult Int
 
 prop_Types_eitherToResult ei =
-    case ei of
-      Left _ -> Types.isBad r
-      Right v -> case r of
-                   Types.Bad _ -> False
-                   Types.Ok v' -> v == v'
+  case ei of
+    Left _ -> Types.isBad r
+    Right v -> case r of
+                 Types.Bad _ -> False
+                 Types.Ok v' -> v == v'
     where r = Types.eitherToResult ei
           _types = ei::Either String Int
 
-testTypes =
-    [ run prop_Types_AllocPolicy_serialisation
-    , run prop_Types_DiskTemplate_serialisation
-    , run prop_Types_opToResult
-    , run prop_Types_eitherToResult
-    ]
+testSuite "Types"
+            [ 'prop_Types_AllocPolicy_serialisation
+            , 'prop_Types_DiskTemplate_serialisation
+            , 'prop_Types_ISpec_serialisation
+            , 'prop_Types_IPolicy_serialisation
+            , 'prop_Types_EvacMode_serialisation
+            , 'prop_Types_opToResult
+            , 'prop_Types_eitherToResult
+            ]
+
+-- ** CLI tests
+
+-- | Test correct parsing.
+prop_CLI_parseISpec descr dsk mem cpu =
+  let str = printf "%d,%d,%d" dsk mem cpu
+  in CLI.parseISpecString descr str ==? Types.Ok (Types.RSpec cpu mem dsk)
+
+-- | Test parsing failure due to wrong section count.
+prop_CLI_parseISpecFail descr =
+  forAll (choose (0,100) `suchThat` ((/=) 3)) $ \nelems ->
+  forAll (replicateM nelems arbitrary) $ \values ->
+  let str = intercalate "," $ map show (values::[Int])
+  in case CLI.parseISpecString descr str of
+       Types.Ok v -> failTest $ "Expected failure, got " ++ show v
+       _ -> property True
+
+-- | Test parseYesNo.
+prop_CLI_parseYesNo def testval val =
+  forAll (elements [val, "yes", "no"]) $ \actual_val ->
+  if testval
+    then CLI.parseYesNo def Nothing ==? Types.Ok def
+    else let result = CLI.parseYesNo def (Just actual_val)
+         in if actual_val `elem` ["yes", "no"]
+              then result ==? Types.Ok (actual_val == "yes")
+              else property $ Types.isBad result
+
+-- | Helper to check for correct parsing of string arg.
+checkStringArg val (opt, fn) =
+  let GetOpt.Option _ longs _ _ = opt
+  in case longs of
+       [] -> failTest "no long options?"
+       cmdarg:_ ->
+         case CLI.parseOptsInner ["--" ++ cmdarg ++ "=" ++ val] "prog" [opt] of
+           Left e -> failTest $ "Failed to parse option: " ++ show e
+           Right (options, _) -> fn options ==? Just val
+
+-- | Test a few string arguments.
+prop_CLI_StringArg argument =
+  let args = [ (CLI.oDataFile,      CLI.optDataFile)
+             , (CLI.oDynuFile,      CLI.optDynuFile)
+             , (CLI.oSaveCluster,   CLI.optSaveCluster)
+             , (CLI.oReplay,        CLI.optReplay)
+             , (CLI.oPrintCommands, CLI.optShowCmds)
+             , (CLI.oLuxiSocket,    CLI.optLuxi)
+             ]
+  in conjoin $ map (checkStringArg argument) args
+
+-- | Helper to test that a given option is accepted OK with quick exit.
+checkEarlyExit name options param =
+  case CLI.parseOptsInner [param] name options of
+    Left (code, _) -> if code == 0
+                          then property True
+                          else failTest $ "Program " ++ name ++
+                                 " returns invalid code " ++ show code ++
+                                 " for option " ++ param
+    _ -> failTest $ "Program " ++ name ++ " doesn't consider option " ++
+         param ++ " as early exit one"
+
+-- | Test that all binaries support some common options. There is
+-- nothing actually random about this test...
+prop_CLI_stdopts =
+  let params = ["-h", "--help", "-V", "--version"]
+      opts = map (\(name, (_, o)) -> (name, o)) Program.personalities
+      -- apply checkEarlyExit across the cartesian product of params and opts
+  in conjoin [checkEarlyExit n o p | p <- params, (n, o) <- opts]
+
+testSuite "CLI"
+          [ 'prop_CLI_parseISpec
+          , 'prop_CLI_parseISpecFail
+          , 'prop_CLI_parseYesNo
+          , 'prop_CLI_StringArg
+          , 'prop_CLI_stdopts
+          ]
+
+-- * JSON tests
+
+prop_JSON_toArray :: [Int] -> Property
+prop_JSON_toArray intarr =
+  let arr = map J.showJSON intarr in
+  case JSON.toArray (J.JSArray arr) of
+    Types.Ok arr' -> arr ==? arr'
+    Types.Bad err -> failTest $ "Failed to parse array: " ++ err
+
+prop_JSON_toArrayFail :: Int -> String -> Bool -> Property
+prop_JSON_toArrayFail i s b =
+  -- poor man's instance Arbitrary JSValue
+  forAll (elements [J.showJSON i, J.showJSON s, J.showJSON b]) $ \item ->
+  case JSON.toArray item of
+    Types.Bad _ -> property True
+    Types.Ok result -> failTest $ "Unexpected parse, got " ++ show result
+
+testSuite "JSON"
+          [ 'prop_JSON_toArray
+          , 'prop_JSON_toArrayFail
+          ]
+
+-- * Luxi tests
+
+instance Arbitrary Luxi.LuxiReq where
+  arbitrary = elements [minBound..maxBound]
+
+instance Arbitrary Luxi.QrViaLuxi where
+  arbitrary = elements [minBound..maxBound]
+
+instance Arbitrary Luxi.LuxiOp where
+  arbitrary = do
+    lreq <- arbitrary
+    case lreq of
+      Luxi.ReqQuery -> Luxi.Query <$> arbitrary <*> getFields <*> arbitrary
+      Luxi.ReqQueryNodes -> Luxi.QueryNodes <$> (listOf getFQDN) <*>
+                            getFields <*> arbitrary
+      Luxi.ReqQueryGroups -> Luxi.QueryGroups <$> arbitrary <*>
+                             arbitrary <*> arbitrary
+      Luxi.ReqQueryInstances -> Luxi.QueryInstances <$> (listOf getFQDN) <*>
+                                getFields <*> arbitrary
+      Luxi.ReqQueryJobs -> Luxi.QueryJobs <$> arbitrary <*> getFields
+      Luxi.ReqQueryExports -> Luxi.QueryExports <$>
+                              (listOf getFQDN) <*> arbitrary
+      Luxi.ReqQueryConfigValues -> Luxi.QueryConfigValues <$> getFields
+      Luxi.ReqQueryClusterInfo -> pure Luxi.QueryClusterInfo
+      Luxi.ReqQueryTags -> Luxi.QueryTags <$> getName <*> getFQDN
+      Luxi.ReqSubmitJob -> Luxi.SubmitJob <$> (resize maxOpCodes arbitrary)
+      Luxi.ReqSubmitManyJobs -> Luxi.SubmitManyJobs <$>
+                                (resize maxOpCodes arbitrary)
+      Luxi.ReqWaitForJobChange -> Luxi.WaitForJobChange <$> arbitrary <*>
+                                  getFields <*> pure J.JSNull <*>
+                                  pure J.JSNull <*> arbitrary
+      Luxi.ReqArchiveJob -> Luxi.ArchiveJob <$> arbitrary
+      Luxi.ReqAutoArchiveJobs -> Luxi.AutoArchiveJobs <$> arbitrary <*>
+                                 arbitrary
+      Luxi.ReqCancelJob -> Luxi.CancelJob <$> arbitrary
+      Luxi.ReqSetDrainFlag -> Luxi.SetDrainFlag <$> arbitrary
+      Luxi.ReqSetWatcherPause -> Luxi.SetWatcherPause <$> arbitrary
+
+-- | Simple check that encoding/decoding of LuxiOp works.
+prop_Luxi_CallEncoding :: Luxi.LuxiOp -> Property
+prop_Luxi_CallEncoding op =
+  (Luxi.validateCall (Luxi.buildCall op) >>= Luxi.decodeCall) ==? Types.Ok op
+
+testSuite "LUXI"
+          [ 'prop_Luxi_CallEncoding
+          ]
+
+-- * Ssconf tests
+
+instance Arbitrary Ssconf.SSKey where
+  arbitrary = elements [minBound..maxBound]
+
+prop_Ssconf_filename key =
+  printTestCase "Key doesn't start with correct prefix" $
+    Ssconf.sSFilePrefix `isPrefixOf` Ssconf.keyToFilename (Just "") key
+
+testSuite "Ssconf"
+  [ 'prop_Ssconf_filename
+  ]
diff --git a/htools/Ganeti/HTools/QCHelper.hs b/htools/Ganeti/HTools/QCHelper.hs
new file mode 100644 (file)
index 0000000..8cd165a
--- /dev/null
@@ -0,0 +1,47 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+{-| Unittest helpers for ganeti-htools
+
+-}
+
+{-
+
+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.QCHelper
+  ( testSuite
+  ) where
+
+import Test.QuickCheck
+import Language.Haskell.TH
+
+run :: Testable prop => prop -> Args -> IO Result
+run = flip quickCheckWithResult
+
+testSuite :: String -> [Name] -> Q [Dec]
+testSuite tsname tdef = do
+  let fullname = mkName $ "test" ++ tsname
+  tests <- mapM (\n -> [| (run $(varE n), $(litE . StringL . nameBase $ n)) |])
+           tdef
+  sigtype <- [t| (String, [(Args -> IO Result, String)]) |]
+  return [ SigD fullname sigtype
+         , ValD (VarP fullname) (NormalB (TupE [LitE (StringL tsname),
+                                                ListE tests])) []
+         ]
index ce310ed..710bfbb 100644 (file)
@@ -4,7 +4,7 @@
 
 {-
 
-Copyright (C) 2009, 2010, 2011 Google Inc.
+Copyright (C) 2009, 2010, 2011, 2012 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
@@ -26,29 +26,38 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 {-# LANGUAGE BangPatterns, CPP #-}
 
 module Ganeti.HTools.Rapi
-    (
-      loadData
-    , parseData
-    ) where
+  ( loadData
+  , parseData
+  ) where
 
+import Control.Exception
+import Data.List (isPrefixOf)
 import Data.Maybe (fromMaybe)
 #ifndef NO_CURL
 import Network.Curl
 import Network.Curl.Types ()
 #endif
 import Control.Monad
+import Prelude hiding (catch)
 import Text.JSON (JSObject, fromJSObject, decodeStrict)
 import Text.JSON.Types (JSValue(..))
 import Text.Printf (printf)
+import System.FilePath
 
-import Ganeti.HTools.Utils
 import Ganeti.HTools.Loader
 import Ganeti.HTools.Types
+import Ganeti.HTools.JSON
 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
 
+{-# ANN module "HLint: ignore Eta reduce" #-}
+
+-- | File method prefix.
+filePrefix :: String
+filePrefix = "file://"
+
 -- | Read an URL via curl and return the body if successful.
 getUrl :: (Monad m) => String -> IO (m String)
 
@@ -73,10 +82,17 @@ getUrl url = do
                  url (show code))
 #endif
 
+-- | Helper to convert I/O errors in 'Bad' values.
+ioErrToResult :: IO a -> IO (Result a)
+ioErrToResult ioaction =
+  catch (ioaction >>= return . Ok)
+        (\e -> return . Bad . show $ (e::IOException))
+
 -- | Append the default port if not passed in.
 formatHost :: String -> String
 formatHost master =
-    if ':' `elem` master then  master
+  if ':' `elem` master
+    then  master
     else "https://" ++ master ++ ":" ++ show C.defaultRapiPort
 
 -- | Parse a instance list in JSON format.
@@ -84,18 +100,18 @@ getInstances :: NameAssoc
              -> String
              -> Result [(String, Instance.Instance)]
 getInstances ktn body =
-    loadJSArray "Parsing instance data" body >>=
-    mapM (parseInstance ktn . fromJSObject)
+  loadJSArray "Parsing instance data" body >>=
+  mapM (parseInstance ktn . fromJSObject)
 
 -- | Parse a node list in JSON format.
 getNodes :: NameAssoc -> String -> Result [(String, Node.Node)]
 getNodes ktg body = loadJSArray "Parsing node data" body >>=
-                mapM (parseNode ktg . fromJSObject)
+                    mapM (parseNode ktg . fromJSObject)
 
 -- | Parse a group list in JSON format.
 getGroups :: String -> Result [(String, Group.Group)]
 getGroups body = loadJSArray "Parsing group data" body >>=
-                mapM (parseGroup . fromJSObject)
+                 mapM (parseGroup . fromJSObject)
 
 -- | Construct an instance from a JSON object.
 parseInstance :: NameAssoc
@@ -108,20 +124,22 @@ parseInstance ktn a = do
   disk <- extract "disk_usage" a
   beparams <- liftM fromJSObject (extract "beparams" a)
   omem <- extract "oper_ram" a
-  mem <- (case omem of
-            JSRational _ _ -> annotateResult owner_name (fromJVal omem)
-            _ -> extract "memory" beparams)
+  mem <- case omem of
+           JSRational _ _ -> annotateResult owner_name (fromJVal omem)
+           _ -> extract "memory" beparams `mplus` extract "maxmem" beparams
   vcpus <- extract "vcpus" beparams
   pnode <- extract "pnode" a >>= lookupNode ktn name
   snodes <- extract "snodes" a
-  snode <- (if null snodes then return Node.noSecondary
-            else readEitherString (head snodes) >>= lookupNode ktn name)
+  snode <- if null snodes
+             then return Node.noSecondary
+             else readEitherString (head snodes) >>= lookupNode ktn name
   running <- extract "status" a
   tags <- extract "tags" a
   auto_balance <- extract "auto_balance" beparams
   dt <- extract "disk_template" a
+  su <- extract "spindle_use" beparams
   let inst = Instance.create name mem disk vcpus running tags
-             auto_balance pnode snode dt
+             auto_balance pnode snode dt su
   return (name, inst)
 
 -- | Construct a node from a JSON object.
@@ -134,19 +152,21 @@ parseNode ktg a = do
   drained <- extract "drained"
   vm_cap  <- annotateResult desc $ maybeFromObj a "vm_capable"
   let vm_cap' = fromMaybe True vm_cap
+  ndparams <- extract "ndparams" >>= asJSObject
+  spindles <- tryFromObj desc (fromJSObject ndparams) "spindle_count"
   guuid   <- annotateResult desc $ maybeFromObj a "group.uuid"
   guuid' <-  lookupGroup ktg name (fromMaybe defaultGroupID guuid)
-  node <- (if offline || drained || not vm_cap'
-           then return $ Node.create name 0 0 0 0 0 0 True guuid'
-           else do
-             mtotal  <- extract "mtotal"
-             mnode   <- extract "mnode"
-             mfree   <- extract "mfree"
-             dtotal  <- extract "dtotal"
-             dfree   <- extract "dfree"
-             ctotal  <- extract "ctotal"
-             return $ Node.create name mtotal mnode mfree
-                    dtotal dfree ctotal False guuid')
+  node <- if offline || drained || not vm_cap'
+            then return $ Node.create name 0 0 0 0 0 0 True 0 guuid'
+            else do
+              mtotal  <- extract "mtotal"
+              mnode   <- extract "mnode"
+              mfree   <- extract "mfree"
+              dtotal  <- extract "dtotal"
+              dfree   <- extract "dfree"
+              ctotal  <- extract "ctotal"
+              return $ Node.create name mtotal mnode mfree
+                     dtotal dfree ctotal False spindles guuid'
   return (name, node)
 
 -- | Construct a group from a JSON object.
@@ -156,31 +176,61 @@ parseGroup a = do
   let extract s = tryFromObj ("Group '" ++ name ++ "'") a s
   uuid <- extract "uuid"
   apol <- extract "alloc_policy"
-  return (uuid, Group.create name uuid apol)
+  ipol <- extract "ipolicy"
+  return (uuid, Group.create name uuid apol ipol)
+
+-- | Parse cluster data from the info resource.
+parseCluster :: JSObject JSValue -> Result ([String], IPolicy)
+parseCluster obj = do
+  let obj' = fromJSObject obj
+      extract s = tryFromObj "Parsing cluster data" obj' s
+  tags <- extract "tags"
+  ipolicy <- extract "ipolicy"
+  return (tags, ipolicy)
 
 -- | Loads the raw cluster data from an URL.
-readData :: String -- ^ Cluster or URL to use as source
-         -> IO (Result String, Result String, Result String, Result String)
-readData master = do
+readDataHttp :: String -- ^ Cluster or URL to use as source
+             -> IO (Result String, Result String, Result String, Result String)
+readDataHttp master = do
   let url = formatHost master
   group_body <- getUrl $ printf "%s/2/groups?bulk=1" url
   node_body <- getUrl $ printf "%s/2/nodes?bulk=1" url
   inst_body <- getUrl $ printf "%s/2/instances?bulk=1" url
-  tags_body <- getUrl $ printf "%s/2/tags" url
-  return (group_body, node_body, inst_body, tags_body)
+  info_body <- getUrl $ printf "%s/2/info" url
+  return (group_body, node_body, inst_body, info_body)
+
+-- | Loads the raw cluster data from the filesystem.
+readDataFile:: String -- ^ Path to the directory containing the files
+             -> IO (Result String, Result String, Result String, Result String)
+readDataFile path = do
+  group_body <- ioErrToResult $ readFile $ path </> "groups.json"
+  node_body <- ioErrToResult $ readFile $ path </> "nodes.json"
+  inst_body <- ioErrToResult $ readFile $ path </> "instances.json"
+  info_body <- ioErrToResult $ readFile $ path </> "info.json"
+  return (group_body, node_body, inst_body, info_body)
+
+-- | Loads data via either 'readDataFile' or 'readDataHttp'.
+readData :: String -- ^ URL to use as source
+         -> IO (Result String, Result String, Result String, Result String)
+readData url = do
+  if filePrefix `isPrefixOf` url
+    then readDataFile (drop (length filePrefix) url)
+    else readDataHttp url
 
 -- | Builds the cluster data from the raw Rapi content.
 parseData :: (Result String, Result String, Result String, Result String)
           -> Result ClusterData
-parseData (group_body, node_body, inst_body, tags_body) = do
+parseData (group_body, node_body, inst_body, info_body) = do
   group_data <- group_body >>= getGroups
   let (group_names, group_idx) = assignIndices group_data
   node_data <- node_body >>= getNodes group_names
   let (node_names, node_idx) = assignIndices node_data
   inst_data <- inst_body >>= getInstances node_names
   let (_, inst_idx) = assignIndices inst_data
-  tags_data <- tags_body >>= (fromJResult "Parsing tags data" . decodeStrict)
-  return (ClusterData group_idx node_idx inst_idx tags_data)
+  (tags, ipolicy) <- info_body >>=
+                     (fromJResult "Parsing cluster info" . decodeStrict) >>=
+                     parseCluster
+  return (ClusterData group_idx node_idx inst_idx tags ipolicy)
 
 -- | Top level function for data loading.
 loadData :: String -- ^ Cluster or URL to use as source
index 8126f2d..890eae1 100644 (file)
@@ -6,7 +6,7 @@ This module holds the code for parsing a cluster description.
 
 {-
 
-Copyright (C) 2009, 2010, 2011 Google Inc.
+Copyright (C) 2009, 2010, 2011, 2012 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
@@ -26,11 +26,11 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 -}
 
 module Ganeti.HTools.Simu
-    (
-      loadData
-    , parseData
-    ) where
+  ( loadData
+  , parseData
+  ) where
 
+import Control.Monad (mplus)
 import Text.Printf (printf)
 
 import Ganeti.HTools.Utils
@@ -40,36 +40,50 @@ import qualified Ganeti.HTools.Container as Container
 import qualified Ganeti.HTools.Group as Group
 import qualified Ganeti.HTools.Node as Node
 
+-- | Parse a shortened policy string (for command line usage).
+apolAbbrev :: String -> Result AllocPolicy
+apolAbbrev c | c == "p"  = return AllocPreferred
+             | c == "a"  = return AllocLastResort
+             | c == "u"  = return AllocUnallocable
+             | otherwise = fail $ "Cannot parse AllocPolicy abbreviation '"
+                           ++ c ++ "'"
+
 -- | Parse the string description into nodes.
-parseDesc :: String -> Result (AllocPolicy, Int, Int, Int, Int)
-parseDesc desc =
-    case sepSplit ',' desc of
-      [a, n, d, m, c] -> do
-        apol <- apolFromString a
-        ncount <- tryRead "node count" n
-        disk <- annotateResult "disk size" (parseUnit d)
-        mem <- annotateResult "memory size" (parseUnit m)
-        cpu <- tryRead "cpu count" c
-        return (apol, ncount, disk, mem, cpu)
-      es -> fail $ printf
-            "Invalid cluster specification, expected 5 comma-separated\
-            \ sections (allocation policy, node count, disk size,\
-            \ memory size, number of CPUs) but got %d: '%s'" (length es) desc
+parseDesc :: String -> [String]
+          -> Result (AllocPolicy, Int, Int, Int, Int, Int)
+parseDesc _ [a, n, d, m, c, s] = do
+  apol <- allocPolicyFromRaw a `mplus` apolAbbrev a
+  ncount <- tryRead "node count" n
+  disk <- annotateResult "disk size" (parseUnit d)
+  mem <- annotateResult "memory size" (parseUnit m)
+  cpu <- tryRead "cpu count" c
+  spindles <- tryRead "spindles" s
+  return (apol, ncount, disk, mem, cpu, spindles)
+
+parseDesc desc [a, n, d, m, c] = parseDesc desc [a, n, d, m, c, "1"]
+
+parseDesc desc es =
+  fail $ printf
+         "Invalid cluster specification, expected 6 comma-separated\
+         \ sections (allocation policy, node count, disk size,\
+         \ memory size, number of CPUs, spindles) but got %d: '%s'"
+         (length es) desc
 
 -- | Creates a node group with the given specifications.
 createGroup :: Int    -- ^ The group index
             -> String -- ^ The group specification
             -> Result (Group.Group, [Node.Node])
 createGroup grpIndex spec = do
-  (apol, ncount, disk, mem, cpu) <- parseDesc spec
+  (apol, ncount, disk, mem, cpu, spindles) <- parseDesc spec $
+                                              sepSplit ',' spec
   let nodes = map (\idx ->
-                       Node.create (printf "node-%02d-%03d" grpIndex idx)
-                               (fromIntegral mem) 0 mem
-                               (fromIntegral disk) disk
-                               (fromIntegral cpu) False grpIndex
+                     Node.create (printf "node-%02d-%03d" grpIndex idx)
+                           (fromIntegral mem) 0 mem
+                           (fromIntegral disk) disk
+                           (fromIntegral cpu) False spindles grpIndex
                   ) [1..ncount]
       grp = Group.create (printf "group-%02d" grpIndex)
-            (printf "fake-uuid-%02d" grpIndex) apol
+            (printf "fake-uuid-%02d" grpIndex) apol defIPolicy
   return (Group.setIdx grp grpIndex, nodes)
 
 -- | Builds the cluster data from node\/instance files.
@@ -83,7 +97,7 @@ parseData ndata = do
             $ zip [1..] nodes'
       ktg = map (\g -> (Group.idx g, g)) groups
   return (ClusterData (Container.fromList ktg)
-                      (Container.fromList ktn) Container.empty [])
+                      (Container.fromList ktn) Container.empty [] defIPolicy)
 
 -- | Builds the cluster data from node\/instance files.
 loadData :: [String] -- ^ Cluster description in text format
index 172d67f..3b4bece 100644 (file)
@@ -7,7 +7,7 @@ files, as produced by @gnt-node@ and @gnt-instance@ @list@ command.
 
 {-
 
-Copyright (C) 2009, 2010, 2011 Google Inc.
+Copyright (C) 2009, 2010, 2011, 2012 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
@@ -27,16 +27,20 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 -}
 
 module Ganeti.HTools.Text
-    (
-      loadData
-    , parseData
-    , loadInst
-    , loadNode
-    , serializeInstances
-    , serializeNode
-    , serializeNodes
-    , serializeCluster
-    ) where
+  ( loadData
+  , parseData
+  , loadInst
+  , loadNode
+  , loadISpec
+  , loadIPolicy
+  , serializeInstances
+  , serializeNode
+  , serializeNodes
+  , serializeGroup
+  , serializeISpec
+  , serializeIPolicy
+  , serializeCluster
+  ) where
 
 import Control.Monad
 import Data.List
@@ -51,13 +55,19 @@ import qualified Ganeti.HTools.Group as Group
 import qualified Ganeti.HTools.Node as Node
 import qualified Ganeti.HTools.Instance as Instance
 
+-- * Helper functions
+
+-- | Simple wrapper over sepSplit
+commaSplit :: String -> [String]
+commaSplit = sepSplit ','
+
 -- * Serialisation functions
 
 -- | Serialize a single group.
 serializeGroup :: Group.Group -> String
 serializeGroup grp =
-    printf "%s|%s|%s" (Group.name grp) (Group.uuid grp)
-               (apolToString (Group.allocPolicy grp))
+  printf "%s|%s|%s" (Group.name grp) (Group.uuid grp)
+           (allocPolicyToRaw (Group.allocPolicy grp))
 
 -- | Generate group file data from a group list.
 serializeGroups :: Group.List -> String
@@ -68,11 +78,12 @@ serializeNode :: Group.List -- ^ The list of groups (needed for group uuid)
               -> Node.Node  -- ^ The node to be serialised
               -> String
 serializeNode gl node =
-    printf "%s|%.0f|%d|%d|%.0f|%d|%.0f|%c|%s" (Node.name node)
-               (Node.tMem node) (Node.nMem node) (Node.fMem node)
-               (Node.tDsk node) (Node.fDsk node) (Node.tCpu node)
-               (if Node.offline node then 'Y' else 'N')
-               (Group.uuid grp)
+  printf "%s|%.0f|%d|%d|%.0f|%d|%.0f|%c|%s|%d" (Node.name node)
+           (Node.tMem node) (Node.nMem node) (Node.fMem node)
+           (Node.tDsk node) (Node.fDsk node) (Node.tCpu node)
+           (if Node.offline node then 'Y' else 'N')
+           (Group.uuid grp)
+           (Node.spindleCount node)
     where grp = Container.find (Node.group node) gl
 
 -- | Generate node file data from node objects.
@@ -85,34 +96,70 @@ serializeInstance :: Node.List         -- ^ The node list (needed for
                   -> Instance.Instance -- ^ The instance to be serialised
                   -> String
 serializeInstance nl inst =
-    let
-        iname = Instance.name inst
-        pnode = Container.nameOf nl (Instance.pNode inst)
-        sidx = Instance.sNode inst
-        snode = (if sidx == Node.noSecondary
-                    then ""
-                    else Container.nameOf nl sidx)
-    in
-      printf "%s|%d|%d|%d|%s|%s|%s|%s|%s|%s"
-             iname (Instance.mem inst) (Instance.dsk inst)
-             (Instance.vcpus inst) (Instance.runSt inst)
-             (if Instance.autoBalance inst then "Y" else "N")
-             pnode snode (dtToString (Instance.diskTemplate inst))
-             (intercalate "," (Instance.tags inst))
+  let iname = Instance.name inst
+      pnode = Container.nameOf nl (Instance.pNode inst)
+      sidx = Instance.sNode inst
+      snode = (if sidx == Node.noSecondary
+                 then ""
+                 else Container.nameOf nl sidx)
+  in printf "%s|%d|%d|%d|%s|%s|%s|%s|%s|%s|%d"
+       iname (Instance.mem inst) (Instance.dsk inst)
+       (Instance.vcpus inst) (instanceStatusToRaw (Instance.runSt inst))
+       (if Instance.autoBalance inst then "Y" else "N")
+       pnode snode (diskTemplateToRaw (Instance.diskTemplate inst))
+       (intercalate "," (Instance.tags inst)) (Instance.spindleUse inst)
 
 -- | Generate instance file data from instance objects.
 serializeInstances :: Node.List -> Instance.List -> String
 serializeInstances nl =
-    unlines . map (serializeInstance nl) . Container.elems
+  unlines . map (serializeInstance nl) . Container.elems
+
+-- | Generate a spec data from a given ISpec object.
+serializeISpec :: ISpec -> String
+serializeISpec ispec =
+  -- this needs to be kept in sync with the object definition
+  let ISpec mem_s cpu_c disk_s disk_c nic_c su = ispec
+      strings = [show mem_s, show cpu_c, show disk_s, show disk_c, show nic_c,
+                 show su]
+  in intercalate "," strings
+
+-- | Generate disk template data.
+serializeDiskTemplates :: [DiskTemplate] -> String
+serializeDiskTemplates = intercalate "," . map diskTemplateToRaw
+
+-- | Generate policy data from a given policy object.
+serializeIPolicy :: String -> IPolicy -> String
+serializeIPolicy owner ipol =
+  let IPolicy stdspec minspec maxspec dts vcpu_ratio spindle_ratio = ipol
+      strings = [ owner
+                , serializeISpec stdspec
+                , serializeISpec minspec
+                , serializeISpec maxspec
+                , serializeDiskTemplates dts
+                , show vcpu_ratio
+                , show spindle_ratio
+                ]
+  in intercalate "|" strings
+
+-- | Generates the entire ipolicy section from the cluster and group
+-- objects.
+serializeAllIPolicies :: IPolicy -> Group.List -> String
+serializeAllIPolicies cpol gl =
+  let groups = Container.elems gl
+      allpolicies = [("", cpol)] ++
+                    map (\g -> (Group.name g, Group.iPolicy g)) groups
+      strings = map (uncurry serializeIPolicy) allpolicies
+  in unlines strings
 
 -- | Generate complete cluster data from node and instance lists.
 serializeCluster :: ClusterData -> String
-serializeCluster (ClusterData gl nl il ctags) =
+serializeCluster (ClusterData gl nl il ctags cpol) =
   let gdata = serializeGroups gl
       ndata = serializeNodes gl nl
       idata = serializeInstances nl il
+      pdata = serializeAllIPolicies cpol gl
   -- note: not using 'unlines' as that adds too many newlines
-  in intercalate "\n" [gdata, ndata, idata, unlines ctags]
+  in intercalate "\n" [gdata, ndata, idata, unlines ctags, pdata]
 
 -- * Parsing functions
 
@@ -121,8 +168,8 @@ loadGroup :: (Monad m) => [String]
           -> m (String, Group.Group) -- ^ The result, a tuple of group
                                      -- UUID and group object
 loadGroup [name, gid, apol] = do
-  xapol <- apolFromString apol
-  return (gid, Group.create name gid xapol)
+  xapol <- allocPolicyFromRaw apol
+  return (gid, Group.create name gid xapol defIPolicy)
 
 loadGroup s = fail $ "Invalid/incomplete group data: '" ++ show s ++ "'"
 
@@ -132,11 +179,11 @@ loadNode :: (Monad m) =>
          -> [String]              -- ^ Input data as a list of fields
          -> m (String, Node.Node) -- ^ The result, a tuple o node name
                                   -- and node object
-loadNode ktg [name, tm, nm, fm, td, fd, tc, fo, gu] = do
+loadNode ktg [name, tm, nm, fm, td, fd, tc, fo, gu, spindles] = do
   gdx <- lookupGroup ktg name gu
   new_node <-
       if any (== "?") [tm,nm,fm,td,fd,tc] || fo == "Y" then
-          return $ Node.create name 0 0 0 0 0 0 True gdx
+          return $ Node.create name 0 0 0 0 0 0 True 0 gdx
       else do
         vtm <- tryRead name tm
         vnm <- tryRead name nm
@@ -144,8 +191,13 @@ loadNode ktg [name, tm, nm, fm, td, fd, tc, fo, gu] = do
         vtd <- tryRead name td
         vfd <- tryRead name fd
         vtc <- tryRead name tc
-        return $ Node.create name vtm vnm vfm vtd vfd vtc False gdx
+        vspindles <- tryRead name spindles
+        return $ Node.create name vtm vnm vfm vtd vfd vtc False vspindles gdx
   return (name, new_node)
+
+loadNode ktg [name, tm, nm, fm, td, fd, tc, fo, gu] =
+  loadNode ktg [name, tm, nm, fm, td, fd, tc, fo, gu, "1"]
+
 loadNode _ s = fail $ "Invalid/incomplete node data: '" ++ show s ++ "'"
 
 -- | Load an instance from a field list.
@@ -155,27 +207,79 @@ loadInst :: NameAssoc -- ^ Association list with the current nodes
                                                -- instance name and
                                                -- the instance object
 loadInst ktn [ name, mem, dsk, vcpus, status, auto_bal, pnode, snode
-             , dt, tags ] = do
+             , dt, tags, su ] = do
   pidx <- lookupNode ktn name pnode
-  sidx <- (if null snode then return Node.noSecondary
-           else lookupNode ktn name snode)
+  sidx <- if null snode
+            then return Node.noSecondary
+            else lookupNode ktn name snode
   vmem <- tryRead name mem
   vdsk <- tryRead name dsk
   vvcpus <- tryRead name vcpus
+  vstatus <- instanceStatusFromRaw status
   auto_balance <- case auto_bal of
                     "Y" -> return True
                     "N" -> return False
                     _ -> fail $ "Invalid auto_balance value '" ++ auto_bal ++
                          "' for instance " ++ name
-  disk_template <- annotateResult ("Instance " ++ name) (dtFromString dt)
+  disk_template <- annotateResult ("Instance " ++ name)
+                   (diskTemplateFromRaw dt)
+  spindle_use <- tryRead name su
   when (sidx == pidx) $ fail $ "Instance " ++ name ++
            " has same primary and secondary node - " ++ pnode
-  let vtags = sepSplit ',' tags
-      newinst = Instance.create name vmem vdsk vvcpus status vtags
-                auto_balance pidx sidx disk_template
+  let vtags = commaSplit tags
+      newinst = Instance.create name vmem vdsk vvcpus vstatus vtags
+                auto_balance pidx sidx disk_template spindle_use
   return (name, newinst)
+
+loadInst ktn [ name, mem, dsk, vcpus, status, auto_bal, pnode, snode
+             , dt, tags ] = loadInst ktn [ name, mem, dsk, vcpus, status,
+                                           auto_bal, pnode, snode, dt, tags,
+                                           "1" ]
 loadInst _ s = fail $ "Invalid/incomplete instance data: '" ++ show s ++ "'"
 
+-- | Loads a spec from a field list.
+loadISpec :: String -> [String] -> Result ISpec
+loadISpec owner [mem_s, cpu_c, dsk_s, dsk_c, nic_c, su] = do
+  xmem_s <- tryRead (owner ++ "/memsize") mem_s
+  xcpu_c <- tryRead (owner ++ "/cpucount") cpu_c
+  xdsk_s <- tryRead (owner ++ "/disksize") dsk_s
+  xdsk_c <- tryRead (owner ++ "/diskcount") dsk_c
+  xnic_c <- tryRead (owner ++ "/niccount") nic_c
+  xsu    <- tryRead (owner ++ "/spindleuse") su
+  return $ ISpec xmem_s xcpu_c xdsk_s xdsk_c xnic_c xsu
+loadISpec owner s = fail $ "Invalid ispec data for " ++ owner ++ ": " ++ show s
+
+-- | Loads an ipolicy from a field list.
+loadIPolicy :: [String] -> Result (String, IPolicy)
+loadIPolicy [owner, stdspec, minspec, maxspec, dtemplates,
+             vcpu_ratio, spindle_ratio] = do
+  xstdspec <- loadISpec (owner ++ "/stdspec") (commaSplit stdspec)
+  xminspec <- loadISpec (owner ++ "/minspec") (commaSplit minspec)
+  xmaxspec <- loadISpec (owner ++ "/maxspec") (commaSplit maxspec)
+  xdts <- mapM diskTemplateFromRaw $ commaSplit dtemplates
+  xvcpu_ratio <- tryRead (owner ++ "/vcpu_ratio") vcpu_ratio
+  xspindle_ratio <- tryRead (owner ++ "/spindle_ratio") spindle_ratio
+  return $ (owner, IPolicy xstdspec xminspec xmaxspec xdts
+            xvcpu_ratio xspindle_ratio)
+loadIPolicy s = fail $ "Invalid ipolicy data: '" ++ show s ++ "'"
+
+loadOnePolicy :: (IPolicy, Group.List) -> String
+              -> Result (IPolicy, Group.List)
+loadOnePolicy (cpol, gl) line = do
+  (owner, ipol) <- loadIPolicy (sepSplit '|' line)
+  case owner of
+    "" -> return (ipol, gl) -- this is a cluster policy (no owner)
+    _ -> do
+      grp <- Container.findByName gl owner
+      let grp' = grp { Group.iPolicy = ipol }
+          gl' = Container.add (Group.idx grp') grp' gl
+      return (cpol, gl')
+
+-- | Loads all policies from the policy section
+loadAllIPolicies :: Group.List -> [String] -> Result (IPolicy, Group.List)
+loadAllIPolicies gl =
+  foldM loadOnePolicy (defIPolicy, gl)
+
 -- | Convert newline and delimiter-separated text.
 --
 -- This function converts a text in tabular format as generated by
@@ -209,11 +313,12 @@ parseData :: String -- ^ Text data
           -> Result ClusterData
 parseData fdata = do
   let flines = lines fdata
-  (glines, nlines, ilines, ctags) <-
+  (glines, nlines, ilines, ctags, pollines) <-
       case sepSplit "" flines of
-        [a, b, c, d] -> Ok (a, b, c, d)
+        [a, b, c, d, e] -> Ok (a, b, c, d, e)
+        [a, b, c, d] -> Ok (a, b, c, d, [])
         xs -> Bad $ printf "Invalid format of the input file: %d sections\
-                           \ instead of 4" (length xs)
+                           \ instead of 4 or 5" (length xs)
   {- group file: name uuid -}
   (ktg, gl) <- loadTabular glines loadGroup
   {- node file: name t_mem n_mem f_mem t_disk f_disk -}
@@ -221,7 +326,9 @@ parseData fdata = do
   {- instance file: name mem disk status pnode snode -}
   (_, il) <- loadTabular ilines (loadInst ktn)
   {- the tags are simply line-based, no processing needed -}
-  return (ClusterData gl nl il ctags)
+  {- process policies -}
+  (cpol, gl') <- loadAllIPolicies gl pollines
+  return (ClusterData gl' nl il ctags cpol)
 
 -- | Top level function for data loading.
 loadData :: String -- ^ Path to the text file
index 6452f5b..b6e92b7 100644 (file)
@@ -1,10 +1,12 @@
+{-# LANGUAGE TemplateHaskell #-}
+
 {-| Some common types.
 
 -}
 
 {-
 
-Copyright (C) 2009, 2010, 2011 Google Inc.
+Copyright (C) 2009, 2010, 2011, 2012 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
@@ -24,53 +26,68 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 -}
 
 module Ganeti.HTools.Types
-    ( Idx
-    , Ndx
-    , Gdx
-    , NameAssoc
-    , Score
-    , Weight
-    , GroupID
-    , AllocPolicy(..)
-    , apolFromString
-    , apolToString
-    , RSpec(..)
-    , DynUtil(..)
-    , zeroUtil
-    , baseUtil
-    , addUtil
-    , subUtil
-    , defVcpuRatio
-    , defReservedDiskRatio
-    , unitMem
-    , unitCpu
-    , unitDsk
-    , unknownField
-    , Placement
-    , IMove(..)
-    , DiskTemplate(..)
-    , dtToString
-    , dtFromString
-    , MoveJob
-    , JobSet
-    , Result(..)
-    , isOk
-    , isBad
-    , eitherToResult
-    , Element(..)
-    , FailMode(..)
-    , FailStats
-    , OpResult(..)
-    , opToResult
-    , connTimeout
-    , queryTimeout
-    , EvacMode(..)
-    ) where
+  ( Idx
+  , Ndx
+  , Gdx
+  , NameAssoc
+  , Score
+  , Weight
+  , GroupID
+  , defaultGroupID
+  , AllocPolicy(..)
+  , allocPolicyFromRaw
+  , allocPolicyToRaw
+  , InstanceStatus(..)
+  , instanceStatusFromRaw
+  , instanceStatusToRaw
+  , RSpec(..)
+  , AllocInfo(..)
+  , AllocStats
+  , DynUtil(..)
+  , zeroUtil
+  , baseUtil
+  , addUtil
+  , subUtil
+  , defReservedDiskRatio
+  , unitMem
+  , unitCpu
+  , unitDsk
+  , unknownField
+  , Placement
+  , IMove(..)
+  , DiskTemplate(..)
+  , diskTemplateToRaw
+  , diskTemplateFromRaw
+  , MirrorType(..)
+  , templateMirrorType
+  , MoveJob
+  , JobSet
+  , Result(..)
+  , isOk
+  , isBad
+  , eitherToResult
+  , annotateResult
+  , Element(..)
+  , FailMode(..)
+  , FailStats
+  , OpResult(..)
+  , opToResult
+  , connTimeout
+  , queryTimeout
+  , EvacMode(..)
+  , ISpec(..)
+  , IPolicy(..)
+  , defIPolicy
+  , rspecFromISpec
+  ) where
 
 import qualified Data.Map as M
-import qualified Text.JSON as JSON
+import Text.JSON (makeObj, readJSON, showJSON)
 
 import qualified Ganeti.Constants as C
+import qualified Ganeti.THH as THH
+import Ganeti.BasicTypes
+import Ganeti.HTools.JSON
 
 -- | The instance index type.
 type Idx = Int
@@ -93,58 +110,167 @@ type Weight = Double
 -- | The Group UUID type.
 type GroupID = String
 
+-- | Default group UUID (just a string, not a real UUID).
+defaultGroupID :: GroupID
+defaultGroupID = "00000000-0000-0000-0000-000000000000"
+
+-- | Instance disk template type.
+$(THH.declareSADT "DiskTemplate"
+       [ ("DTDiskless",   'C.dtDiskless)
+       , ("DTFile",       'C.dtFile)
+       , ("DTSharedFile", 'C.dtSharedFile)
+       , ("DTPlain",      'C.dtPlain)
+       , ("DTBlock",      'C.dtBlock)
+       , ("DTDrbd8",      'C.dtDrbd8)
+       , ("DTRbd",        'C.dtRbd)
+       ])
+$(THH.makeJSONInstance ''DiskTemplate)
+
+-- | Mirroring type.
+data MirrorType = MirrorNone     -- ^ No mirroring/movability
+                | MirrorInternal -- ^ DRBD-type mirroring
+                | MirrorExternal -- ^ Shared-storage type mirroring
+                  deriving (Eq, Show, Read)
+
+-- | Correspondence between disk template and mirror type.
+templateMirrorType :: DiskTemplate -> MirrorType
+templateMirrorType DTDiskless   = MirrorExternal
+templateMirrorType DTFile       = MirrorNone
+templateMirrorType DTSharedFile = MirrorExternal
+templateMirrorType DTPlain      = MirrorNone
+templateMirrorType DTBlock      = MirrorExternal
+templateMirrorType DTDrbd8      = MirrorInternal
+templateMirrorType DTRbd        = MirrorExternal
+
 -- | The Group allocation policy type.
 --
 -- Note that the order of constructors is important as the automatic
 -- Ord instance will order them in the order they are defined, so when
 -- changing this data type be careful about the interaction with the
 -- desired sorting order.
-data AllocPolicy
-    = AllocPreferred   -- ^ This is the normal status, the group
-                       -- should be used normally during allocations
-    | AllocLastResort  -- ^ This group should be used only as
-                       -- last-resort, after the preferred groups
-    | AllocUnallocable -- ^ This group must not be used for new
-                       -- allocations
-      deriving (Show, Read, Eq, Ord, Enum, Bounded)
-
--- | Convert a string to an alloc policy.
-apolFromString :: (Monad m) => String -> m AllocPolicy
-apolFromString s =
-    case () of
-      _ | s == C.allocPolicyPreferred -> return AllocPreferred
-        | s == C.allocPolicyLastResort -> return AllocLastResort
-        | s == C.allocPolicyUnallocable -> return AllocUnallocable
-        | otherwise -> fail $ "Invalid alloc policy mode: " ++ s
-
--- | Convert an alloc policy to the Ganeti string equivalent.
-apolToString :: AllocPolicy -> String
-apolToString AllocPreferred   = C.allocPolicyPreferred
-apolToString AllocLastResort  = C.allocPolicyLastResort
-apolToString AllocUnallocable = C.allocPolicyUnallocable
-
-instance JSON.JSON AllocPolicy where
-    showJSON = JSON.showJSON . apolToString
-    readJSON s = case JSON.readJSON s of
-                   JSON.Ok s' -> apolFromString s'
-                   JSON.Error e -> JSON.Error $
-                                   "Can't parse alloc_policy: " ++ e
+$(THH.declareSADT "AllocPolicy"
+       [ ("AllocPreferred",   'C.allocPolicyPreferred)
+       , ("AllocLastResort",  'C.allocPolicyLastResort)
+       , ("AllocUnallocable", 'C.allocPolicyUnallocable)
+       ])
+$(THH.makeJSONInstance ''AllocPolicy)
+
+-- | The Instance real state type.
+$(THH.declareSADT "InstanceStatus"
+       [ ("AdminDown", 'C.inststAdmindown)
+       , ("AdminOffline", 'C.inststAdminoffline)
+       , ("ErrorDown", 'C.inststErrordown)
+       , ("ErrorUp", 'C.inststErrorup)
+       , ("NodeDown", 'C.inststNodedown)
+       , ("NodeOffline", 'C.inststNodeoffline)
+       , ("Running", 'C.inststRunning)
+       , ("WrongNode", 'C.inststWrongnode)
+       ])
+$(THH.makeJSONInstance ''InstanceStatus)
 
 -- | The resource spec type.
 data RSpec = RSpec
-    { rspecCpu  :: Int  -- ^ Requested VCPUs
-    , rspecMem  :: Int  -- ^ Requested memory
-    , rspecDsk  :: Int  -- ^ Requested disk
-    } deriving (Show, Read, Eq)
+  { rspecCpu  :: Int  -- ^ Requested VCPUs
+  , rspecMem  :: Int  -- ^ Requested memory
+  , rspecDsk  :: Int  -- ^ Requested disk
+  } deriving (Show, Read, Eq)
+
+-- | Allocation stats type. This is used instead of 'RSpec' (which was
+-- used at first), because we need to track more stats. The actual
+-- data can refer either to allocated, or available, etc. values
+-- depending on the context. See also
+-- 'Cluster.computeAllocationDelta'.
+data AllocInfo = AllocInfo
+  { allocInfoVCpus :: Int    -- ^ VCPUs
+  , allocInfoNCpus :: Double -- ^ Normalised CPUs
+  , allocInfoMem   :: Int    -- ^ Memory
+  , allocInfoDisk  :: Int    -- ^ Disk
+  } deriving (Show, Read, Eq)
+
+-- | Currently used, possibly to allocate, unallocable.
+type AllocStats = (AllocInfo, AllocInfo, AllocInfo)
+
+-- | Instance specification type.
+$(THH.buildObject "ISpec" "iSpec"
+  [ THH.renameField "MemorySize"   $ THH.simpleField C.ispecMemSize      [t| Int |]
+  , THH.renameField "CpuCount"     $ THH.simpleField C.ispecCpuCount     [t| Int |]
+  , THH.renameField "DiskSize"     $ THH.simpleField C.ispecDiskSize     [t| Int |]
+  , THH.renameField "DiskCount"    $ THH.simpleField C.ispecDiskCount    [t| Int |]
+  , THH.renameField "NicCount"     $ THH.simpleField C.ispecNicCount     [t| Int |]
+  , THH.renameField "SpindleUse"   $ THH.simpleField C.ispecSpindleUse   [t| Int |]
+  ])
+
+-- | The default minimum ispec.
+defMinISpec :: ISpec
+defMinISpec = ISpec { iSpecMemorySize = C.ipolicyDefaultsMinMemorySize
+                    , iSpecCpuCount   = C.ipolicyDefaultsMinCpuCount
+                    , iSpecDiskSize   = C.ipolicyDefaultsMinDiskSize
+                    , iSpecDiskCount  = C.ipolicyDefaultsMinDiskCount
+                    , iSpecNicCount   = C.ipolicyDefaultsMinNicCount
+                    , iSpecSpindleUse = C.ipolicyDefaultsMinSpindleUse
+                    }
+
+-- | The default standard ispec.
+defStdISpec :: ISpec
+defStdISpec = ISpec { iSpecMemorySize = C.ipolicyDefaultsStdMemorySize
+                    , iSpecCpuCount   = C.ipolicyDefaultsStdCpuCount
+                    , iSpecDiskSize   = C.ipolicyDefaultsStdDiskSize
+                    , iSpecDiskCount  = C.ipolicyDefaultsStdDiskCount
+                    , iSpecNicCount   = C.ipolicyDefaultsStdNicCount
+                    , iSpecSpindleUse = C.ipolicyDefaultsStdSpindleUse
+                    }
+
+-- | The default max ispec.
+defMaxISpec :: ISpec
+defMaxISpec = ISpec { iSpecMemorySize = C.ipolicyDefaultsMaxMemorySize
+                    , iSpecCpuCount   = C.ipolicyDefaultsMaxCpuCount
+                    , iSpecDiskSize   = C.ipolicyDefaultsMaxDiskSize
+                    , iSpecDiskCount  = C.ipolicyDefaultsMaxDiskCount
+                    , iSpecNicCount   = C.ipolicyDefaultsMaxNicCount
+                    , iSpecSpindleUse = C.ipolicyDefaultsMaxSpindleUse
+                    }
+
+-- | Instance policy type.
+$(THH.buildObject "IPolicy" "iPolicy"
+  [ THH.renameField "StdSpec" $ THH.simpleField C.ispecsStd [t| ISpec |]
+  , THH.renameField "MinSpec" $ THH.simpleField C.ispecsMin [t| ISpec |]
+  , THH.renameField "MaxSpec" $ THH.simpleField C.ispecsMax [t| ISpec |]
+  , THH.renameField "DiskTemplates" $
+      THH.simpleField C.ipolicyDts [t| [DiskTemplate] |]
+  , THH.renameField "VcpuRatio" $
+      THH.simpleField C.ipolicyVcpuRatio [t| Double |]
+  , THH.renameField "SpindleRatio" $
+      THH.simpleField C.ipolicySpindleRatio [t| Double |]
+  ])
+
+-- | Converts an ISpec type to a RSpec one.
+rspecFromISpec :: ISpec -> RSpec
+rspecFromISpec ispec = RSpec { rspecCpu = iSpecCpuCount ispec
+                             , rspecMem = iSpecMemorySize ispec
+                             , rspecDsk = iSpecDiskSize ispec
+                             }
+
+-- | The default instance policy.
+defIPolicy :: IPolicy
+defIPolicy = IPolicy { iPolicyStdSpec = defStdISpec
+                     , iPolicyMinSpec = defMinISpec
+                     , iPolicyMaxSpec = defMaxISpec
+                     -- hardcoding here since Constants.hs exports the
+                     -- string values, not the actual type; and in
+                     -- htools, we are mostly looking at DRBD
+                     , iPolicyDiskTemplates = [minBound..maxBound]
+                     , iPolicyVcpuRatio = C.ipolicyDefaultsVcpuRatio
+                     , iPolicySpindleRatio = C.ipolicyDefaultsSpindleRatio
+                     }
 
 -- | The dynamic resource specs of a machine (i.e. load or load
 -- capacity, as opposed to size).
 data DynUtil = DynUtil
-    { cpuWeight :: Weight -- ^ Standardised CPU usage
-    , memWeight :: Weight -- ^ Standardised memory load
-    , dskWeight :: Weight -- ^ Standardised disk I\/O usage
-    , netWeight :: Weight -- ^ Standardised network usage
-    } deriving (Show, Read, Eq)
+  { cpuWeight :: Weight -- ^ Standardised CPU usage
+  , memWeight :: Weight -- ^ Standardised memory load
+  , dskWeight :: Weight -- ^ Standardised disk I\/O usage
+  , netWeight :: Weight -- ^ Standardised network usage
+  } deriving (Show, Read, Eq)
 
 -- | Initial empty utilisation.
 zeroUtil :: DynUtil
@@ -160,12 +286,12 @@ baseUtil = DynUtil { cpuWeight = 1, memWeight = 1
 -- | Sum two utilisation records.
 addUtil :: DynUtil -> DynUtil -> DynUtil
 addUtil (DynUtil a1 a2 a3 a4) (DynUtil b1 b2 b3 b4) =
-    DynUtil (a1+b1) (a2+b2) (a3+b3) (a4+b4)
+  DynUtil (a1+b1) (a2+b2) (a3+b3) (a4+b4)
 
 -- | Substracts one utilisation record from another.
 subUtil :: DynUtil -> DynUtil -> DynUtil
 subUtil (DynUtil a1 a2 a3 a4) (DynUtil b1 b2 b3 b4) =
-    DynUtil (a1-b1) (a2-b2) (a3-b3) (a4-b4)
+  DynUtil (a1-b1) (a2-b2) (a3-b3) (a4-b4)
 
 -- | The description of an instance placement. It contains the
 -- instance index, the new primary and secondary node, the move being
@@ -174,49 +300,14 @@ type Placement = (Idx, Ndx, Ndx, IMove, Score)
 
 -- | An instance move definition.
 data IMove = Failover                -- ^ Failover the instance (f)
+           | FailoverToAny Ndx       -- ^ Failover to a random node
+                                     -- (fa:np), for shared storage
            | ReplacePrimary Ndx      -- ^ Replace primary (f, r:np, f)
            | ReplaceSecondary Ndx    -- ^ Replace secondary (r:ns)
            | ReplaceAndFailover Ndx  -- ^ Replace secondary, failover (r:np, f)
            | FailoverAndReplace Ndx  -- ^ Failover, replace secondary (f, r:ns)
              deriving (Show, Read)
 
--- | Instance disk template type.
-data DiskTemplate = DTDiskless
-                  | DTFile
-                  | DTSharedFile
-                  | DTPlain
-                  | DTBlock
-                  | DTDrbd8
-                    deriving (Show, Read, Eq, Enum, Bounded)
-
--- | Converts a DiskTemplate to String.
-dtToString :: DiskTemplate -> String
-dtToString DTDiskless   = C.dtDiskless
-dtToString DTFile       = C.dtFile
-dtToString DTSharedFile = C.dtSharedFile
-dtToString DTPlain      = C.dtPlain
-dtToString DTBlock      = C.dtBlock
-dtToString DTDrbd8      = C.dtDrbd8
-
--- | Converts a DiskTemplate from String.
-dtFromString :: (Monad m) => String -> m DiskTemplate
-dtFromString s =
-    case () of
-      _ | s == C.dtDiskless   -> return DTDiskless
-        | s == C.dtFile       -> return DTFile
-        | s == C.dtSharedFile -> return DTSharedFile
-        | s == C.dtPlain      -> return DTPlain
-        | s == C.dtBlock      -> return DTBlock
-        | s == C.dtDrbd8      -> return DTDrbd8
-        | otherwise           -> fail $ "Invalid disk template: " ++ s
-
-instance JSON.JSON DiskTemplate where
-    showJSON = JSON.showJSON . dtToString
-    readJSON s = case JSON.readJSON s of
-                   JSON.Ok s' -> dtFromString s'
-                   JSON.Error e -> JSON.Error $
-                                   "Can't parse disk_template as string: " ++ e
-
 -- | Formatted solution output for one move (involved nodes and
 -- commands.
 type MoveJob = ([Ndx], Idx, IMove, [String])
@@ -236,10 +327,6 @@ connTimeout = 15
 queryTimeout :: Int
 queryTimeout = 60
 
--- | Default vcpu-to-pcpu ratio (randomly chosen value).
-defVcpuRatio :: Double
-defVcpuRatio = 64
-
 -- | Default max disk usage ratio.
 defReservedDiskRatio :: Double
 defReservedDiskRatio = 0
@@ -256,36 +343,6 @@ unitDsk = 256
 unitCpu :: Int
 unitCpu = 1
 
--- | This is similar to the JSON library Result type - /very/ similar,
--- but we want to use it in multiple places, so we abstract it into a
--- mini-library here.
---
--- The failure value for this monad is simply a string.
-data Result a
-    = Bad String
-    | Ok a
-    deriving (Show, Read, Eq)
-
-instance Monad Result where
-    (>>=) (Bad x) _ = Bad x
-    (>>=) (Ok x) fn = fn x
-    return = Ok
-    fail = Bad
-
--- | Simple checker for whether a 'Result' is OK.
-isOk :: Result a -> Bool
-isOk (Ok _) = True
-isOk _ = False
-
--- | Simple checker for whether a 'Result' is a failure.
-isBad :: Result a  -> Bool
-isBad = not . isOk
-
--- | Converter from Either String to 'Result'.
-eitherToResult :: Either String a -> Result a
-eitherToResult (Left s) = Bad s
-eitherToResult (Right v) = Ok v
-
 -- | Reason for an operation's falure.
 data FailMode = FailMem  -- ^ Failed due to not enough RAM
               | FailDisk -- ^ Failed due to not enough disk
@@ -309,9 +366,9 @@ data OpResult a = OpFail FailMode -- ^ Failed operation
                   deriving (Show, Read)
 
 instance Monad OpResult where
-    (OpGood x) >>= fn = fn x
-    (OpFail y) >>= _ = OpFail y
-    return = OpGood
+  (OpGood x) >>= fn = fn x
+  (OpFail y) >>= _ = OpFail y
+  return = OpGood
 
 -- | Conversion from 'OpResult' to 'Result'.
 opToResult :: OpResult a -> Result a
@@ -320,39 +377,27 @@ opToResult (OpGood v) = Ok v
 
 -- | A generic class for items that have updateable names and indices.
 class Element a where
-    -- | Returns the name of the element
-    nameOf  :: a -> String
-    -- | Returns all the known names of the element
-    allNames :: a -> [String]
-    -- | Returns the index of the element
-    idxOf   :: a -> Int
-    -- | Updates the alias of the element
-    setAlias :: a -> String -> a
-    -- | Compute the alias by stripping a given suffix (domain) from
-    -- the name
-    computeAlias :: String -> a -> a
-    computeAlias dom e = setAlias e alias
-        where alias = take (length name - length dom) name
-              name = nameOf e
-    -- | Updates the index of the element
-    setIdx  :: a -> Int -> a
+  -- | Returns the name of the element
+  nameOf  :: a -> String
+  -- | Returns all the known names of the element
+  allNames :: a -> [String]
+  -- | Returns the index of the element
+  idxOf   :: a -> Int
+  -- | Updates the alias of the element
+  setAlias :: a -> String -> a
+  -- | Compute the alias by stripping a given suffix (domain) from
+  -- the name
+  computeAlias :: String -> a -> a
+  computeAlias dom e = setAlias e alias
+    where alias = take (length name - length dom) name
+          name = nameOf e
+  -- | Updates the index of the element
+  setIdx  :: a -> Int -> a
 
 -- | The iallocator node-evacuate evac_mode type.
-data EvacMode = ChangePrimary
-              | ChangeSecondary
-              | ChangeAll
-                deriving (Show, Read)
-
-instance JSON.JSON EvacMode where
-    showJSON mode = case mode of
-                      ChangeAll       -> JSON.showJSON C.iallocatorNevacAll
-                      ChangePrimary   -> JSON.showJSON C.iallocatorNevacPri
-                      ChangeSecondary -> JSON.showJSON C.iallocatorNevacSec
-    readJSON v =
-        case JSON.readJSON v of
-          JSON.Ok s | s == C.iallocatorNevacAll -> return ChangeAll
-                    | s == C.iallocatorNevacPri -> return ChangePrimary
-                    | s == C.iallocatorNevacSec -> return ChangeSecondary
-                    | otherwise -> fail $ "Invalid evacuate mode " ++ s
-          JSON.Error e -> JSON.Error $
-                          "Can't parse evacuate mode as string: " ++ e
+$(THH.declareSADT "EvacMode"
+       [ ("ChangePrimary",   'C.iallocatorNevacPri)
+       , ("ChangeSecondary", 'C.iallocatorNevacSec)
+       , ("ChangeAll",       'C.iallocatorNevacAll)
+       ])
+$(THH.makeJSONInstance ''EvacMode)
index d4e8024..2b21518 100644 (file)
@@ -2,7 +2,7 @@
 
 {-
 
-Copyright (C) 2009, 2010, 2011 Google Inc.
+Copyright (C) 2009, 2010, 2011, 2012 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
@@ -22,43 +22,35 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 -}
 
 module Ganeti.HTools.Utils
-    (
-      debug
-    , debugFn
-    , debugXy
-    , sepSplit
-    , stdDev
-    , if'
-    , select
-    , commaJoin
-    , readEitherString
-    , JSRecord
-    , loadJSArray
-    , fromObj
-    , fromObjWithDefault
-    , maybeFromObj
-    , tryFromObj
-    , fromJVal
-    , asJSObject
-    , asObjectList
-    , fromJResult
-    , tryRead
-    , formatTable
-    , annotateResult
-    , defaultGroupID
-    , parseUnit
-    ) where
-
-import Control.Monad (liftM)
-import Data.Char (toUpper)
+  ( debug
+  , debugFn
+  , debugXy
+  , sepSplit
+  , stdDev
+  , if'
+  , select
+  , applyIf
+  , commaJoin
+  , ensureQuoted
+  , tryRead
+  , formatTable
+  , printTable
+  , parseUnit
+  , plural
+  , exitIfBad
+  , exitErr
+  , exitWhen
+  , exitUnless
+  ) where
+
+import Data.Char (toUpper, isAlphaNum)
 import Data.List
-import Data.Maybe (fromMaybe)
-import qualified Text.JSON as J
-import Text.Printf (printf)
 
 import Debug.Trace
 
-import Ganeti.HTools.Types
+import Ganeti.BasicTypes
+import System.IO
+import System.Exit
 
 -- * Debug functions
 
@@ -73,10 +65,14 @@ debugFn fn x = debug (fn x) `seq` x
 
 -- | Show the first parameter before returning the second one.
 debugXy :: Show a => a -> b -> b
-debugXy a b = debug a `seq` b
+debugXy = seq . debug
 
 -- * Miscellaneous
 
+-- | Apply the function if condition holds, otherwise use default value.
+applyIf :: Bool -> (a -> a) -> a -> a
+applyIf b f x = if b then f x else x
+
 -- | Comma-join a string list.
 commaJoin :: [String] -> String
 commaJoin = intercalate ","
@@ -84,12 +80,23 @@ commaJoin = intercalate ","
 -- | Split a list on a separator and return an array.
 sepSplit :: Eq a => a -> [a] -> [[a]]
 sepSplit sep s
-    | null s    = []
-    | null xs   = [x]
-    | null ys   = [x,[]]
-    | otherwise = x:sepSplit sep ys
-    where (x, xs) = break (== sep) s
-          ys = drop 1 xs
+  | null s    = []
+  | null xs   = [x]
+  | null ys   = [x,[]]
+  | otherwise = x:sepSplit sep ys
+  where (x, xs) = break (== sep) s
+        ys = drop 1 xs
+
+-- | Simple pluralize helper
+plural :: Int -> String -> String -> String
+plural 1 s _ = s
+plural _ _ p = p
+
+-- | Ensure a value is quoted if needed.
+ensureQuoted :: String -> String
+ensureQuoted v = if not (all (\c -> isAlphaNum c || c == '.') v)
+                 then '\'':v ++ "'"
+                 else v
 
 -- * Mathematical functions
 
@@ -125,99 +132,6 @@ if' :: Bool -- ^ condition
 if' True x _ = x
 if' _    _ y = y
 
--- | Return the first result with a True condition, or the default otherwise.
-select :: a            -- ^ default result
-       -> [(Bool, a)]  -- ^ list of \"condition, result\"
-       -> a            -- ^ first result which has a True condition, or default
-select def = maybe def snd . find fst
-
--- * JSON-related functions
-
--- | A type alias for the list-based representation of J.JSObject.
-type JSRecord = [(String, J.JSValue)]
-
--- | Converts a JSON Result into a monadic value.
-fromJResult :: Monad m => String -> J.Result a -> m a
-fromJResult s (J.Error x) = fail (s ++ ": " ++ x)
-fromJResult _ (J.Ok x) = return x
-
--- | Tries to read a string from a JSON value.
---
--- In case the value was not a string, we fail the read (in the
--- context of the current monad.
-readEitherString :: (Monad m) => J.JSValue -> m String
-readEitherString v =
-    case v of
-      J.JSString s -> return $ J.fromJSString s
-      _ -> fail "Wrong JSON type"
-
--- | Converts a JSON message into an array of JSON objects.
-loadJSArray :: (Monad m)
-               => String -- ^ Operation description (for error reporting)
-               -> String -- ^ Input message
-               -> m [J.JSObject J.JSValue]
-loadJSArray s = fromJResult s . J.decodeStrict
-
--- | Reads the value of a key in a JSON object.
-fromObj :: (J.JSON a, Monad m) => JSRecord -> String -> m a
-fromObj o k =
-    case lookup k o of
-      Nothing -> fail $ printf "key '%s' not found, object contains only %s"
-                 k (show (map fst o))
-      Just val -> fromKeyValue k val
-
--- | Reads the value of an optional key in a JSON object.
-maybeFromObj :: (J.JSON a, Monad m) =>
-                JSRecord -> String -> m (Maybe a)
-maybeFromObj o k =
-    case lookup k o of
-      Nothing -> return Nothing
-      Just val -> liftM Just (fromKeyValue k val)
-
--- | Reads the value of a key in a JSON object with a default if missing.
-fromObjWithDefault :: (J.JSON a, Monad m) =>
-                      JSRecord -> String -> a -> m a
-fromObjWithDefault o k d = liftM (fromMaybe d) $ maybeFromObj o k
-
--- | Reads a JValue, that originated from an object key.
-fromKeyValue :: (J.JSON a, Monad m)
-              => String     -- ^ The key name
-              -> J.JSValue  -- ^ The value to read
-              -> m a
-fromKeyValue k val =
-  fromJResult (printf "key '%s', value '%s'" k (show val)) (J.readJSON val)
-
--- | Annotate a Result with an ownership information.
-annotateResult :: String -> Result a -> Result a
-annotateResult owner (Bad s) = Bad $ owner ++ ": " ++ s
-annotateResult _ v = v
-
--- | Try to extract a key from a object with better error reporting
--- than fromObj.
-tryFromObj :: (J.JSON a) =>
-              String     -- ^ Textual "owner" in error messages
-           -> JSRecord   -- ^ The object array
-           -> String     -- ^ The desired key from the object
-           -> Result a
-tryFromObj t o = annotateResult t . fromObj o
-
--- | Small wrapper over readJSON.
-fromJVal :: (Monad m, J.JSON a) => J.JSValue -> m a
-fromJVal v =
-    case J.readJSON v of
-      J.Error s -> fail ("Cannot convert value '" ++ show v ++
-                         "', error: " ++ s)
-      J.Ok x -> return x
-
--- | Converts a JSON value into a JSON object.
-asJSObject :: (Monad m) => J.JSValue -> m (J.JSObject J.JSValue)
-asJSObject (J.JSObject a) = return a
-asJSObject _ = fail "not an object"
-
--- | Coneverts a list of JSON values into a list of JSON objects.
-asObjectList :: (Monad m) => [J.JSValue] -> m [J.JSObject J.JSValue]
-asObjectList = mapM asJSObject
-
 -- * Parsing utility functions
 
 -- | Parse results from readsPrec.
@@ -251,9 +165,30 @@ formatTable vals numpos =
                     ) (zip3 vtrans numpos mlens)
    in transpose expnd
 
--- | Default group UUID (just a string, not a real UUID).
-defaultGroupID :: GroupID
-defaultGroupID = "00000000-0000-0000-0000-000000000000"
+-- | Constructs a printable table from given header and rows
+printTable :: String -> [String] -> [[String]] -> [Bool] -> String
+printTable lp header rows isnum =
+  unlines . map ((++) lp) . map ((:) ' ' . unwords) $
+  formatTable (header:rows) isnum
+
+-- | Converts a unit (e.g. m or GB) into a scaling factor.
+parseUnitValue :: (Monad m) => String -> m Rational
+parseUnitValue unit
+  -- binary conversions first
+  | null unit                     = return 1
+  | unit == "m" || upper == "MIB" = return 1
+  | unit == "g" || upper == "GIB" = return kbBinary
+  | unit == "t" || upper == "TIB" = return $ kbBinary * kbBinary
+  -- SI conversions
+  | unit == "M" || upper == "MB"  = return mbFactor
+  | unit == "G" || upper == "GB"  = return $ mbFactor * kbDecimal
+  | unit == "T" || upper == "TB"  = return $ mbFactor * kbDecimal * kbDecimal
+  | otherwise = fail $ "Unknown unit '" ++ unit ++ "'"
+  where upper = map toUpper unit
+        kbBinary = 1024 :: Rational
+        kbDecimal = 1000 :: Rational
+        decToBin = kbDecimal / kbBinary -- factor for 1K conversion
+        mbFactor = decToBin * decToBin -- twice the factor for just 1K
 
 -- | Tries to extract number and scale from the given string.
 --
@@ -262,22 +197,36 @@ defaultGroupID = "00000000-0000-0000-0000-000000000000"
 -- value in MiB.
 parseUnit :: (Monad m, Integral a, Read a) => String -> m a
 parseUnit str =
-    -- TODO: enhance this by splitting the unit parsing code out and
-    -- accepting floating-point numbers
-    case reads str of
-      [(v, suffix)] ->
-          let unit = dropWhile (== ' ') suffix
-              upper = map toUpper unit
-              siConvert x = x * 1000000 `div` 1048576
-          in case () of
-               _ | null unit -> return v
-                 | unit == "m" || upper == "MIB" -> return v
-                 | unit == "M" || upper == "MB"  -> return $ siConvert v
-                 | unit == "g" || upper == "GIB" -> return $ v * 1024
-                 | unit == "G" || upper == "GB"  -> return $ siConvert
-                                                    (v * 1000)
-                 | unit == "t" || upper == "TIB" -> return $ v * 1048576
-                 | unit == "T" || upper == "TB"  -> return $
-                                                    siConvert (v * 1000000)
-                 | otherwise -> fail $ "Unknown unit '" ++ unit ++ "'"
-      _ -> fail $ "Can't parse string '" ++ str ++ "'"
+  -- TODO: enhance this by splitting the unit parsing code out and
+  -- accepting floating-point numbers
+  case (reads str::[(Int, String)]) of
+    [(v, suffix)] ->
+      let unit = dropWhile (== ' ') suffix
+      in do
+        scaling <- parseUnitValue unit
+        return $ truncate (fromIntegral v * scaling)
+    _ -> fail $ "Can't parse string '" ++ str ++ "'"
+
+-- | Unwraps a 'Result', exiting the program if it is a 'Bad' value,
+-- otherwise returning the actual contained value.
+exitIfBad :: String -> Result a -> IO a
+exitIfBad msg (Bad s) = do
+  hPutStrLn stderr $ "Error: " ++ msg ++ ": " ++ s
+  exitWith (ExitFailure 1)
+exitIfBad _ (Ok v) = return v
+
+-- | Exits immediately with an error message.
+exitErr :: String -> IO a
+exitErr errmsg = do
+  hPutStrLn stderr $ "Error: " ++ errmsg ++ "."
+  exitWith (ExitFailure 1)
+
+-- | Exits with an error message if the given boolean condition if true.
+exitWhen :: Bool -> String -> IO ()
+exitWhen True msg = exitErr msg
+exitWhen False _  = return ()
+
+-- | Exits with an error message /unless/ the given boolean condition
+-- if true, the opposite of 'exitWhen'.
+exitUnless :: Bool -> String -> IO ()
+exitUnless cond = exitWhen (not cond)
diff --git a/htools/Ganeti/Hash.hs b/htools/Ganeti/Hash.hs
new file mode 100644 (file)
index 0000000..56d6601
--- /dev/null
@@ -0,0 +1,60 @@
+{-| Crypto-related helper functions.
+
+-}
+
+{-
+
+Copyright (C) 2011, 2012 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.Hash
+  ( computeMac
+  , verifyMac
+  , HashKey
+  ) where
+
+import qualified Data.ByteString as B
+import Data.Char
+import Data.HMAC (hmac_sha1)
+import qualified Data.Text as T
+import Data.Text.Encoding (encodeUtf8)
+import Data.Word
+import Text.Printf (printf)
+
+-- | Type alias for the hash key. This depends on the library being
+-- used.
+type HashKey = [Word8]
+
+-- | Converts a string to a list of bytes.
+stringToWord8 :: String -> HashKey
+stringToWord8 = B.unpack . encodeUtf8 . T.pack
+
+-- | Converts a list of bytes to a string.
+word8ToString :: HashKey -> String
+word8ToString = concat . map (printf "%02x")
+
+-- | Computes the HMAC for a given key/test and salt.
+computeMac :: HashKey -> Maybe String -> String -> String
+computeMac key salt text =
+  word8ToString . hmac_sha1 key . stringToWord8 $ maybe text (++ text) salt
+
+-- | Verifies the HMAC for a given message.
+verifyMac :: HashKey -> Maybe String -> String -> String -> Bool
+verifyMac key salt text digest =
+  map toLower digest == computeMac key salt text
index ed7bc7d..405f833 100644 (file)
@@ -1,10 +1,12 @@
+{-# LANGUAGE TemplateHaskell #-}
+
 {-| Implementation of the job information.
 
 -}
 
 {-
 
-Copyright (C) 2009, 2010, 2011 Google Inc.
+Copyright (C) 2009, 2010, 2011, 2012 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
@@ -24,75 +26,37 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 -}
 
 module Ganeti.Jobs
-    ( OpStatus(..)
-    , JobStatus(..)
-    ) where
+  ( OpStatus(..)
+  , JobStatus(..)
+  ) where
 
 import Text.JSON (readJSON, showJSON, JSON)
-import qualified Text.JSON as J
 
 import qualified Ganeti.Constants as C
+import qualified Ganeti.THH as THH
 
 -- | Our ADT for the OpCode status at runtime (while in a job).
-data OpStatus = OP_STATUS_QUEUED
-              | OP_STATUS_WAITING
-              | OP_STATUS_CANCELING
-              | OP_STATUS_RUNNING
-              | OP_STATUS_CANCELED
-              | OP_STATUS_SUCCESS
-              | OP_STATUS_ERROR
-                deriving (Eq, Enum, Bounded, Show, Read)
-
-instance JSON OpStatus where
-    showJSON os = showJSON w
-      where w = case os of
-              OP_STATUS_QUEUED    -> C.opStatusQueued
-              OP_STATUS_WAITING   -> C.opStatusWaiting
-              OP_STATUS_CANCELING -> C.opStatusCanceling
-              OP_STATUS_RUNNING   -> C.opStatusRunning
-              OP_STATUS_CANCELED  -> C.opStatusCanceled
-              OP_STATUS_SUCCESS   -> C.opStatusSuccess
-              OP_STATUS_ERROR     -> C.opStatusError
-    readJSON s = case readJSON s of
-      J.Ok v | v == C.opStatusQueued    -> J.Ok OP_STATUS_QUEUED
-             | v == C.opStatusWaiting   -> J.Ok OP_STATUS_WAITING
-             | v == C.opStatusCanceling -> J.Ok OP_STATUS_CANCELING
-             | v == C.opStatusRunning   -> J.Ok OP_STATUS_RUNNING
-             | v == C.opStatusCanceled  -> J.Ok OP_STATUS_CANCELED
-             | v == C.opStatusSuccess   -> J.Ok OP_STATUS_SUCCESS
-             | v == C.opStatusError     -> J.Ok OP_STATUS_ERROR
-             | otherwise -> J.Error ("Unknown opcode status " ++ v)
-      _ -> J.Error ("Cannot parse opcode status " ++ show s)
+$(THH.declareSADT "OpStatus"
+       [ ("OP_STATUS_QUEUED",    'C.opStatusQueued)
+       , ("OP_STATUS_WAITING",   'C.opStatusWaiting)
+       , ("OP_STATUS_CANCELING", 'C.opStatusCanceling)
+       , ("OP_STATUS_RUNNING",   'C.opStatusRunning)
+       , ("OP_STATUS_CANCELED",  'C.opStatusCanceled)
+       , ("OP_STATUS_SUCCESS",   'C.opStatusSuccess)
+       , ("OP_STATUS_ERROR",     'C.opStatusError)
+       ])
+$(THH.makeJSONInstance ''OpStatus)
 
 -- | The JobStatus data type. Note that this is ordered especially
 -- such that greater\/lesser comparison on values of this type makes
 -- sense.
-data JobStatus = JOB_STATUS_QUEUED
-               | JOB_STATUS_WAITING
-               | JOB_STATUS_RUNNING
-               | JOB_STATUS_SUCCESS
-               | JOB_STATUS_CANCELING
-               | JOB_STATUS_CANCELED
-               | JOB_STATUS_ERROR
-                 deriving (Eq, Enum, Ord, Bounded, Show, Read)
-
-instance JSON JobStatus where
-    showJSON js = showJSON w
-        where w = case js of
-                JOB_STATUS_QUEUED    -> C.jobStatusQueued
-                JOB_STATUS_WAITING   -> C.jobStatusWaiting
-                JOB_STATUS_CANCELING -> C.jobStatusCanceling
-                JOB_STATUS_RUNNING   -> C.jobStatusRunning
-                JOB_STATUS_CANCELED  -> C.jobStatusCanceled
-                JOB_STATUS_SUCCESS   -> C.jobStatusSuccess
-                JOB_STATUS_ERROR     -> C.jobStatusError
-    readJSON s = case readJSON s of
-      J.Ok v | v == C.jobStatusQueued    -> J.Ok JOB_STATUS_QUEUED
-             | v == C.jobStatusWaiting   -> J.Ok JOB_STATUS_WAITING
-             | v == C.jobStatusCanceling -> J.Ok JOB_STATUS_CANCELING
-             | v == C.jobStatusRunning   -> J.Ok JOB_STATUS_RUNNING
-             | v == C.jobStatusSuccess   -> J.Ok JOB_STATUS_SUCCESS
-             | v == C.jobStatusCanceled  -> J.Ok JOB_STATUS_CANCELED
-             | v == C.jobStatusError     -> J.Ok JOB_STATUS_ERROR
-             | otherwise -> J.Error ("Unknown job status " ++ v)
-      _ -> J.Error ("Unknown job status " ++ show s)
+$(THH.declareSADT "JobStatus"
+       [ ("JOB_STATUS_QUEUED",    'C.jobStatusQueued)
+       , ("JOB_STATUS_WAITING",   'C.jobStatusWaiting)
+       , ("JOB_STATUS_CANCELING", 'C.jobStatusCanceling)
+       , ("JOB_STATUS_RUNNING",   'C.jobStatusRunning)
+       , ("JOB_STATUS_CANCELED",  'C.jobStatusCanceled)
+       , ("JOB_STATUS_SUCCESS",   'C.jobStatusSuccess)
+       , ("JOB_STATUS_ERROR",     'C.jobStatusError)
+       ])
+$(THH.makeJSONInstance ''JobStatus)
diff --git a/htools/Ganeti/Logging.hs b/htools/Ganeti/Logging.hs
new file mode 100644 (file)
index 0000000..c717757
--- /dev/null
@@ -0,0 +1,156 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+{-| Implementation of the Ganeti logging functionality.
+
+This currently lacks the following (FIXME):
+
+- log file reopening
+
+Note that this requires the hslogger library version 1.1 and above.
+
+-}
+
+{-
+
+Copyright (C) 2011, 2012 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.Logging
+  ( setupLogging
+  , logDebug
+  , logInfo
+  , logNotice
+  , logWarning
+  , logError
+  , logCritical
+  , logAlert
+  , logEmergency
+  , SyslogUsage(..)
+  , syslogUsageToRaw
+  , syslogUsageFromRaw
+  ) where
+
+import Control.Monad (when)
+import System.Log.Logger
+import System.Log.Handler.Simple
+import System.Log.Handler.Syslog
+import System.Log.Handler (setFormatter, LogHandler)
+import System.Log.Formatter
+import System.IO
+
+import Ganeti.THH
+import qualified Ganeti.Constants as C
+
+-- | Syslog usage type.
+$(declareSADT "SyslogUsage"
+  [ ("SyslogNo",   'C.syslogNo)
+  , ("SyslogYes",  'C.syslogYes)
+  , ("SyslogOnly", 'C.syslogOnly)
+  ])
+
+-- | Builds the log formatter.
+logFormatter :: String  -- ^ Program
+             -> Bool    -- ^ Multithreaded
+             -> Bool    -- ^ Syslog
+             -> LogFormatter a
+logFormatter prog mt syslog =
+  let parts = [ if syslog
+                  then "[$pid]:"
+                  else "$time: " ++ prog ++ " pid=$pid"
+              , if mt then if syslog then " ($tid)" else "/$tid"
+                  else ""
+              , " $prio $msg"
+              ]
+  in simpleLogFormatter $ concat parts
+
+-- | Helper to open and set the formatter on a log if enabled by a
+-- given condition, otherwise returning an empty list.
+openFormattedHandler :: (LogHandler a) => Bool
+                     -> LogFormatter a -> IO a -> IO [a]
+openFormattedHandler False _ _ = return []
+openFormattedHandler True fmt opener = do
+  handler <- opener
+  return [setFormatter handler fmt]
+
+-- | Sets up the logging configuration.
+setupLogging :: Maybe String    -- ^ Log file
+             -> String    -- ^ Program name
+             -> Bool      -- ^ Debug level
+             -> Bool      -- ^ Log to stderr
+             -> Bool      -- ^ Log to console
+             -> SyslogUsage -- ^ Syslog usage
+             -> IO ()
+setupLogging logf program debug stderr_logging console syslog = do
+  let level = if debug then DEBUG else INFO
+      destf = if console then Just C.devConsole else logf
+      fmt = logFormatter program False False
+      file_logging = syslog /= SyslogOnly
+
+  updateGlobalLogger rootLoggerName (setLevel level)
+
+  stderr_handlers <- openFormattedHandler stderr_logging fmt $
+                     streamHandler stderr level
+
+  file_handlers <- case destf of
+                     Nothing -> return []
+                     Just path -> openFormattedHandler file_logging fmt $
+                                  fileHandler path level
+
+  let handlers = concat [file_handlers, stderr_handlers]
+  updateGlobalLogger rootLoggerName $ setHandlers handlers
+  -- syslog handler is special (another type, still instance of the
+  -- typeclass, and has a built-in formatter), so we can't pass it in
+  -- the above list
+  when (syslog /= SyslogNo) $ do
+    syslog_handler <- openlog program [PID] DAEMON INFO
+    updateGlobalLogger rootLoggerName $ addHandler syslog_handler
+
+-- * Logging function aliases
+
+-- | Log at debug level.
+logDebug :: String -> IO ()
+logDebug = debugM rootLoggerName
+
+-- | Log at info level.
+logInfo :: String -> IO ()
+logInfo = infoM rootLoggerName
+
+-- | Log at notice level.
+logNotice :: String -> IO ()
+logNotice = noticeM rootLoggerName
+
+-- | Log at warning level.
+logWarning :: String -> IO ()
+logWarning = warningM rootLoggerName
+
+-- | Log at error level.
+logError :: String -> IO ()
+logError = errorM rootLoggerName
+
+-- | Log at critical level.
+logCritical :: String -> IO ()
+logCritical = criticalM rootLoggerName
+
+-- | Log at alert level.
+logAlert :: String -> IO ()
+logAlert = alertM rootLoggerName
+
+-- | Log at emergency level.
+logEmergency :: String -> IO ()
+logEmergency = emergencyM rootLoggerName
index f475245..4c0daed 100644 (file)
@@ -1,10 +1,12 @@
+{-# LANGUAGE TemplateHaskell #-}
+
 {-| Implementation of the Ganeti LUXI interface.
 
 -}
 
 {-
 
-Copyright (C) 2009, 2010, 2011 Google Inc.
+Copyright (C) 2009, 2010, 2011, 2012 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
@@ -24,14 +26,21 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 -}
 
 module Ganeti.Luxi
-    ( LuxiOp(..)
-    , Client
-    , getClient
-    , closeClient
-    , callMethod
-    , submitManyJobs
-    , queryJobsStatus
-    ) where
+  ( LuxiOp(..)
+  , QrViaLuxi(..)
+  , ResultStatus(..)
+  , LuxiReq(..)
+  , Client
+  , checkRS
+  , getClient
+  , closeClient
+  , callMethod
+  , submitManyJobs
+  , queryJobsStatus
+  , buildCall
+  , validateCall
+  , decodeCall
+  ) where
 
 import Data.IORef
 import Control.Monad
@@ -41,61 +50,130 @@ import Text.JSON.Types
 import System.Timeout
 import qualified Network.Socket as S
 
-import Ganeti.HTools.Utils
+import Ganeti.HTools.JSON
 import Ganeti.HTools.Types
+import Ganeti.HTools.Utils
 
+import Ganeti.Constants
 import Ganeti.Jobs (JobStatus)
 import Ganeti.OpCodes (OpCode)
+import Ganeti.THH
 
 -- * Utility functions
 
 -- | Wrapper over System.Timeout.timeout that fails in the IO monad.
 withTimeout :: Int -> String -> IO a -> IO a
 withTimeout secs descr action = do
-    result <- timeout (secs * 1000000) action
-    (case result of
-       Nothing -> fail $ "Timeout in " ++ descr
-       Just v -> return v)
+  result <- timeout (secs * 1000000) action
+  case result of
+    Nothing -> fail $ "Timeout in " ++ descr
+    Just v -> return v
 
 -- * Generic protocol functionality
 
--- | Currently supported Luxi operations.
-data LuxiOp = QueryInstances [String] [String] Bool
-            | QueryNodes [String] [String] Bool
-            | QueryGroups [String] [String] Bool
-            | QueryJobs [Int] [String]
-            | QueryExports [String] Bool
-            | QueryConfigValues [String]
-            | QueryClusterInfo
-            | QueryTags String String
-            | SubmitJob [OpCode]
-            | SubmitManyJobs [[OpCode]]
-            | WaitForJobChange Int [String] JSValue JSValue Int
-            | ArchiveJob Int
-            | AutoArchiveJobs Int Int
-            | CancelJob Int
-            | SetDrainFlag Bool
-            | SetWatcherPause Double
-              deriving (Show, Read)
+$(declareSADT "QrViaLuxi"
+  [ ("QRLock", 'qrLock)
+  , ("QRInstance", 'qrInstance)
+  , ("QRNode", 'qrNode)
+  , ("QRGroup", 'qrGroup)
+  , ("QROs", 'qrOs)
+  ])
+$(makeJSONInstance ''QrViaLuxi)
+
+-- | Currently supported Luxi operations and JSON serialization.
+$(genLuxiOp "LuxiOp"
+  [(luxiReqQuery,
+    [ ("what",    [t| QrViaLuxi |], [| id |])
+    , ("fields",  [t| [String]  |], [| id |])
+    , ("qfilter", [t| ()        |], [| const JSNull |])
+    ])
+  , (luxiReqQueryNodes,
+     [ ("names",  [t| [String] |], [| id |])
+     , ("fields", [t| [String] |], [| id |])
+     , ("lock",   [t| Bool     |], [| id |])
+     ])
+  , (luxiReqQueryGroups,
+     [ ("names",  [t| [String] |], [| id |])
+     , ("fields", [t| [String] |], [| id |])
+     , ("lock",   [t| Bool     |], [| id |])
+     ])
+  , (luxiReqQueryInstances,
+     [ ("names",  [t| [String] |], [| id |])
+     , ("fields", [t| [String] |], [| id |])
+     , ("lock",   [t| Bool     |], [| id |])
+     ])
+  , (luxiReqQueryJobs,
+     [ ("ids",    [t| [Int]    |], [| map show |])
+     , ("fields", [t| [String] |], [| id |])
+     ])
+  , (luxiReqQueryExports,
+     [ ("nodes", [t| [String] |], [| id |])
+     , ("lock",  [t| Bool     |], [| id |])
+     ])
+  , (luxiReqQueryConfigValues,
+     [ ("fields", [t| [String] |], [| id |]) ]
+    )
+  , (luxiReqQueryClusterInfo, [])
+  , (luxiReqQueryTags,
+     [ ("kind", [t| String |], [| id |])
+     , ("name", [t| String |], [| id |])
+     ])
+  , (luxiReqSubmitJob,
+     [ ("job", [t| [OpCode] |], [| id |]) ]
+    )
+  , (luxiReqSubmitManyJobs,
+     [ ("ops", [t| [[OpCode]] |], [| id |]) ]
+    )
+  , (luxiReqWaitForJobChange,
+     [ ("job",      [t| Int     |], [| show |])
+     , ("fields",   [t| [String]|], [| id |])
+     , ("prev_job", [t| JSValue |], [| id |])
+     , ("prev_log", [t| JSValue |], [| id |])
+     , ("tmout",    [t| Int     |], [| id |])
+     ])
+  , (luxiReqArchiveJob,
+     [ ("job", [t| Int |], [| show |]) ]
+    )
+  , (luxiReqAutoArchiveJobs,
+     [ ("age",   [t| Int |], [| id |])
+     , ("tmout", [t| Int |], [| id |])
+     ])
+  , (luxiReqCancelJob,
+     [ ("job", [t| Int |], [| show |]) ]
+    )
+  , (luxiReqSetDrainFlag,
+     [ ("flag", [t| Bool |], [| id |]) ]
+    )
+  , (luxiReqSetWatcherPause,
+     [ ("duration", [t| Double |], [| id |]) ]
+    )
+  ])
+
+$(makeJSONInstance ''LuxiReq)
 
 -- | The serialisation of LuxiOps into strings in messages.
-strOfOp :: LuxiOp -> String
-strOfOp QueryNodes {}        = "QueryNodes"
-strOfOp QueryGroups {}       = "QueryGroups"
-strOfOp QueryInstances {}    = "QueryInstances"
-strOfOp QueryJobs {}         = "QueryJobs"
-strOfOp QueryExports {}      = "QueryExports"
-strOfOp QueryConfigValues {} = "QueryConfigValues"
-strOfOp QueryClusterInfo {}  = "QueryClusterInfo"
-strOfOp QueryTags {}         = "QueryTags"
-strOfOp SubmitManyJobs {}    = "SubmitManyJobs"
-strOfOp WaitForJobChange {}  = "WaitForJobChange"
-strOfOp SubmitJob {}         = "SubmitJob"
-strOfOp ArchiveJob {}        = "ArchiveJob"
-strOfOp AutoArchiveJobs {}   = "AutoArchiveJobs"
-strOfOp CancelJob {}         = "CancelJob"
-strOfOp SetDrainFlag {}      = "SetDrainFlag"
-strOfOp SetWatcherPause {}   = "SetWatcherPause"
+$(genStrOfOp ''LuxiOp "strOfOp")
+
+$(declareIADT "ResultStatus"
+  [ ("RSNormal", 'rsNormal)
+  , ("RSUnknown", 'rsUnknown)
+  , ("RSNoData", 'rsNodata)
+  , ("RSUnavailable", 'rsUnavail)
+  , ("RSOffline", 'rsOffline)
+  ])
+
+$(makeJSONInstance ''ResultStatus)
+
+-- | Type holding the initial (unparsed) Luxi call.
+data LuxiCall = LuxiCall LuxiReq JSValue
+
+-- | Check that ResultStatus is success or fail with descriptive message.
+checkRS :: (Monad m) => ResultStatus -> a -> m a
+checkRS RSNormal val    = return val
+checkRS RSUnknown _     = fail "Unknown field"
+checkRS RSNoData _      = fail "No data for a field"
+checkRS RSUnavailable _ = fail "Ganeti reports unavailable data"
+checkRS RSOffline _     = fail "Ganeti reports resource as offline"
 
 -- | The end-of-message separator.
 eOM :: Char
@@ -108,11 +186,7 @@ data MsgKeys = Method
              | Result
 
 -- | The serialisation of MsgKeys into strings in messages.
-strOfKey :: MsgKeys -> String
-strOfKey Method = "method"
-strOfKey Args = "args"
-strOfKey Success = "success"
-strOfKey Result = "result"
+$(genStrOfKey ''MsgKeys "strOfKey")
 
 -- | Luxi client encapsulation.
 data Client = Client { socket :: S.Socket   -- ^ The socket of the client
@@ -122,11 +196,11 @@ data Client = Client { socket :: S.Socket   -- ^ The socket of the client
 -- | Connects to the master daemon and returns a luxi Client.
 getClient :: String -> IO Client
 getClient path = do
-    s <- S.socket S.AF_UNIX S.Stream S.defaultProtocol
-    withTimeout connTimeout "creating luxi connection" $
-                S.connect s (S.SockAddrUnix path)
-    rf <- newIORef ""
-    return Client { socket=s, rbuf=rf}
+  s <- S.socket S.AF_UNIX S.Stream S.defaultProtocol
+  withTimeout connTimeout "creating luxi connection" $
+              S.connect s (S.SockAddrUnix path)
+  rf <- newIORef ""
+  return Client { socket=s, rbuf=rf}
 
 -- | Closes the client socket.
 closeClient :: Client -> IO ()
@@ -135,12 +209,12 @@ closeClient = S.sClose . socket
 -- | Sends a message over a luxi transport.
 sendMsg :: Client -> String -> IO ()
 sendMsg s buf =
-    let _send obuf = do
-          sbytes <- withTimeout queryTimeout
-                    "sending luxi message" $
-                    S.send (socket s) obuf
-          unless (sbytes == length obuf) $ _send (drop sbytes obuf)
-    in _send (buf ++ [eOM])
+  let _send obuf = do
+        sbytes <- withTimeout queryTimeout
+                  "sending luxi message" $
+                  S.send (socket s) obuf
+        unless (sbytes == length obuf) $ _send (drop sbytes obuf)
+  in _send (buf ++ [eOM])
 
 -- | Waits for a message over a luxi transport.
 recvMsg :: Client -> IO String
@@ -149,50 +223,114 @@ recvMsg s = do
               nbuf <- withTimeout queryTimeout "reading luxi response" $
                       S.recv (socket s) 4096
               let (msg, remaining) = break (eOM ==) nbuf
-              (if null remaining
-               then _recv (obuf ++ msg)
-               else return (obuf ++ msg, tail remaining))
+              if null remaining
+                then _recv (obuf ++ msg)
+                else return (obuf ++ msg, tail remaining)
   cbuf <- readIORef $ rbuf s
   let (imsg, ibuf) = break (eOM ==) cbuf
   (msg, nbuf) <-
-      (if null ibuf      -- if old buffer didn't contain a full message
-       then _recv cbuf   -- then we read from network
-       else return (imsg, tail ibuf)) -- else we return data from our buffer
+    if null ibuf      -- if old buffer didn't contain a full message
+      then _recv cbuf   -- then we read from network
+      else return (imsg, tail ibuf) -- else we return data from our buffer
   writeIORef (rbuf s) nbuf
   return msg
 
--- | Compute the serialized form of a Luxi operation.
-opToArgs :: LuxiOp -> JSValue
-opToArgs (QueryNodes names fields lock) = J.showJSON (names, fields, lock)
-opToArgs (QueryGroups names fields lock) = J.showJSON (names, fields, lock)
-opToArgs (QueryInstances names fields lock) = J.showJSON (names, fields, lock)
-opToArgs (QueryJobs ids fields) = J.showJSON (map show ids, fields)
-opToArgs (QueryExports nodes lock) = J.showJSON (nodes, lock)
-opToArgs (QueryConfigValues fields) = J.showJSON fields
-opToArgs (QueryClusterInfo) = J.showJSON ()
-opToArgs (QueryTags kind name) =  J.showJSON (kind, name)
-opToArgs (SubmitJob j) = J.showJSON j
-opToArgs (SubmitManyJobs ops) = J.showJSON ops
--- This is special, since the JSON library doesn't export an instance
--- of a 5-tuple
-opToArgs (WaitForJobChange a b c d e) =
-    JSArray [ J.showJSON a, J.showJSON b, J.showJSON c
-            , J.showJSON d, J.showJSON e]
-opToArgs (ArchiveJob a) = J.showJSON (show a)
-opToArgs (AutoArchiveJobs a b) = J.showJSON (a, b)
-opToArgs (CancelJob a) = J.showJSON (show a)
-opToArgs (SetDrainFlag flag) = J.showJSON flag
-opToArgs (SetWatcherPause duration) = J.showJSON [duration]
-
 -- | Serialize a request to String.
 buildCall :: LuxiOp  -- ^ The method
           -> String  -- ^ The serialized form
 buildCall lo =
-    let ja = [ (strOfKey Method, JSString $ toJSString $ strOfOp lo::JSValue)
-             , (strOfKey Args, opToArgs lo::JSValue)
-             ]
-        jo = toJSObject ja
-    in encodeStrict jo
+  let ja = [ (strOfKey Method, JSString $ toJSString $ strOfOp lo::JSValue)
+           , (strOfKey Args, opToArgs lo::JSValue)
+           ]
+      jo = toJSObject ja
+  in encodeStrict jo
+
+-- | Check that luxi request contains the required keys and parse it.
+validateCall :: String -> Result LuxiCall
+validateCall s = do
+  arr <- fromJResult "luxi call" $ decodeStrict s::Result (JSObject JSValue)
+  let aobj = fromJSObject arr
+  call <- fromObj aobj (strOfKey Method)::Result LuxiReq
+  args <- fromObj aobj (strOfKey Args)
+  return (LuxiCall call args)
+
+-- | Converts Luxi call arguments into a 'LuxiOp' data structure.
+--
+-- This is currently hand-coded until we make it more uniform so that
+-- it can be generated using TH.
+decodeCall :: LuxiCall -> Result LuxiOp
+decodeCall (LuxiCall call args) =
+  case call of
+    ReqQueryJobs -> do
+              (jid, jargs) <- fromJVal args
+              rid <- mapM (tryRead "parsing job ID" . fromJSString) jid
+              let rargs = map fromJSString jargs
+              return $ QueryJobs rid rargs
+    ReqQueryInstances -> do
+              (names, fields, locking) <- fromJVal args
+              return $ QueryInstances names fields locking
+    ReqQueryNodes -> do
+              (names, fields, locking) <- fromJVal args
+              return $ QueryNodes names fields locking
+    ReqQueryGroups -> do
+              (names, fields, locking) <- fromJVal args
+              return $ QueryGroups names fields locking
+    ReqQueryClusterInfo -> do
+              return QueryClusterInfo
+    ReqQuery -> do
+              (what, fields, _) <-
+                fromJVal args::Result (QrViaLuxi, [String], JSValue)
+              return $ Query what fields ()
+    ReqSubmitJob -> do
+              [ops1] <- fromJVal args
+              ops2 <- mapM (fromJResult (luxiReqToRaw call) . J.readJSON) ops1
+              return $ SubmitJob ops2
+    ReqSubmitManyJobs -> do
+              [ops1] <- fromJVal args
+              ops2 <- mapM (fromJResult (luxiReqToRaw call) . J.readJSON) ops1
+              return $ SubmitManyJobs ops2
+    ReqWaitForJobChange -> do
+              (jid, fields, pinfo, pidx, wtmout) <-
+                -- No instance for 5-tuple, code copied from the
+                -- json sources and adapted
+                fromJResult "Parsing WaitForJobChange message" $
+                case args of
+                  JSArray [a, b, c, d, e] ->
+                    (,,,,) `fmap`
+                    J.readJSON a `ap`
+                    J.readJSON b `ap`
+                    J.readJSON c `ap`
+                    J.readJSON d `ap`
+                    J.readJSON e
+                  _ -> J.Error "Not enough values"
+              rid <- tryRead "parsing job ID" jid
+              return $ WaitForJobChange rid fields pinfo pidx wtmout
+    ReqArchiveJob -> do
+              [jid] <- fromJVal args
+              rid <- tryRead "parsing job ID" jid
+              return $ ArchiveJob rid
+    ReqAutoArchiveJobs -> do
+              (age, tmout) <- fromJVal args
+              return $ AutoArchiveJobs age tmout
+    ReqQueryExports -> do
+              (nodes, lock) <- fromJVal args
+              return $ QueryExports nodes lock
+    ReqQueryConfigValues -> do
+              [fields] <- fromJVal args
+              return $ QueryConfigValues fields
+    ReqQueryTags -> do
+              (kind, name) <- fromJVal args
+              return $ QueryTags kind name
+    ReqCancelJob -> do
+              [job] <- fromJVal args
+              rid <- tryRead "parsing job ID" job
+              return $ CancelJob rid
+    ReqSetDrainFlag -> do
+              [flag] <- fromJVal args
+              return $ SetDrainFlag flag
+    ReqSetWatcherPause -> do
+              [duration] <- fromJVal args
+              return $ SetWatcherPause duration
 
 -- | Check that luxi responses contain the required keys and that the
 -- call was successful.
@@ -203,9 +341,9 @@ validateResult s = do
   let arr = J.fromJSObject oarr
   status <- fromObj arr (strOfKey Success)::Result Bool
   let rkey = strOfKey Result
-  (if status
-   then fromObj arr rkey
-   else fromObj arr rkey >>= fail)
+  if status
+    then fromObj arr rkey
+    else fromObj arr rkey >>= fail
 
 -- | Generic luxi method call.
 callMethod :: LuxiOp -> Client -> IO (Result JSValue)
diff --git a/htools/Ganeti/Objects.hs b/htools/Ganeti/Objects.hs
new file mode 100644 (file)
index 0000000..6aa0649
--- /dev/null
@@ -0,0 +1,371 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+{-| Implementation of the Ganeti config objects.
+
+Some object fields are not implemented yet, and as such they are
+commented out below.
+
+-}
+
+{-
+
+Copyright (C) 2011, 2012 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.Objects
+  ( NICMode(..)
+  , PartialNICParams(..)
+  , FilledNICParams(..)
+  , fillNICParams
+  , PartialNIC(..)
+  , DiskMode(..)
+  , DiskType(..)
+  , DiskLogicalId(..)
+  , Disk(..)
+  , DiskTemplate(..)
+  , PartialBEParams(..)
+  , FilledBEParams(..)
+  , fillBEParams
+  , Instance(..)
+  , toDictInstance
+  , PartialNDParams(..)
+  , FilledNDParams(..)
+  , fillNDParams
+  , Node(..)
+  , AllocPolicy(..)
+  , NodeGroup(..)
+  , Cluster(..)
+  , ConfigData(..)
+  ) where
+
+import Data.Maybe
+import Text.JSON (makeObj, showJSON, readJSON, JSON, JSValue(..))
+import qualified Text.JSON as J
+
+import qualified Ganeti.Constants as C
+import Ganeti.HTools.JSON
+
+import Ganeti.THH
+
+-- * NIC definitions
+
+$(declareSADT "NICMode"
+  [ ("NMBridged", 'C.nicModeBridged)
+  , ("NMRouted",  'C.nicModeRouted)
+  ])
+$(makeJSONInstance ''NICMode)
+
+$(buildParam "NIC" "nicp"
+  [ simpleField "mode" [t| NICMode |]
+  , simpleField "link" [t| String  |]
+  ])
+
+$(buildObject "PartialNIC" "nic"
+  [ simpleField "mac" [t| String |]
+  , optionalField $ simpleField "ip" [t| String |]
+  , simpleField "nicparams" [t| PartialNICParams |]
+  ])
+
+-- * Disk definitions
+
+$(declareSADT "DiskMode"
+  [ ("DiskRdOnly", 'C.diskRdonly)
+  , ("DiskRdWr",   'C.diskRdwr)
+  ])
+$(makeJSONInstance ''DiskMode)
+
+$(declareSADT "DiskType"
+  [ ("LD_LV",       'C.ldLv)
+  , ("LD_DRBD8",    'C.ldDrbd8)
+  , ("LD_FILE",     'C.ldFile)
+  , ("LD_BLOCKDEV", 'C.ldBlockdev)
+  , ("LD_RADOS",    'C.ldRbd)
+  ])
+$(makeJSONInstance ''DiskType)
+
+-- | The file driver type.
+$(declareSADT "FileDriver"
+  [ ("FileLoop",   'C.fdLoop)
+  , ("FileBlktap", 'C.fdBlktap)
+  ])
+$(makeJSONInstance ''FileDriver)
+
+-- | The persistent block driver type. Currently only one type is allowed.
+$(declareSADT "BlockDriver"
+  [ ("BlockDrvManual", 'C.blockdevDriverManual)
+  ])
+$(makeJSONInstance ''BlockDriver)
+
+-- | Constant for the dev_type key entry in the disk config.
+devType :: String
+devType = "dev_type"
+
+-- | The disk configuration type. This includes the disk type itself,
+-- for a more complete consistency. Note that since in the Python
+-- code-base there's no authoritative place where we document the
+-- logical id, this is probably a good reference point.
+data DiskLogicalId
+  = LIDPlain String String  -- ^ Volume group, logical volume
+  | LIDDrbd8 String String Int Int Int String
+  -- ^ NodeA, NodeB, Port, MinorA, MinorB, Secret
+  | LIDFile FileDriver String -- ^ Driver, path
+  | LIDBlockDev BlockDriver String -- ^ Driver, path (must be under /dev)
+  | LIDRados String String -- ^ Unused, path
+    deriving (Read, Show, Eq)
+
+-- | Mapping from a logical id to a disk type.
+lidDiskType :: DiskLogicalId -> DiskType
+lidDiskType (LIDPlain {}) = LD_LV
+lidDiskType (LIDDrbd8 {}) = LD_DRBD8
+lidDiskType (LIDFile  {}) = LD_FILE
+lidDiskType (LIDBlockDev {}) = LD_BLOCKDEV
+lidDiskType (LIDRados {}) = LD_RADOS
+
+-- | Builds the extra disk_type field for a given logical id.
+lidEncodeType :: DiskLogicalId -> [(String, JSValue)]
+lidEncodeType v = [(devType, showJSON . lidDiskType $ v)]
+
+-- | Custom encoder for DiskLogicalId (logical id only).
+encodeDLId :: DiskLogicalId -> JSValue
+encodeDLId (LIDPlain vg lv) = JSArray [showJSON vg, showJSON lv]
+encodeDLId (LIDDrbd8 nodeA nodeB port minorA minorB key) =
+  JSArray [ showJSON nodeA, showJSON nodeB, showJSON port
+          , showJSON minorA, showJSON minorB, showJSON key ]
+encodeDLId (LIDRados pool name) = JSArray [showJSON pool, showJSON name]
+encodeDLId (LIDFile driver name) = JSArray [showJSON driver, showJSON name]
+encodeDLId (LIDBlockDev driver name) = JSArray [showJSON driver, showJSON name]
+
+-- | Custom encoder for DiskLogicalId, composing both the logical id
+-- and the extra disk_type field.
+encodeFullDLId :: DiskLogicalId -> (JSValue, [(String, JSValue)])
+encodeFullDLId v = (encodeDLId v, lidEncodeType v)
+
+-- | Custom decoder for DiskLogicalId. This is manual for now, since
+-- we don't have yet automation for separate-key style fields.
+decodeDLId :: [(String, JSValue)] -> JSValue -> J.Result DiskLogicalId
+decodeDLId obj lid = do
+  dtype <- fromObj obj devType
+  case dtype of
+    LD_DRBD8 ->
+      case lid of
+        JSArray [nA, nB, p, mA, mB, k] -> do
+          nA' <- readJSON nA
+          nB' <- readJSON nB
+          p'  <- readJSON p
+          mA' <- readJSON mA
+          mB' <- readJSON mB
+          k'  <- readJSON k
+          return $ LIDDrbd8 nA' nB' p' mA' mB' k'
+        _ -> fail $ "Can't read logical_id for DRBD8 type"
+    LD_LV ->
+      case lid of
+        JSArray [vg, lv] -> do
+          vg' <- readJSON vg
+          lv' <- readJSON lv
+          return $ LIDPlain vg' lv'
+        _ -> fail $ "Can't read logical_id for plain type"
+    LD_FILE ->
+      case lid of
+        JSArray [driver, path] -> do
+          driver' <- readJSON driver
+          path'   <- readJSON path
+          return $ LIDFile driver' path'
+        _ -> fail $ "Can't read logical_id for file type"
+    LD_BLOCKDEV ->
+      case lid of
+        JSArray [driver, path] -> do
+          driver' <- readJSON driver
+          path'   <- readJSON path
+          return $ LIDBlockDev driver' path'
+        _ -> fail $ "Can't read logical_id for blockdev type"
+    LD_RADOS ->
+      case lid of
+        JSArray [driver, path] -> do
+          driver' <- readJSON driver
+          path'   <- readJSON path
+          return $ LIDRados driver' path'
+        _ -> fail $ "Can't read logical_id for rdb type"
+
+-- | Disk data structure.
+--
+-- This is declared manually as it's a recursive structure, and our TH
+-- code currently can't build it.
+data Disk = Disk
+  { diskLogicalId  :: DiskLogicalId
+--  , diskPhysicalId :: String
+  , diskChildren   :: [Disk]
+  , diskIvName     :: String
+  , diskSize       :: Int
+  , diskMode       :: DiskMode
+  } deriving (Read, Show, Eq)
+
+$(buildObjectSerialisation "Disk"
+  [ customField 'decodeDLId 'encodeFullDLId $
+      simpleField "logical_id"    [t| DiskLogicalId   |]
+--  , simpleField "physical_id" [t| String   |]
+  , defaultField  [| [] |] $ simpleField "children" [t| [Disk] |]
+  , defaultField [| "" |] $ simpleField "iv_name" [t| String |]
+  , simpleField "size" [t| Int |]
+  , defaultField [| DiskRdWr |] $ simpleField "mode" [t| DiskMode |]
+  ])
+
+-- * Instance definitions
+
+-- | Instance disk template type. **Copied from HTools/Types.hs**
+$(declareSADT "DiskTemplate"
+  [ ("DTDiskless",   'C.dtDiskless)
+  , ("DTFile",       'C.dtFile)
+  , ("DTSharedFile", 'C.dtSharedFile)
+  , ("DTPlain",      'C.dtPlain)
+  , ("DTBlock",      'C.dtBlock)
+  , ("DTDrbd8",      'C.dtDrbd8)
+  ])
+$(makeJSONInstance ''DiskTemplate)
+
+$(declareSADT "AdminState"
+  [ ("AdminOffline", 'C.adminstOffline)
+  , ("AdminDown",    'C.adminstDown)
+  , ("AdminUp",      'C.adminstUp)
+  ])
+$(makeJSONInstance ''AdminState)
+
+$(buildParam "BE" "bep" $
+  [ simpleField "minmem"       [t| Int  |]
+  , simpleField "maxmem"       [t| Int  |]
+  , simpleField "vcpus"        [t| Int  |]
+  , simpleField "auto_balance" [t| Bool |]
+  ])
+
+$(buildObject "Instance" "inst" $
+  [ simpleField "name"           [t| String             |]
+  , simpleField "primary_node"   [t| String             |]
+  , simpleField "os"             [t| String             |]
+  , simpleField "hypervisor"     [t| String             |]
+--  , simpleField "hvparams"     [t| [(String, String)] |]
+  , simpleField "beparams"       [t| PartialBEParams |]
+--  , simpleField "osparams"     [t| [(String, String)] |]
+  , simpleField "admin_state"    [t| AdminState         |]
+  , simpleField "nics"           [t| [PartialNIC]              |]
+  , simpleField "disks"          [t| [Disk]             |]
+  , simpleField "disk_template"  [t| DiskTemplate       |]
+  , optionalField $ simpleField "network_port" [t| Int |]
+  ]
+  ++ timeStampFields
+  ++ uuidFields
+  ++ serialFields)
+
+-- * Node definitions
+
+$(buildParam "ND" "ndp" $
+  [ simpleField "oob_program" [t| String |]
+  ])
+
+$(buildObject "Node" "node" $
+  [ simpleField "name"             [t| String |]
+  , simpleField "primary_ip"       [t| String |]
+  , simpleField "secondary_ip"     [t| String |]
+  , simpleField "master_candidate" [t| Bool   |]
+  , simpleField "offline"          [t| Bool   |]
+  , simpleField "drained"          [t| Bool   |]
+  , simpleField "group"            [t| String |]
+  , simpleField "master_capable"   [t| Bool   |]
+  , simpleField "vm_capable"       [t| Bool   |]
+--  , simpleField "ndparams"       [t| PartialNDParams |]
+  , simpleField "powered"          [t| Bool   |]
+  ]
+  ++ timeStampFields
+  ++ uuidFields
+  ++ serialFields)
+
+-- * NodeGroup definitions
+
+-- | The Group allocation policy type.
+--
+-- Note that the order of constructors is important as the automatic
+-- Ord instance will order them in the order they are defined, so when
+-- changing this data type be careful about the interaction with the
+-- desired sorting order.
+--
+-- FIXME: COPIED from Types.hs; we need to eliminate this duplication later
+$(declareSADT "AllocPolicy"
+  [ ("AllocPreferred",   'C.allocPolicyPreferred)
+  , ("AllocLastResort",  'C.allocPolicyLastResort)
+  , ("AllocUnallocable", 'C.allocPolicyUnallocable)
+  ])
+$(makeJSONInstance ''AllocPolicy)
+
+$(buildObject "NodeGroup" "group" $
+  [ simpleField "name"         [t| String |]
+  , defaultField  [| [] |] $ simpleField "members" [t| [String] |]
+--  , simpleField "ndparams"   [t| PartialNDParams |]
+  , simpleField "alloc_policy" [t| AllocPolicy |]
+  ]
+  ++ timeStampFields
+  ++ uuidFields
+  ++ serialFields)
+
+-- * Cluster definitions
+$(buildObject "Cluster" "cluster" $
+  [ simpleField "rsahostkeypub"             [t| String   |]
+  , simpleField "highest_used_port"         [t| Int      |]
+  , simpleField "tcpudp_port_pool"          [t| [Int]    |]
+  , simpleField "mac_prefix"                [t| String   |]
+  , simpleField "volume_group_name"         [t| String   |]
+  , simpleField "reserved_lvs"              [t| [String] |]
+--  , simpleField "drbd_usermode_helper"      [t| String   |]
+-- , simpleField "default_bridge"          [t| String   |]
+-- , simpleField "default_hypervisor"      [t| String   |]
+  , simpleField "master_node"               [t| String   |]
+  , simpleField "master_ip"                 [t| String   |]
+  , simpleField "master_netdev"             [t| String   |]
+-- , simpleField "master_netmask"          [t| String   |]
+  , simpleField "cluster_name"              [t| String   |]
+  , simpleField "file_storage_dir"          [t| String   |]
+-- , simpleField "shared_file_storage_dir" [t| String   |]
+  , simpleField "enabled_hypervisors"       [t| [String] |]
+-- , simpleField "hvparams"                [t| [(String, [(String, String)])] |]
+-- , simpleField "os_hvp"                  [t| [(String, String)] |]
+  , containerField $ simpleField "beparams" [t| FilledBEParams |]
+-- , simpleField "osparams"                [t| [(String, String)] |]
+  , containerField $ simpleField "nicparams" [t| FilledNICParams    |]
+--  , simpleField "ndparams"                  [t| FilledNDParams |]
+  , simpleField "candidate_pool_size"       [t| Int                |]
+  , simpleField "modify_etc_hosts"          [t| Bool               |]
+  , simpleField "modify_ssh_setup"          [t| Bool               |]
+  , simpleField "maintain_node_health"      [t| Bool               |]
+  , simpleField "uid_pool"                  [t| [Int]              |]
+  , simpleField "default_iallocator"        [t| String             |]
+  , simpleField "hidden_os"                 [t| [String]           |]
+  , simpleField "blacklisted_os"            [t| [String]           |]
+  , simpleField "primary_ip_family"         [t| Int                |]
+  , simpleField "prealloc_wipe_disks"       [t| Bool               |]
+ ]
+ ++ serialFields)
+
+-- * ConfigData definitions
+
+$(buildObject "ConfigData" "config" $
+--  timeStampFields ++
+  [ simpleField "version"       [t| Int                |]
+  , simpleField "cluster"       [t| Cluster            |]
+  , containerField $ simpleField "nodes"      [t| Node     |]
+  , containerField $ simpleField "nodegroups" [t| NodeGroup |]
+  , containerField $ simpleField "instances"  [t| Instance |]
+  ]
+  ++ serialFields)
index 0930598..3ecc645 100644 (file)
@@ -1,3 +1,5 @@
+{-# LANGUAGE TemplateHaskell #-}
+
 {-| Implementation of the opcodes.
 
 -}
@@ -24,133 +26,60 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 -}
 
 module Ganeti.OpCodes
-    ( OpCode(..)
-    , ReplaceDisksMode(..)
-    , opID
-    ) where
+  ( OpCode(..)
+  , ReplaceDisksMode(..)
+  , opID
+  ) where
 
-import Control.Monad
 import Text.JSON (readJSON, showJSON, makeObj, JSON)
-import qualified Text.JSON as J
-import Text.JSON.Types
 
-import Ganeti.HTools.Utils
+import qualified Ganeti.Constants as C
+import Ganeti.THH
+
+import Ganeti.HTools.JSON
 
 -- | Replace disks type.
-data ReplaceDisksMode = ReplaceOnPrimary
-                  | ReplaceOnSecondary
-                  | ReplaceNewSecondary
-                  | ReplaceAuto
-                  deriving (Show, Read, Eq)
-
-instance JSON ReplaceDisksMode where
-    showJSON m = case m of
-                 ReplaceOnPrimary -> showJSON "replace_on_primary"
-                 ReplaceOnSecondary -> showJSON "replace_on_secondary"
-                 ReplaceNewSecondary -> showJSON "replace_new_secondary"
-                 ReplaceAuto -> showJSON "replace_auto"
-    readJSON s = case readJSON s of
-                   J.Ok "replace_on_primary" -> J.Ok ReplaceOnPrimary
-                   J.Ok "replace_on_secondary" -> J.Ok ReplaceOnSecondary
-                   J.Ok "replace_new_secondary" -> J.Ok ReplaceNewSecondary
-                   J.Ok "replace_auto" -> J.Ok ReplaceAuto
-                   _ -> J.Error "Can't parse a valid ReplaceDisksMode"
+$(declareSADT "ReplaceDisksMode"
+  [ ("ReplaceOnPrimary",    'C.replaceDiskPri)
+  , ("ReplaceOnSecondary",  'C.replaceDiskSec)
+  , ("ReplaceNewSecondary", 'C.replaceDiskChg)
+  , ("ReplaceAuto",         'C.replaceDiskAuto)
+  ])
+$(makeJSONInstance ''ReplaceDisksMode)
 
 -- | OpCode representation.
 --
 -- We only implement a subset of Ganeti opcodes, but only what we
 -- actually use in the htools codebase.
-data OpCode = OpTestDelay Double Bool [String]
-            | OpInstanceReplaceDisks String (Maybe String) ReplaceDisksMode
-              [Int] (Maybe String)
-            | OpInstanceFailover String Bool (Maybe String)
-            | OpInstanceMigrate String Bool Bool Bool (Maybe String)
-            deriving (Show, Read, Eq)
-
-
--- | Computes the OP_ID for an OpCode.
-opID :: OpCode -> String
-opID (OpTestDelay _ _ _) = "OP_TEST_DELAY"
-opID (OpInstanceReplaceDisks _ _ _ _ _) = "OP_INSTANCE_REPLACE_DISKS"
-opID (OpInstanceFailover {}) = "OP_INSTANCE_FAILOVER"
-opID (OpInstanceMigrate  {}) = "OP_INSTANCE_MIGRATE"
-
--- | Loads an OpCode from the JSON serialised form.
-loadOpCode :: JSValue -> J.Result OpCode
-loadOpCode v = do
-  o <- liftM J.fromJSObject (readJSON v)
-  let extract x = fromObj o x
-  op_id <- extract "OP_ID"
-  case op_id of
-    "OP_TEST_DELAY" -> do
-                 on_nodes  <- extract "on_nodes"
-                 on_master <- extract "on_master"
-                 duration  <- extract "duration"
-                 return $ OpTestDelay duration on_master on_nodes
-    "OP_INSTANCE_REPLACE_DISKS" -> do
-                 inst   <- extract "instance_name"
-                 node   <- maybeFromObj o "remote_node"
-                 mode   <- extract "mode"
-                 disks  <- extract "disks"
-                 ialloc <- maybeFromObj o "iallocator"
-                 return $ OpInstanceReplaceDisks inst node mode disks ialloc
-    "OP_INSTANCE_FAILOVER" -> do
-                 inst    <- extract "instance_name"
-                 consist <- extract "ignore_consistency"
-                 tnode   <- maybeFromObj o "target_node"
-                 return $ OpInstanceFailover inst consist tnode
-    "OP_INSTANCE_MIGRATE" -> do
-                 inst    <- extract "instance_name"
-                 live    <- extract "live"
-                 cleanup <- extract "cleanup"
-                 allow_failover <- fromObjWithDefault o "allow_failover" False
-                 tnode   <- maybeFromObj o "target_node"
-                 return $ OpInstanceMigrate inst live cleanup
-                        allow_failover tnode
-    _ -> J.Error $ "Unknown opcode " ++ op_id
-
--- | Serialises an opcode to JSON.
-saveOpCode :: OpCode -> JSValue
-saveOpCode op@(OpTestDelay duration on_master on_nodes) =
-    let ol = [ ("OP_ID", showJSON $ opID op)
-             , ("duration", showJSON duration)
-             , ("on_master", showJSON on_master)
-             , ("on_nodes", showJSON on_nodes) ]
-    in makeObj ol
-
-saveOpCode op@(OpInstanceReplaceDisks inst node mode disks iallocator) =
-    let ol = [ ("OP_ID", showJSON $ opID op)
-             , ("instance_name", showJSON inst)
-             , ("mode", showJSON mode)
-             , ("disks", showJSON disks)]
-        ol2 = case node of
-                Just n -> ("remote_node", showJSON n):ol
-                Nothing -> ol
-        ol3 = case iallocator of
-                Just i -> ("iallocator", showJSON i):ol2
-                Nothing -> ol2
-    in makeObj ol3
-
-saveOpCode op@(OpInstanceFailover inst consist tnode) =
-    let ol = [ ("OP_ID", showJSON $ opID op)
-             , ("instance_name", showJSON inst)
-             , ("ignore_consistency", showJSON consist) ]
-        ol' = case tnode of
-                Nothing -> ol
-                Just node -> ("target_node", showJSON node):ol
-    in makeObj ol'
-
-saveOpCode op@(OpInstanceMigrate inst live cleanup allow_failover tnode) =
-    let ol = [ ("OP_ID", showJSON $ opID op)
-             , ("instance_name", showJSON inst)
-             , ("live", showJSON live)
-             , ("cleanup", showJSON cleanup)
-             , ("allow_failover", showJSON allow_failover) ]
-        ol' = case tnode of
-                Nothing -> ol
-                Just node -> ("target_node", showJSON node):ol
-    in makeObj ol'
+$(genOpCode "OpCode"
+  [ ("OpTestDelay",
+     [ simpleField "duration"  [t| Double   |]
+     , simpleField "on_master" [t| Bool     |]
+     , simpleField "on_nodes"  [t| [String] |]
+     ])
+  , ("OpInstanceReplaceDisks",
+     [ simpleField "instance_name" [t| String |]
+     , optionalField $ simpleField "remote_node" [t| String |]
+     , simpleField "mode"  [t| ReplaceDisksMode |]
+     , simpleField "disks" [t| [Int] |]
+     , optionalField $ simpleField "iallocator" [t| String |]
+     ])
+  , ("OpInstanceFailover",
+     [ simpleField "instance_name"      [t| String |]
+     , simpleField "ignore_consistency" [t| Bool   |]
+     , optionalField $ simpleField "target_node" [t| String |]
+     ])
+  , ("OpInstanceMigrate",
+     [ simpleField "instance_name"  [t| String |]
+     , simpleField "live"           [t| Bool   |]
+     , simpleField "cleanup"        [t| Bool   |]
+     , defaultField [| False |] $ simpleField "allow_failover" [t| Bool |]
+     , optionalField $ simpleField "target_node" [t| String |]
+     ])
+  ])
+
+$(genOpID ''OpCode "opID")
 
 instance JSON OpCode where
-    readJSON = loadOpCode
-    showJSON = saveOpCode
+  readJSON = loadOpCode
+  showJSON = saveOpCode
diff --git a/htools/Ganeti/Runtime.hs b/htools/Ganeti/Runtime.hs
new file mode 100644 (file)
index 0000000..b6371d3
--- /dev/null
@@ -0,0 +1,154 @@
+{-| Implementation of the runtime configuration details.
+
+-}
+
+{-
+
+Copyright (C) 2011, 2012 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.Runtime
+  ( GanetiDaemon(..)
+  , MiscGroup(..)
+  , GanetiGroup(..)
+  , RuntimeEnts
+  , daemonName
+  , daemonUser
+  , daemonGroup
+  , daemonLogFile
+  , daemonPidFile
+  , getEnts
+  , verifyDaemonUser
+  ) where
+
+import Control.Exception
+import Control.Monad
+import qualified Data.Map as M
+import System.Exit
+import System.FilePath
+import System.IO
+import System.IO.Error
+import System.Posix.Types
+import System.Posix.User
+import Text.Printf
+
+import qualified Ganeti.Constants as C
+import Ganeti.BasicTypes
+
+data GanetiDaemon = GanetiMasterd
+                  | GanetiNoded
+                  | GanetiRapi
+                  | GanetiConfd
+                    deriving (Show, Enum, Bounded, Eq, Ord)
+
+data MiscGroup = DaemonsGroup
+               | AdminGroup
+                 deriving (Show, Enum, Bounded, Eq, Ord)
+
+data GanetiGroup = DaemonGroup GanetiDaemon
+                 | ExtraGroup MiscGroup
+                   deriving (Show, Eq, Ord)
+
+type RuntimeEnts = (M.Map GanetiDaemon UserID, M.Map GanetiGroup GroupID)
+
+-- | Returns the daemon name for a given daemon.
+daemonName :: GanetiDaemon -> String
+daemonName GanetiMasterd = C.masterd
+daemonName GanetiNoded   = C.noded
+daemonName GanetiRapi    = C.rapi
+daemonName GanetiConfd   = C.confd
+
+-- | Returns the configured user name for a daemon.
+daemonUser :: GanetiDaemon -> String
+daemonUser GanetiMasterd = C.masterdUser
+daemonUser GanetiNoded   = C.nodedUser
+daemonUser GanetiRapi    = C.rapiUser
+daemonUser GanetiConfd   = C.confdUser
+
+-- | Returns the configured group for a daemon.
+daemonGroup :: GanetiGroup -> String
+daemonGroup (DaemonGroup GanetiMasterd) = C.masterdGroup
+daemonGroup (DaemonGroup GanetiNoded)   = C.nodedGroup
+daemonGroup (DaemonGroup GanetiRapi)    = C.rapiGroup
+daemonGroup (DaemonGroup GanetiConfd)   = C.confdGroup
+daemonGroup (ExtraGroup  DaemonsGroup)  = C.daemonsGroup
+daemonGroup (ExtraGroup  AdminGroup)    = C.adminGroup
+
+-- | Returns the log file for a daemon.
+daemonLogFile :: GanetiDaemon -> FilePath
+daemonLogFile GanetiConfd = C.daemonsLogfilesGanetiConfd
+daemonLogFile _           = error "Unimplemented"
+
+-- | Returns the pid file name for a daemon.
+daemonPidFile :: GanetiDaemon -> FilePath
+daemonPidFile daemon = C.runGanetiDir </> daemonName daemon <.> "pid"
+
+-- | All groups list. A bit hacking, as we can't enforce it's complete
+-- at compile time.
+allGroups :: [GanetiGroup]
+allGroups = map DaemonGroup [minBound..maxBound] ++
+            map ExtraGroup  [minBound..maxBound]
+
+ignoreDoesNotExistErrors :: IO a -> IO (Result a)
+ignoreDoesNotExistErrors value = do
+  result <- tryJust (\e -> if isDoesNotExistError e
+                             then Just (show e)
+                             else Nothing) value
+  return $ eitherToResult result
+
+-- | Computes the group/user maps.
+getEnts :: IO (Result RuntimeEnts)
+getEnts = do
+  users <- mapM (\daemon -> do
+                   entry <- ignoreDoesNotExistErrors .
+                            getUserEntryForName .
+                            daemonUser $ daemon
+                   return (entry >>= \e -> return (daemon, userID e))
+                ) [minBound..maxBound]
+  groups <- mapM (\group -> do
+                    entry <- ignoreDoesNotExistErrors .
+                             getGroupEntryForName .
+                             daemonGroup $ group
+                    return (entry >>= \e -> return (group, groupID e))
+                 ) allGroups
+  return $ do -- 'Result' monad
+    users'  <- sequence users
+    groups' <- sequence groups
+    let usermap = M.fromList users'
+        groupmap = M.fromList groups'
+    return (usermap, groupmap)
+
+
+-- | Checks whether a daemon runs as the right user.
+verifyDaemonUser :: GanetiDaemon -> RuntimeEnts -> IO ()
+verifyDaemonUser daemon ents = do
+  myuid <- getEffectiveUserID
+  -- note: we use directly ! as lookup failues shouldn't happen, due
+  -- to the above map construction
+  checkUidMatch (daemonName daemon) ((M.!) (fst ents) daemon) myuid
+
+-- | Check that two UIDs are matching or otherwise exit.
+checkUidMatch :: String -> UserID -> UserID -> IO ()
+checkUidMatch name expected actual =
+  when (expected /= actual) $ do
+    hPrintf stderr "%s started using wrong user ID (%d), \
+                   \expected %d\n" name
+              (fromIntegral actual::Int)
+              (fromIntegral expected::Int) :: IO ()
+    exitWith $ ExitFailure C.exitFailure
diff --git a/htools/Ganeti/Ssconf.hs b/htools/Ganeti/Ssconf.hs
new file mode 100644 (file)
index 0000000..39a3d95
--- /dev/null
@@ -0,0 +1,132 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+{-| Implementation of the Ganeti Ssconf interface.
+
+-}
+
+{-
+
+Copyright (C) 2012 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.Ssconf
+  ( SSKey(..)
+  , sSKeyToRaw
+  , sSKeyFromRaw
+  , getPrimaryIPFamily
+  , keyToFilename
+  , sSFilePrefix
+  ) where
+
+import Ganeti.THH
+
+import Control.Exception
+import Control.Monad (liftM)
+import Data.Char (isSpace)
+import Data.Maybe (fromMaybe)
+import Prelude hiding (catch)
+import qualified Network.Socket as Socket
+import System.FilePath ((</>))
+import System.IO.Error (isDoesNotExistError)
+
+import qualified Ganeti.Constants as C
+import Ganeti.BasicTypes
+import Ganeti.HTools.Utils
+
+-- | Maximum ssconf file size we support.
+maxFileSize :: Int
+maxFileSize = 131072
+
+-- | ssconf file prefix, re-exported from Constants.
+sSFilePrefix :: FilePath
+sSFilePrefix = C.ssconfFileprefix
+
+$(declareSADT "SSKey"
+  [ ("SSClusterName",          'C.ssClusterName)
+  , ("SSClusterTags",          'C.ssClusterTags)
+  , ("SSFileStorageDir",       'C.ssFileStorageDir)
+  , ("SSSharedFileStorageDir", 'C.ssSharedFileStorageDir)
+  , ("SSMasterCandidates",     'C.ssMasterCandidates)
+  , ("SSMasterCandidatesIps",  'C.ssMasterCandidatesIps)
+  , ("SSMasterIp",             'C.ssMasterIp)
+  , ("SSMasterNetdev",         'C.ssMasterNetdev)
+  , ("SSMasterNetmask",        'C.ssMasterNetmask)
+  , ("SSMasterNode",           'C.ssMasterNode)
+  , ("SSNodeList",             'C.ssNodeList)
+  , ("SSNodePrimaryIps",       'C.ssNodePrimaryIps)
+  , ("SSNodeSecondaryIps",     'C.ssNodeSecondaryIps)
+  , ("SSOfflineNodes",         'C.ssOfflineNodes)
+  , ("SSOnlineNodes",          'C.ssOnlineNodes)
+  , ("SSPrimaryIpFamily",      'C.ssPrimaryIpFamily)
+  , ("SSInstanceList",         'C.ssInstanceList)
+  , ("SSReleaseVersion",       'C.ssReleaseVersion)
+  , ("SSHypervisorList",       'C.ssHypervisorList)
+  , ("SSMaintainNodeHealth",   'C.ssMaintainNodeHealth)
+  , ("SSUidPool",              'C.ssUidPool)
+  , ("SSNodegroups",           'C.ssNodegroups)
+  ])
+
+-- | Convert a ssconf key into a (full) file path.
+keyToFilename :: Maybe FilePath     -- ^ Optional config path override
+              -> SSKey              -- ^ ssconf key
+              -> FilePath
+keyToFilename optpath key = fromMaybe C.dataDir optpath </>
+                            sSFilePrefix ++ sSKeyToRaw key
+
+-- | Runs an IO action while transforming any error into 'Bad'
+-- values. It also accepts an optional value to use in case the error
+-- is just does not exist.
+catchIOErrors :: Maybe a         -- ^ Optional default
+              -> IO a            -- ^ Action to run
+              -> IO (Result a)
+catchIOErrors def action =
+  catch (do
+          result <- action
+          return (Ok result)
+        ) (\err -> let bad_result = Bad (show err)
+                   in return $ if isDoesNotExistError err
+                                 then maybe bad_result Ok def
+                                 else bad_result)
+
+-- | Read an ssconf file.
+readSSConfFile :: Maybe FilePath            -- ^ Optional config path override
+               -> Maybe String              -- ^ Optional default value
+               -> SSKey                     -- ^ Desired ssconf key
+               -> IO (Result String)
+readSSConfFile optpath def key = do
+  result <- catchIOErrors def . readFile . keyToFilename optpath $ key
+  return (liftM (take maxFileSize) result)
+
+-- | Strip space characthers (including newline). As this is
+-- expensive, should only be run on small strings.
+rstripSpace :: String -> String
+rstripSpace = reverse . dropWhile isSpace . reverse
+
+-- | Parses a string containing an IP family
+parseIPFamily :: Int -> Result Socket.Family
+parseIPFamily fam | fam == C.ip4Family = Ok Socket.AF_INET
+                  | fam == C.ip6Family = Ok Socket.AF_INET6
+                  | otherwise = Bad $ "Unknown af_family value: " ++ show fam
+
+-- | Read the primary IP family.
+getPrimaryIPFamily :: Maybe FilePath -> IO (Result Socket.Family)
+getPrimaryIPFamily optpath = do
+  result <- readSSConfFile optpath (Just (show C.ip4Family)) SSPrimaryIpFamily
+  return (result >>= return . rstripSpace >>=
+          tryRead "Parsing af_family" >>= parseIPFamily)
diff --git a/htools/Ganeti/THH.hs b/htools/Ganeti/THH.hs
new file mode 100644 (file)
index 0000000..929da6f
--- /dev/null
@@ -0,0 +1,805 @@
+{-# LANGUAGE TemplateHaskell #-}
+
+{-| TemplateHaskell helper for HTools.
+
+As TemplateHaskell require that splices be defined in a separate
+module, we combine all the TemplateHaskell functionality that HTools
+needs in this module (except the one for unittests).
+
+-}
+
+{-
+
+Copyright (C) 2011, 2012 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.THH ( declareSADT
+                  , declareIADT
+                  , makeJSONInstance
+                  , genOpID
+                  , genOpCode
+                  , genStrOfOp
+                  , genStrOfKey
+                  , genLuxiOp
+                  , Field
+                  , simpleField
+                  , defaultField
+                  , optionalField
+                  , renameField
+                  , containerField
+                  , customField
+                  , timeStampFields
+                  , uuidFields
+                  , serialFields
+                  , buildObject
+                  , buildObjectSerialisation
+                  , buildParam
+                  , Container
+                  ) where
+
+import Control.Arrow
+import Control.Monad (liftM, liftM2)
+import Data.Char
+import Data.List
+import qualified Data.Map as M
+import Language.Haskell.TH
+
+import qualified Text.JSON as JSON
+
+import Ganeti.HTools.JSON
+
+-- * Exported types
+
+type Container = M.Map String
+
+-- | Serialised field data type.
+data Field = Field { fieldName        :: String
+                   , fieldType        :: Q Type
+                   , fieldRead        :: Maybe (Q Exp)
+                   , fieldShow        :: Maybe (Q Exp)
+                   , fieldDefault     :: Maybe (Q Exp)
+                   , fieldConstr      :: Maybe String
+                   , fieldIsContainer :: Bool
+                   , fieldIsOptional  :: Bool
+                   }
+
+-- | Generates a simple field.
+simpleField :: String -> Q Type -> Field
+simpleField fname ftype =
+  Field { fieldName        = fname
+        , fieldType        = ftype
+        , fieldRead        = Nothing
+        , fieldShow        = Nothing
+        , fieldDefault     = Nothing
+        , fieldConstr      = Nothing
+        , fieldIsContainer = False
+        , fieldIsOptional  = False
+        }
+
+-- | Sets the renamed constructor field.
+renameField :: String -> Field -> Field
+renameField constrName field = field { fieldConstr = Just constrName }
+
+-- | Sets the default value on a field (makes it optional with a
+-- default value).
+defaultField :: Q Exp -> Field -> Field
+defaultField defval field = field { fieldDefault = Just defval }
+
+-- | Marks a field optional (turning its base type into a Maybe).
+optionalField :: Field -> Field
+optionalField field = field { fieldIsOptional = True }
+
+-- | Marks a field as a container.
+containerField :: Field -> Field
+containerField field = field { fieldIsContainer = True }
+
+-- | Sets custom functions on a field.
+customField :: Name    -- ^ The name of the read function
+            -> Name    -- ^ The name of the show function
+            -> Field   -- ^ The original field
+            -> Field   -- ^ Updated field
+customField readfn showfn field =
+  field { fieldRead = Just (varE readfn), fieldShow = Just (varE showfn) }
+
+fieldRecordName :: Field -> String
+fieldRecordName (Field { fieldName = name, fieldConstr = alias }) =
+  maybe (camelCase name) id alias
+
+-- | Computes the preferred variable name to use for the value of this
+-- field. If the field has a specific constructor name, then we use a
+-- first-letter-lowercased version of that; otherwise, we simply use
+-- the field name. See also 'fieldRecordName'.
+fieldVariable :: Field -> String
+fieldVariable f =
+  case (fieldConstr f) of
+    Just name -> ensureLower name
+    _ -> fieldName f
+
+actualFieldType :: Field -> Q Type
+actualFieldType f | fieldIsContainer f = [t| Container $t |]
+                  | fieldIsOptional f  = [t| Maybe $t     |]
+                  | otherwise = t
+                  where t = fieldType f
+
+checkNonOptDef :: (Monad m) => Field -> m ()
+checkNonOptDef (Field { fieldIsOptional = True, fieldName = name }) =
+  fail $ "Optional field " ++ name ++ " used in parameter declaration"
+checkNonOptDef (Field { fieldDefault = (Just _), fieldName = name }) =
+  fail $ "Default field " ++ name ++ " used in parameter declaration"
+checkNonOptDef _ = return ()
+
+-- | Produces the expression that will de-serialise a given
+-- field. Since some custom parsing functions might need to use the
+-- entire object, we do take and pass the object to any custom read
+-- functions.
+loadFn :: Field   -- ^ The field definition
+       -> Q Exp   -- ^ The value of the field as existing in the JSON message
+       -> Q Exp   -- ^ The entire object in JSON object format
+       -> Q Exp   -- ^ Resulting expression
+loadFn (Field { fieldIsContainer = True }) expr _ =
+  [| $expr >>= readContainer |]
+loadFn (Field { fieldRead = Just readfn }) expr o = [| $expr >>= $readfn $o |]
+loadFn _ expr _ = expr
+
+-- * Common field declarations
+
+timeStampFields :: [Field]
+timeStampFields =
+    [ defaultField [| 0::Double |] $ simpleField "ctime" [t| Double |]
+    , defaultField [| 0::Double |] $ simpleField "mtime" [t| Double |]
+    ]
+
+serialFields :: [Field]
+serialFields =
+    [ renameField  "Serial" $ simpleField "serial_no" [t| Int |] ]
+
+uuidFields :: [Field]
+uuidFields = [ simpleField "uuid" [t| String |] ]
+
+-- * Helper functions
+
+-- | Ensure first letter is lowercase.
+--
+-- Used to convert type name to function prefix, e.g. in @data Aa ->
+-- aaToRaw@.
+ensureLower :: String -> String
+ensureLower [] = []
+ensureLower (x:xs) = toLower x:xs
+
+-- | Ensure first letter is uppercase.
+--
+-- Used to convert constructor name to component
+ensureUpper :: String -> String
+ensureUpper [] = []
+ensureUpper (x:xs) = toUpper x:xs
+
+-- | Helper for quoted expressions.
+varNameE :: String -> Q Exp
+varNameE = varE . mkName
+
+-- | showJSON as an expression, for reuse.
+showJSONE :: Q Exp
+showJSONE = varNameE "showJSON"
+
+-- | ToRaw function name.
+toRawName :: String -> Name
+toRawName = mkName . (++ "ToRaw") . ensureLower
+
+-- | FromRaw function name.
+fromRawName :: String -> Name
+fromRawName = mkName . (++ "FromRaw") . ensureLower
+
+-- | Converts a name to it's varE/litE representations.
+--
+reprE :: Either String Name -> Q Exp
+reprE = either stringE varE
+
+-- | Smarter function application.
+--
+-- This does simply f x, except that if is 'id', it will skip it, in
+-- order to generate more readable code when using -ddump-splices.
+appFn :: Exp -> Exp -> Exp
+appFn f x | f == VarE 'id = x
+          | otherwise = AppE f x
+
+-- | Container loader
+readContainer :: (Monad m, JSON.JSON a) =>
+                 JSON.JSObject JSON.JSValue -> m (Container a)
+readContainer obj = do
+  let kjvlist = JSON.fromJSObject obj
+  kalist <- mapM (\(k, v) -> fromKeyValue k v >>= \a -> return (k, a)) kjvlist
+  return $ M.fromList kalist
+
+-- | Container dumper
+showContainer :: (JSON.JSON a) => Container a -> JSON.JSValue
+showContainer = JSON.makeObj . map (second JSON.showJSON) . M.toList
+
+-- * Template code for simple raw type-equivalent ADTs
+
+-- | Generates a data type declaration.
+--
+-- The type will have a fixed list of instances.
+strADTDecl :: Name -> [String] -> Dec
+strADTDecl name constructors =
+  DataD [] name []
+          (map (flip NormalC [] . mkName) constructors)
+          [''Show, ''Read, ''Eq, ''Enum, ''Bounded, ''Ord]
+
+-- | Generates a toRaw function.
+--
+-- This generates a simple function of the form:
+--
+-- @
+-- nameToRaw :: Name -> /traw/
+-- nameToRaw Cons1 = var1
+-- nameToRaw Cons2 = \"value2\"
+-- @
+genToRaw :: Name -> Name -> Name -> [(String, Either String Name)] -> Q [Dec]
+genToRaw traw fname tname constructors = do
+  let sigt = AppT (AppT ArrowT (ConT tname)) (ConT traw)
+  -- the body clauses, matching on the constructor and returning the
+  -- raw value
+  clauses <- mapM  (\(c, v) -> clause [recP (mkName c) []]
+                             (normalB (reprE v)) []) constructors
+  return [SigD fname sigt, FunD fname clauses]
+
+-- | Generates a fromRaw function.
+--
+-- The function generated is monadic and can fail parsing the
+-- raw value. It is of the form:
+--
+-- @
+-- nameFromRaw :: (Monad m) => /traw/ -> m Name
+-- nameFromRaw s | s == var1       = Cons1
+--               | s == \"value2\" = Cons2
+--               | otherwise = fail /.../
+-- @
+genFromRaw :: Name -> Name -> Name -> [(String, Name)] -> Q [Dec]
+genFromRaw traw fname tname constructors = do
+  -- signature of form (Monad m) => String -> m $name
+  sigt <- [t| (Monad m) => $(conT traw) -> m $(conT tname) |]
+  -- clauses for a guarded pattern
+  let varp = mkName "s"
+      varpe = varE varp
+  clauses <- mapM (\(c, v) -> do
+                     -- the clause match condition
+                     g <- normalG [| $varpe == $(varE v) |]
+                     -- the clause result
+                     r <- [| return $(conE (mkName c)) |]
+                     return (g, r)) constructors
+  -- the otherwise clause (fallback)
+  oth_clause <- do
+    g <- normalG [| otherwise |]
+    r <- [|fail ("Invalid string value for type " ++
+                 $(litE (stringL (nameBase tname))) ++ ": " ++ show $varpe) |]
+    return (g, r)
+  let fun = FunD fname [Clause [VarP varp]
+                        (GuardedB (clauses++[oth_clause])) []]
+  return [SigD fname sigt, fun]
+
+-- | Generates a data type from a given raw format.
+--
+-- The format is expected to multiline. The first line contains the
+-- type name, and the rest of the lines must contain two words: the
+-- constructor name and then the string representation of the
+-- respective constructor.
+--
+-- The function will generate the data type declaration, and then two
+-- functions:
+--
+-- * /name/ToRaw, which converts the type to a raw type
+--
+-- * /name/FromRaw, which (monadically) converts from a raw type to the type
+--
+-- Note that this is basically just a custom show/read instance,
+-- nothing else.
+declareADT :: Name -> String -> [(String, Name)] -> Q [Dec]
+declareADT traw sname cons = do
+  let name = mkName sname
+      ddecl = strADTDecl name (map fst cons)
+      -- process cons in the format expected by genToRaw
+      cons' = map (\(a, b) -> (a, Right b)) cons
+  toraw <- genToRaw traw (toRawName sname) name cons'
+  fromraw <- genFromRaw traw (fromRawName sname) name cons
+  return $ ddecl:toraw ++ fromraw
+
+declareIADT :: String -> [(String, Name)] -> Q [Dec]
+declareIADT = declareADT ''Int
+
+declareSADT :: String -> [(String, Name)] -> Q [Dec]
+declareSADT = declareADT ''String
+
+-- | Creates the showJSON member of a JSON instance declaration.
+--
+-- This will create what is the equivalent of:
+--
+-- @
+-- showJSON = showJSON . /name/ToRaw
+-- @
+--
+-- in an instance JSON /name/ declaration
+genShowJSON :: String -> Q Dec
+genShowJSON name = do
+  body <- [| JSON.showJSON . $(varE (toRawName name)) |]
+  return $ FunD (mkName "showJSON") [Clause [] (NormalB body) []]
+
+-- | Creates the readJSON member of a JSON instance declaration.
+--
+-- This will create what is the equivalent of:
+--
+-- @
+-- readJSON s = case readJSON s of
+--                Ok s' -> /name/FromRaw s'
+--                Error e -> Error /description/
+-- @
+--
+-- in an instance JSON /name/ declaration
+genReadJSON :: String -> Q Dec
+genReadJSON name = do
+  let s = mkName "s"
+  body <- [| case JSON.readJSON $(varE s) of
+               JSON.Ok s' -> $(varE (fromRawName name)) s'
+               JSON.Error e ->
+                   JSON.Error $ "Can't parse raw value for type " ++
+                           $(stringE name) ++ ": " ++ e ++ " from " ++
+                           show $(varE s)
+           |]
+  return $ FunD (mkName "readJSON") [Clause [VarP s] (NormalB body) []]
+
+-- | Generates a JSON instance for a given type.
+--
+-- This assumes that the /name/ToRaw and /name/FromRaw functions
+-- have been defined as by the 'declareSADT' function.
+makeJSONInstance :: Name -> Q [Dec]
+makeJSONInstance name = do
+  let base = nameBase name
+  showJ <- genShowJSON base
+  readJ <- genReadJSON base
+  return [InstanceD [] (AppT (ConT ''JSON.JSON) (ConT name)) [readJ,showJ]]
+
+-- * Template code for opcodes
+
+-- | Transforms a CamelCase string into an_underscore_based_one.
+deCamelCase :: String -> String
+deCamelCase =
+    intercalate "_" . map (map toUpper) . groupBy (\_ b -> not $ isUpper b)
+
+-- | Transform an underscore_name into a CamelCase one.
+camelCase :: String -> String
+camelCase = concatMap (ensureUpper . drop 1) .
+            groupBy (\_ b -> b /= '_') . ('_':)
+
+-- | Computes the name of a given constructor.
+constructorName :: Con -> Q Name
+constructorName (NormalC name _) = return name
+constructorName (RecC name _)    = return name
+constructorName x                = fail $ "Unhandled constructor " ++ show x
+
+-- | Builds the generic constructor-to-string function.
+--
+-- This generates a simple function of the following form:
+--
+-- @
+-- fname (ConStructorOne {}) = trans_fun("ConStructorOne")
+-- fname (ConStructorTwo {}) = trans_fun("ConStructorTwo")
+-- @
+--
+-- This builds a custom list of name/string pairs and then uses
+-- 'genToRaw' to actually generate the function
+genConstrToStr :: (String -> String) -> Name -> String -> Q [Dec]
+genConstrToStr trans_fun name fname = do
+  TyConI (DataD _ _ _ cons _) <- reify name
+  cnames <- mapM (liftM nameBase . constructorName) cons
+  let svalues = map (Left . trans_fun) cnames
+  genToRaw ''String (mkName fname) name $ zip cnames svalues
+
+-- | Constructor-to-string for OpCode.
+genOpID :: Name -> String -> Q [Dec]
+genOpID = genConstrToStr deCamelCase
+
+-- | OpCode parameter (field) type.
+type OpParam = (String, Q Type, Q Exp)
+
+-- | Generates the OpCode data type.
+--
+-- This takes an opcode logical definition, and builds both the
+-- datatype and the JSON serialisation out of it. We can't use a
+-- generic serialisation since we need to be compatible with Ganeti's
+-- own, so we have a few quirks to work around.
+genOpCode :: String                -- ^ Type name to use
+          -> [(String, [Field])]   -- ^ Constructor name and parameters
+          -> Q [Dec]
+genOpCode name cons = do
+  decl_d <- mapM (\(cname, fields) -> do
+                    -- we only need the type of the field, without Q
+                    fields' <- mapM actualFieldType fields
+                    let fields'' = zip (repeat NotStrict) fields'
+                    return $ NormalC (mkName cname) fields'')
+            cons
+  let declD = DataD [] (mkName name) [] decl_d [''Show, ''Read, ''Eq]
+
+  (savesig, savefn) <- genSaveOpCode cons
+  (loadsig, loadfn) <- genLoadOpCode cons
+  return [declD, loadsig, loadfn, savesig, savefn]
+
+-- | Checks whether a given parameter is options.
+--
+-- This requires that it's a 'Maybe'.
+isOptional :: Type -> Bool
+isOptional (AppT (ConT dt) _) | dt == ''Maybe = True
+isOptional _ = False
+
+-- | Generates the \"save\" clause for an entire opcode constructor.
+--
+-- This matches the opcode with variables named the same as the
+-- constructor fields (just so that the spliced in code looks nicer),
+-- and passes those name plus the parameter definition to 'saveObjectField'.
+saveConstructor :: String    -- ^ The constructor name
+                -> [Field]   -- ^ The parameter definitions for this
+                             -- constructor
+                -> Q Clause  -- ^ Resulting clause
+saveConstructor sname fields = do
+  let cname = mkName sname
+  let fnames = map (mkName . fieldVariable) fields
+  let pat = conP cname (map varP fnames)
+  let felems = map (uncurry saveObjectField) (zip fnames fields)
+      -- now build the OP_ID serialisation
+      opid = [| [( $(stringE "OP_ID"),
+                   JSON.showJSON $(stringE . deCamelCase $ sname) )] |]
+      flist = listE (opid:felems)
+      -- and finally convert all this to a json object
+      flist' = [| $(varNameE "makeObj") (concat $flist) |]
+  clause [pat] (normalB flist') []
+
+-- | Generates the main save opcode function.
+--
+-- This builds a per-constructor match clause that contains the
+-- respective constructor-serialisation code.
+genSaveOpCode :: [(String, [Field])] -> Q (Dec, Dec)
+genSaveOpCode opdefs = do
+  cclauses <- mapM (uncurry saveConstructor) opdefs
+  let fname = mkName "saveOpCode"
+  sigt <- [t| $(conT (mkName "OpCode")) -> JSON.JSValue |]
+  return $ (SigD fname sigt, FunD fname cclauses)
+
+loadConstructor :: String -> [Field] -> Q Exp
+loadConstructor sname fields = do
+  let name = mkName sname
+  fbinds <- mapM loadObjectField fields
+  let (fnames, fstmts) = unzip fbinds
+  let cval = foldl (\accu fn -> AppE accu (VarE fn)) (ConE name) fnames
+      fstmts' = fstmts ++ [NoBindS (AppE (VarE 'return) cval)]
+  return $ DoE fstmts'
+
+genLoadOpCode :: [(String, [Field])] -> Q (Dec, Dec)
+genLoadOpCode opdefs = do
+  let fname = mkName "loadOpCode"
+      arg1 = mkName "v"
+      objname = mkName "o"
+      opid = mkName "op_id"
+  st1 <- bindS (varP objname) [| liftM JSON.fromJSObject
+                                 (JSON.readJSON $(varE arg1)) |]
+  st2 <- bindS (varP opid) [| $(varNameE "fromObj")
+                              $(varE objname) $(stringE "OP_ID") |]
+  -- the match results (per-constructor blocks)
+  mexps <- mapM (uncurry loadConstructor) opdefs
+  fails <- [| fail $ "Unknown opcode " ++ $(varE opid) |]
+  let mpats = map (\(me, c) ->
+                       let mp = LitP . StringL . deCamelCase . fst $ c
+                       in Match mp (NormalB me) []
+                  ) $ zip mexps opdefs
+      defmatch = Match WildP (NormalB fails) []
+      cst = NoBindS $ CaseE (VarE opid) $ mpats++[defmatch]
+      body = DoE [st1, st2, cst]
+  sigt <- [t| JSON.JSValue -> JSON.Result $(conT (mkName "OpCode")) |]
+  return $ (SigD fname sigt, FunD fname [Clause [VarP arg1] (NormalB body) []])
+
+-- * Template code for luxi
+
+-- | Constructor-to-string for LuxiOp.
+genStrOfOp :: Name -> String -> Q [Dec]
+genStrOfOp = genConstrToStr id
+
+-- | Constructor-to-string for MsgKeys.
+genStrOfKey :: Name -> String -> Q [Dec]
+genStrOfKey = genConstrToStr ensureLower
+
+-- | LuxiOp parameter type.
+type LuxiParam = (String, Q Type, Q Exp)
+
+-- | Generates the LuxiOp data type.
+--
+-- This takes a Luxi operation definition and builds both the
+-- datatype and the function trnasforming the arguments to JSON.
+-- We can't use anything less generic, because the way different
+-- operations are serialized differs on both parameter- and top-level.
+--
+-- There are three things to be defined for each parameter:
+--
+-- * name
+--
+-- * type
+--
+-- * operation; this is the operation performed on the parameter before
+--   serialization
+--
+genLuxiOp :: String -> [(String, [LuxiParam])] -> Q [Dec]
+genLuxiOp name cons = do
+  decl_d <- mapM (\(cname, fields) -> do
+                    fields' <- mapM (\(_, qt, _) ->
+                                         qt >>= \t -> return (NotStrict, t))
+                               fields
+                    return $ NormalC (mkName cname) fields')
+            cons
+  let declD = DataD [] (mkName name) [] decl_d [''Show, ''Read, ''Eq]
+  (savesig, savefn) <- genSaveLuxiOp cons
+  req_defs <- declareSADT "LuxiReq" .
+              map (\(str, _) -> ("Req" ++ str, mkName ("luxiReq" ++ str))) $
+                  cons
+  return $ [declD, savesig, savefn] ++ req_defs
+
+-- | Generates the \"save\" expression for a single luxi parameter.
+saveLuxiField :: Name -> LuxiParam -> Q Exp
+saveLuxiField fvar (_, qt, fn) =
+    [| JSON.showJSON ( $(liftM2 appFn fn $ varE fvar) ) |]
+
+-- | Generates the \"save\" clause for entire LuxiOp constructor.
+saveLuxiConstructor :: (String, [LuxiParam]) -> Q Clause
+saveLuxiConstructor (sname, fields) = do
+  let cname = mkName sname
+      fnames = map (\(nm, _, _) -> mkName nm) fields
+      pat = conP cname (map varP fnames)
+      flist = map (uncurry saveLuxiField) (zip fnames fields)
+      finval = if null flist
+               then [| JSON.showJSON ()    |]
+               else [| JSON.showJSON $(listE flist) |]
+  clause [pat] (normalB finval) []
+
+-- | Generates the main save LuxiOp function.
+genSaveLuxiOp :: [(String, [LuxiParam])]-> Q (Dec, Dec)
+genSaveLuxiOp opdefs = do
+  sigt <- [t| $(conT (mkName "LuxiOp")) -> JSON.JSValue |]
+  let fname = mkName "opToArgs"
+  cclauses <- mapM saveLuxiConstructor opdefs
+  return $ (SigD fname sigt, FunD fname cclauses)
+
+-- * "Objects" functionality
+
+-- | Extract the field's declaration from a Field structure.
+fieldTypeInfo :: String -> Field -> Q (Name, Strict, Type)
+fieldTypeInfo field_pfx fd = do
+  t <- actualFieldType fd
+  let n = mkName . (field_pfx ++) . fieldRecordName $ fd
+  return (n, NotStrict, t)
+
+-- | Build an object declaration.
+buildObject :: String -> String -> [Field] -> Q [Dec]
+buildObject sname field_pfx fields = do
+  let name = mkName sname
+  fields_d <- mapM (fieldTypeInfo field_pfx) fields
+  let decl_d = RecC name fields_d
+  let declD = DataD [] name [] [decl_d] [''Show, ''Read, ''Eq]
+  ser_decls <- buildObjectSerialisation sname fields
+  return $ declD:ser_decls
+
+buildObjectSerialisation :: String -> [Field] -> Q [Dec]
+buildObjectSerialisation sname fields = do
+  let name = mkName sname
+  savedecls <- genSaveObject saveObjectField sname fields
+  (loadsig, loadfn) <- genLoadObject loadObjectField sname fields
+  shjson <- objectShowJSON sname
+  rdjson <- objectReadJSON sname
+  let instdecl = InstanceD [] (AppT (ConT ''JSON.JSON) (ConT name))
+                 [rdjson, shjson]
+  return $ savedecls ++ [loadsig, loadfn, instdecl]
+
+genSaveObject :: (Name -> Field -> Q Exp)
+              -> String -> [Field] -> Q [Dec]
+genSaveObject save_fn sname fields = do
+  let name = mkName sname
+  let fnames = map (mkName . fieldVariable) fields
+  let pat = conP name (map varP fnames)
+  let tdname = mkName ("toDict" ++ sname)
+  tdsigt <- [t| $(conT name) -> [(String, JSON.JSValue)] |]
+
+  let felems = map (uncurry save_fn) (zip fnames fields)
+      flist = listE felems
+      -- and finally convert all this to a json object
+      tdlist = [| concat $flist |]
+      iname = mkName "i"
+  tclause <- clause [pat] (normalB tdlist) []
+  cclause <- [| $(varNameE "makeObj") . $(varE tdname) |]
+  let fname = mkName ("save" ++ sname)
+  sigt <- [t| $(conT name) -> JSON.JSValue |]
+  return [SigD tdname tdsigt, FunD tdname [tclause],
+          SigD fname sigt, ValD (VarP fname) (NormalB cclause) []]
+
+saveObjectField :: Name -> Field -> Q Exp
+saveObjectField fvar field
+  | isContainer = [| [( $nameE , JSON.showJSON . showContainer $ $fvarE)] |]
+  | fisOptional = [| case $(varE fvar) of
+                      Nothing -> []
+                      Just v -> [( $nameE, JSON.showJSON v)]
+                  |]
+  | otherwise = case fieldShow field of
+      Nothing -> [| [( $nameE, JSON.showJSON $fvarE)] |]
+      Just fn -> [| let (actual, extra) = $fn $fvarE
+                    in extra ++ [( $nameE, JSON.showJSON actual)]
+                  |]
+  where isContainer = fieldIsContainer field
+        fisOptional  = fieldIsOptional field
+        nameE = stringE (fieldName field)
+        fvarE = varE fvar
+
+objectShowJSON :: String -> Q Dec
+objectShowJSON name = do
+  body <- [| JSON.showJSON . $(varE . mkName $ "save" ++ name) |]
+  return $ FunD (mkName "showJSON") [Clause [] (NormalB body) []]
+
+genLoadObject :: (Field -> Q (Name, Stmt))
+              -> String -> [Field] -> Q (Dec, Dec)
+genLoadObject load_fn sname fields = do
+  let name = mkName sname
+      funname = mkName $ "load" ++ sname
+      arg1 = mkName "v"
+      objname = mkName "o"
+      opid = mkName "op_id"
+  st1 <- bindS (varP objname) [| liftM JSON.fromJSObject
+                                 (JSON.readJSON $(varE arg1)) |]
+  fbinds <- mapM load_fn fields
+  let (fnames, fstmts) = unzip fbinds
+  let cval = foldl (\accu fn -> AppE accu (VarE fn)) (ConE name) fnames
+      fstmts' = st1:fstmts ++ [NoBindS (AppE (VarE 'return) cval)]
+  sigt <- [t| JSON.JSValue -> JSON.Result $(conT name) |]
+  return $ (SigD funname sigt,
+            FunD funname [Clause [VarP arg1] (NormalB (DoE fstmts')) []])
+
+loadObjectField :: Field -> Q (Name, Stmt)
+loadObjectField field = do
+  let name = fieldVariable field
+      fvar = mkName name
+  -- these are used in all patterns below
+  let objvar = varNameE "o"
+      objfield = stringE (fieldName field)
+      loadexp =
+        if fieldIsOptional field
+          then [| $(varNameE "maybeFromObj") $objvar $objfield |]
+          else case fieldDefault field of
+                 Just defv ->
+                   [| $(varNameE "fromObjWithDefault") $objvar
+                      $objfield $defv |]
+                 Nothing -> [| $(varNameE "fromObj") $objvar $objfield |]
+  bexp <- loadFn field loadexp objvar
+
+  return (fvar, BindS (VarP fvar) bexp)
+
+objectReadJSON :: String -> Q Dec
+objectReadJSON name = do
+  let s = mkName "s"
+  body <- [| case JSON.readJSON $(varE s) of
+               JSON.Ok s' -> $(varE .mkName $ "load" ++ name) s'
+               JSON.Error e ->
+                 JSON.Error $ "Can't parse value for type " ++
+                       $(stringE name) ++ ": " ++ e
+           |]
+  return $ FunD (mkName "readJSON") [Clause [VarP s] (NormalB body) []]
+
+-- * Inheritable parameter tables implementation
+
+-- | Compute parameter type names.
+paramTypeNames :: String -> (String, String)
+paramTypeNames root = ("Filled"  ++ root ++ "Params",
+                       "Partial" ++ root ++ "Params")
+
+-- | Compute information about the type of a parameter field.
+paramFieldTypeInfo :: String -> Field -> Q (Name, Strict, Type)
+paramFieldTypeInfo field_pfx fd = do
+  t <- actualFieldType fd
+  let n = mkName . (++ "P") . (field_pfx ++) .
+          fieldRecordName $ fd
+  return (n, NotStrict, AppT (ConT ''Maybe) t)
+
+-- | Build a parameter declaration.
+--
+-- This function builds two different data structures: a /filled/ one,
+-- in which all fields are required, and a /partial/ one, in which all
+-- fields are optional. Due to the current record syntax issues, the
+-- fields need to be named differrently for the two structures, so the
+-- partial ones get a /P/ suffix.
+buildParam :: String -> String -> [Field] -> Q [Dec]
+buildParam sname field_pfx fields = do
+  let (sname_f, sname_p) = paramTypeNames sname
+      name_f = mkName sname_f
+      name_p = mkName sname_p
+  fields_f <- mapM (fieldTypeInfo field_pfx) fields
+  fields_p <- mapM (paramFieldTypeInfo field_pfx) fields
+  let decl_f = RecC name_f fields_f
+      decl_p = RecC name_p fields_p
+  let declF = DataD [] name_f [] [decl_f] [''Show, ''Read, ''Eq]
+      declP = DataD [] name_p [] [decl_p] [''Show, ''Read, ''Eq]
+  ser_decls_f <- buildObjectSerialisation sname_f fields
+  ser_decls_p <- buildPParamSerialisation sname_p fields
+  fill_decls <- fillParam sname field_pfx fields
+  return $ [declF, declP] ++ ser_decls_f ++ ser_decls_p ++ fill_decls
+
+buildPParamSerialisation :: String -> [Field] -> Q [Dec]
+buildPParamSerialisation sname fields = do
+  let name = mkName sname
+  savedecls <- genSaveObject savePParamField sname fields
+  (loadsig, loadfn) <- genLoadObject loadPParamField sname fields
+  shjson <- objectShowJSON sname
+  rdjson <- objectReadJSON sname
+  let instdecl = InstanceD [] (AppT (ConT ''JSON.JSON) (ConT name))
+                 [rdjson, shjson]
+  return $ savedecls ++ [loadsig, loadfn, instdecl]
+
+savePParamField :: Name -> Field -> Q Exp
+savePParamField fvar field = do
+  checkNonOptDef field
+  let actualVal = mkName "v"
+  normalexpr <- saveObjectField actualVal field
+  -- we have to construct the block here manually, because we can't
+  -- splice-in-splice
+  return $ CaseE (VarE fvar) [ Match (ConP 'Nothing [])
+                                       (NormalB (ConE '[])) []
+                             , Match (ConP 'Just [VarP actualVal])
+                                       (NormalB normalexpr) []
+                             ]
+loadPParamField :: Field -> Q (Name, Stmt)
+loadPParamField field = do
+  checkNonOptDef field
+  let name = fieldName field
+      fvar = mkName name
+  -- these are used in all patterns below
+  let objvar = varNameE "o"
+      objfield = stringE name
+      loadexp = [| $(varNameE "maybeFromObj") $objvar $objfield |]
+  bexp <- loadFn field loadexp objvar
+  return (fvar, BindS (VarP fvar) bexp)
+
+-- | Builds a simple declaration of type @n_x = fromMaybe f_x p_x@.
+buildFromMaybe :: String -> Q Dec
+buildFromMaybe fname =
+  valD (varP (mkName $ "n_" ++ fname))
+         (normalB [| $(varNameE "fromMaybe")
+                        $(varNameE $ "f_" ++ fname)
+                        $(varNameE $ "p_" ++ fname) |]) []
+
+fillParam :: String -> String -> [Field] -> Q [Dec]
+fillParam sname field_pfx fields = do
+  let fnames = map (\fd -> field_pfx ++ fieldRecordName fd) fields
+      (sname_f, sname_p) = paramTypeNames sname
+      oname_f = "fobj"
+      oname_p = "pobj"
+      name_f = mkName sname_f
+      name_p = mkName sname_p
+      fun_name = mkName $ "fill" ++ sname ++ "Params"
+      le_full = ValD (ConP name_f (map (VarP . mkName . ("f_" ++)) fnames))
+                (NormalB . VarE . mkName $ oname_f) []
+      le_part = ValD (ConP name_p (map (VarP . mkName . ("p_" ++)) fnames))
+                (NormalB . VarE . mkName $ oname_p) []
+      obj_new = foldl (\accu vname -> AppE accu (VarE vname)) (ConE name_f)
+                $ map (mkName . ("n_" ++)) fnames
+  le_new <- mapM buildFromMaybe fnames
+  funt <- [t| $(conT name_f) -> $(conT name_p) -> $(conT name_f) |]
+  let sig = SigD fun_name funt
+      fclause = Clause [VarP (mkName oname_f), VarP (mkName oname_p)]
+                (NormalB $ LetE (le_full:le_part:le_new) obj_new) []
+      fun = FunD fun_name [fclause]
+  return [sig, fun]
diff --git a/htools/cli-tests-defs.sh b/htools/cli-tests-defs.sh
new file mode 100644 (file)
index 0000000..59d78ce
--- /dev/null
@@ -0,0 +1,50 @@
+#
+
+# Copyright (C) 2012 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.
+
+# This is an shell testing configuration fragment.
+
+HBINARY=${HBINARY:-./htools/hpc-htools}
+
+export TESTDATA_DIR=${TOP_SRCDIR:-.}/test/data/htools
+
+hbal() {
+  HTOOLS=hbal $HBINARY "$@"
+}
+
+hscan() {
+  HTOOLS=hscan $HBINARY "$@"
+}
+
+hail() {
+  HTOOLS=hail $HBINARY "$@"
+}
+
+hspace() {
+  HTOOLS=hspace $HBINARY "$@"
+}
+
+hinfo() {
+  HTOOLS=hinfo $HBINARY "$@"
+}
+
+hcheck() {
+  HTOOLS=hinfo $HBINARY "$@"
+}
+
+ALL_ROLES="hbal hscan hail hspace hinfo hcheck"
diff --git a/htools/hconfd.hs b/htools/hconfd.hs
new file mode 100644 (file)
index 0000000..32d1208
--- /dev/null
@@ -0,0 +1,48 @@
+{-| Ganeti configuration query daemon
+
+-}
+
+{-
+
+Copyright (C) 2009, 2011, 2012 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 Main (main) where
+
+import qualified Ganeti.Confd.Server
+import Ganeti.Daemon
+import Ganeti.Runtime
+import qualified Ganeti.Constants as C
+
+-- | Options list and functions.
+options :: [OptType]
+options =
+  [ oShowHelp
+  , oShowVer
+  , oNoDaemonize
+  , oNoUserChecks
+  , oDebug
+  , oPort C.defaultConfdPort
+  , oBindAddress
+  , oSyslogUsage
+  ]
+
+-- | Main function.
+main :: IO ()
+main = genericMain GanetiConfd options Ganeti.Confd.Server.main
diff --git a/htools/hpc-htools.hs b/htools/hpc-htools.hs
new file mode 120000 (symlink)
index 0000000..487efdc
--- /dev/null
@@ -0,0 +1 @@
+htools.hs
\ No newline at end of file
index 4ae856f..2e847d7 100644 (file)
@@ -4,7 +4,7 @@
 
 {-
 
-Copyright (C) 2011 Google Inc.
+Copyright (C) 2011, 2012 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
@@ -25,23 +25,18 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 
 module Main (main) where
 
+import Control.Exception
+import Control.Monad (guard)
 import Data.Char (toLower)
-import System
+import Prelude hiding (catch)
+import System.Environment
+import System.Exit
 import System.IO
+import System.IO.Error (isDoesNotExistError)
 
 import Ganeti.HTools.Utils
-import qualified Ganeti.HTools.Program.Hail as Hail
-import qualified Ganeti.HTools.Program.Hbal as Hbal
-import qualified Ganeti.HTools.Program.Hscan as Hscan
-import qualified Ganeti.HTools.Program.Hspace as Hspace
-
--- | Supported binaries.
-personalities :: [(String, IO ())]
-personalities = [ ("hail", Hail.main)
-                , ("hbal", Hbal.main)
-                , ("hscan", Hscan.main)
-                , ("hspace", Hspace.main)
-                ]
+import Ganeti.HTools.CLI (parseOpts)
+import Ganeti.HTools.Program (personalities)
 
 -- | Display usage and exit.
 usage :: String -> IO ()
@@ -56,7 +51,13 @@ usage name = do
 
 main :: IO ()
 main = do
-  binary <- getEnv "HTOOLS" `catch` (\_ -> getProgName)
+  binary <- catchJust (guard . isDoesNotExistError)
+            (getEnv "HTOOLS") (const getProgName)
   let name = map toLower binary
-      boolnames = map (\(x, y) -> (x == name, y)) personalities
-  select (usage name) boolnames
+      boolnames = map (\(x, y) -> (x == name, Just y)) personalities
+  case select Nothing boolnames of
+    Nothing -> usage name
+    Just (fn, options) -> do
+         cmd_args <- getArgs
+         (opts, args) <- parseOpts cmd_args name options
+         fn opts args
diff --git a/htools/lint-hints.hs b/htools/lint-hints.hs
new file mode 100644 (file)
index 0000000..8c9828a
--- /dev/null
@@ -0,0 +1,11 @@
+-- The following two hints warn to simplify e.g. "map (\v -> (v,
+-- True)) lst" to "zip lst (repeat True)", which is more abstract
+warn = map (\v -> (v, x)) y ==> zip y (repeat x)
+  where _ = notIn v x
+warn = map (\v -> (x, v)) ==> zip (repeat x)
+  where _ = notIn v x
+
+-- The following warn on use of length instead of null
+warn = length x > 0 ==> not (null x)
+warn = length x /= 0 ==> not (null x)
+warn = length x == 0 ==> null x
index c47766f..78b680f 100755 (executable)
@@ -1,6 +1,6 @@
 #!/bin/bash
 
-# Copyright (C) 2009, 2010, 2011 Google Inc.
+# Copyright (C) 2009, 2010, 2011, 2012 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
@@ -35,21 +35,23 @@ set -e
 : ${RAPI:=localhost}
 GROUP=${GROUP:+-G $GROUP}
 
+. $(dirname $0)/cli-tests-defs.sh
+
 T=`mktemp -d`
 trap 'rm -rf $T' EXIT
 echo Using $T as temporary dir
 
 echo Checking command line
-for prog in hscan hbal hail hspace; do
-    ./$prog --version
-    ./$prog --help
-    ! ./$prog --no-such-option
+for prog in $ALL_ROLES; do
+  $prog --version
+  $prog --help >/dev/null
+  ! $prog --no-such-option 2>/dev/null
 done
 
 echo Testing hscan/rapi
-./hscan -d$T $RAPI -p
+hscan -d$T $RAPI -p
 echo Testing hscan/luxi
-./hscan -d$T -L$LUXI -p
+hscan -d$T -L$LUXI -p
 echo Comparing hscan results...
 diff -u $T/$RAPI.data $T/LOCAL.data
 
@@ -58,13 +60,13 @@ FI=$($CLUSTER head -n1 /var/lib/ganeti/ssconf_instance_list)
 
 
 echo Testing hbal/luxi
-./hbal -L$LUXI $GROUP -p --print-instances -C$T/hbal-luxi-cmds.sh
+hbal -L$LUXI $GROUP -p --print-instances -C$T/hbal-luxi-cmds.sh
 bash -n $T/hbal-luxi-cmds.sh
 echo Testing hbal/rapi
-./hbal -m$RAPI $GROUP -p --print-instances -C$T/hbal-rapi-cmds.sh
+hbal -m$RAPI $GROUP -p --print-instances -C$T/hbal-rapi-cmds.sh
 bash -n $T/hbal-rapi-cmds.sh
 echo Testing hbal/text
-./hbal -t$T/$RAPI.data $GROUP -p --print-instances -C$T/hbal-text-cmds.sh
+hbal -t$T/$RAPI.data $GROUP -p --print-instances -C$T/hbal-text-cmds.sh
 bash -n $T/hbal-text-cmds.sh
 echo Comparing hbal results
 diff -u $T/hbal-luxi-cmds.sh $T/hbal-rapi-cmds.sh
@@ -72,30 +74,30 @@ diff -u $T/hbal-luxi-cmds.sh $T/hbal-text-cmds.sh
 
 
 echo Testing hbal/text with evacuation mode
-./hbal -t$T/$RAPI.data $GROUP -E
+hbal -t$T/$RAPI.data $GROUP -E
 echo Testing hbal/text with no disk moves
-./hbal -t$T/$RAPI.data $GROUP --no-disk-moves
+hbal -t$T/$RAPI.data $GROUP --no-disk-moves
 echo Testing hbal/text with no instance moves
-./hbal -t$T/$RAPI.data $GROUP --no-instance-moves
+hbal -t$T/$RAPI.data $GROUP --no-instance-moves
 echo Testing hbal/text with offline node mode
-./hbal -t$T/$RAPI.data $GROUP -O$FN
+hbal -t$T/$RAPI.data $GROUP -O$FN
 echo Testing hbal/text with utilization data
 echo "$FI 2 2 2 2" > $T/util.data
-./hbal -t$T/$RAPI.data $GROUP -U $T/util.data
+hbal -t$T/$RAPI.data $GROUP -U $T/util.data
 echo Testing hbal/text with bad utilization data
 echo "$FI 2 a 3b" > $T/util.data
-! ./hbal -t$T/$RAPI.data $GROUP -U $T/util.data
+! hbal -t$T/$RAPI.data $GROUP -U $T/util.data
 echo Testing hbal/text with instance exclusion/selection
-./hbal -t$T/$RAPI.data $GROUP --exclude-instances=$FI
-./hbal -t$T/$RAPI.data $GROUP --select-instances=$FI
-! ./hbal -t$T/$RAPI.data --exclude-instances=no_such_instance
-! ./hbal -t$T/$RAPI.data --select-instances=no_such_instance
+hbal -t$T/$RAPI.data $GROUP --exclude-instances=$FI
+hbal -t$T/$RAPI.data $GROUP --select-instances=$FI
+! hbal -t$T/$RAPI.data --exclude-instances=no_such_instance
+! hbal -t$T/$RAPI.data --select-instances=no_such_instance
 echo Testing hbal/text with tag exclusion
-./hbal -t $T/$RAPI.data $GROUP --exclusion-tags=no_such_tag
+hbal -t $T/$RAPI.data $GROUP --exclusion-tags=no_such_tag
 echo Testing hbal multiple backend failure
-! ./hbal -t $T/$RAPI.data -L$LUXI
+! hbal -t $T/$RAPI.data -L$LUXI
 echo Testing hbal no backend failure
-! ./hbal
+! hbal
 
 echo Getting data files for hail
 for dtemplate in plain drbd; do
@@ -109,44 +111,44 @@ $CLUSTER gnt-debug allocator --dir in --mode multi-evacuate \
     $FN > $T/h-evacuate.json
 for dtemplate in plain drbd; do
   echo Testing hail/allocate-$dtemplate
-  ./hail $T/h-alloc-$dtemplate.json
+  hail $T/h-alloc-$dtemplate.json
 done
 echo Testing hail/relocate for instance $FI
-./hail $T/h-reloc.json
+hail $T/h-reloc.json
 echo Testing hail/evacuate for node $FN
-./hail $T/h-evacuate.json
+hail $T/h-evacuate.json
 
 HOUT="$T/hspace.out"
 
 check_hspace_out() {
-    set -u
-    set -e
-    source "$HOUT"
-    echo ALLOC_INSTANCES=$HTS_ALLOC_INSTANCES
-    echo TSPEC=$HTS_TSPEC
-    echo OK=$HTS_OK
+  set -u
+  set -e
+  source "$HOUT"
+  echo ALLOC_INSTANCES=$HTS_ALLOC_INSTANCES
+  echo TSPEC=$HTS_TSPEC
+  echo OK=$HTS_OK
 }
 
 TIER="--tiered 102400,8192,2"
 SIMU="--simu=preferred,10,6835937,32768,4"
 echo Testing hspace/luxi
-./hspace -L$LUXI $TIER -v > $HOUT
+hspace -L$LUXI $TIER -v > $HOUT
 ( check_hspace_out ) || exit 1
 echo Testing hspace/rapi
-./hspace -m$RAPI $TIER -v > $HOUT
+hspace -m$RAPI $TIER -v > $HOUT
 ( check_hspace_out ) || exit 1
 echo Testing hspace/text
-./hspace -t$T/$RAPI.data $TIER -v > $HOUT
+hspace -t$T/$RAPI.data $TIER -v > $HOUT
 ( check_hspace_out ) || exit 1
 echo Testing hspace/simu
 # ~6T disk space, 32G ram, 4 VCPUs
-./hspace $SIMU $TIER -v > $HOUT
+hspace $SIMU $TIER -v > $HOUT
 ( check_hspace_out ) || exit 1
 # Wrong tiered spec input
-! ./hspace $SIMU --tiered 1,2,3x
-! ./hspace $SIMU --tiered 1,2,x
-! ./hspace $SIMU --tiered 1,2
+! hspace $SIMU --tiered 1,2,3x
+! hspace $SIMU --tiered 1,2,x
+! hspace $SIMU --tiered 1,2
 # Wrong simu spec
-! ./hspace --simu=1,2,x
+! hspace --simu=1,2,x
 
 echo All OK
diff --git a/htools/offline-test.sh b/htools/offline-test.sh
new file mode 100755 (executable)
index 0000000..cd62c0c
--- /dev/null
@@ -0,0 +1,93 @@
+#!/bin/bash
+
+# Copyright (C) 2012 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.
+
+# This is an offline testing script for most/all of the htools
+# programs, checking basic command line functionality.
+
+set -e
+set -o pipefail
+
+. $(dirname $0)/cli-tests-defs.sh
+
+echo Running offline htools tests
+
+export T=`mktemp -d`
+trap 'rm -rf $T' EXIT
+trap 'echo FAIL to build test files' ERR
+echo Using $T as temporary dir
+
+echo -n Generating hspace simulation data for hinfo and hbal...
+# this cluster spec should be fine
+./test/hspace --simu p,4,8T,64g,16 -S $T/simu-onegroup \
+  --disk-template drbd -l 8 -v -v -v >/dev/null 2>&1
+echo OK
+
+echo -n Generating hinfo and hbal test files for multi-group...
+./test/hspace --simu p,4,8T,64g,16 --simu p,4,8T,64g,16 \
+  -S $T/simu-twogroups --disk-template drbd -l 8 >/dev/null 2>&1
+echo OK
+
+echo -n Generating test files for rebalancing...
+# we generate a cluster with two node groups, one with unallocable
+# policy, then we change all nodes from this group to the allocable
+# one, and we check for rebalancing
+FROOT="$T/simu-rebal-orig"
+./test/hspace --simu u,4,8T,64g,16 --simu p,4,8T,64g,16 \
+  -S $FROOT --disk-template drbd -l 8 >/dev/null 2>&1
+for suffix in standard tiered; do
+  RELOC="$T/simu-rebal-merged.$suffix"
+  # this relocates the nodes
+  sed -re 's/^(node-.*|fake-uuid-)-02(|.*)/\1-01\2/' \
+    < $FROOT.$suffix > $RELOC
+done
+export BACKEND_BAL_STD="-t$T/simu-rebal-merged.standard"
+export BACKEND_BAL_TIER="-t$T/simu-rebal-merged.tiered"
+echo OK
+
+# For various tests
+export BACKEND_DYNU="-t $T/simu-onegroup.standard"
+export BACKEND_EXCL="-t $T/simu-onegroup.standard"
+
+echo -n Generating data files for IAllocator checks...
+for evac_mode in primary-only secondary-only all; do
+  sed -e 's/"evac_mode": "all"/"evac_mode": "'${evac_mode}'"/' \
+    < $TESTDATA_DIR/hail-node-evac.json \
+    > $T/hail-node-evac.json.$evac_mode
+done
+echo OK
+
+echo -n Checking file-based RAPI...
+mkdir -p $T/hscan
+export RAPI_URL="file://$TESTDATA_DIR/rapi"
+./test/hscan -d $T/hscan/ -p -v -v $RAPI_URL >/dev/null 2>&1
+# check that we file parsing is correct, i.e. hscan saves correct text
+# files, and is idempotent (rapi+text == rapi); more is tested in
+# shelltest later
+RAPI_TXT="$(ls $T/hscan/*.data|head -n1)"
+./test/hinfo -p --print-instances -m $RAPI_URL > $T/hscan/direct.hinfo 2>&1
+./test/hinfo -p --print-instances -t $RAPI_TXT > $T/hscan/fromtext.hinfo 2>&1
+echo OK
+
+echo Running shelltest...
+
+shelltest $SHELLTESTARGS \
+  ${TOP_SRCDIR:-.}/test/htools-*.test \
+  -- --hide-successes
+
+echo All OK
index 34bd05a..6e43427 100644 (file)
@@ -4,7 +4,7 @@
 
 {-
 
-Copyright (C) 2009, 2011 Google Inc.
+Copyright (C) 2009, 2011, 2012 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
@@ -25,12 +25,15 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 
 module Main(main) where
 
+import Data.Char
 import Data.IORef
-import Test.QuickCheck
-import System.Console.GetOpt
-import System.IO
+import Data.List
+import Data.Maybe (fromMaybe)
+import System.Console.GetOpt ()
+import System.Environment (getArgs)
 import System.Exit
-import System (getArgs)
+import System.IO
+import Test.QuickCheck
 import Text.Printf
 
 import Ganeti.HTools.QC
@@ -40,11 +43,12 @@ import Ganeti.HTools.Utils (sepSplit)
 -- | Options list and functions.
 options :: [OptType]
 options =
-    [ oReplay
-    , oVerbose
-    , oShowVer
-    , oShowHelp
-    ]
+  [ oReplay
+  , oVerbose
+  , oShowVer
+  , oShowHelp
+  , oTestCount
+  ]
 
 fast :: Args
 fast = stdArgs
@@ -63,86 +67,117 @@ incIORef ir = atomicModifyIORef ir (\x -> (x + 1, ()))
 
 -- | Wrapper over a test runner with error counting.
 wrapTest :: IORef Int
-         -> (Args -> IO Result)
+         -> (Args -> IO Result, String)
          -> Args
-         -> IO (Result, Char)
-wrapTest ir test opts = do
+         -> IO (Result, Char, String)
+wrapTest ir (test, desc) opts = do
   r <- test opts
   c <- case r of
          Success {} -> return '.'
          GaveUp  {} -> return '?'
          Failure {} -> incIORef ir >> return '#'
          NoExpectedFailure {} -> incIORef ir >> return '*'
-  return (r, c)
+  return (r, c, desc)
+
+runTests :: String
+         -> Args
+         -> [Args -> IO (Result, Char, String)]
+         -> Int
+         -> IO [(Result, String)]
 
 runTests name opts tests max_count = do
   _ <- printf "%25s : " name
   hFlush stdout
   results <- mapM (\t -> do
-                     (r, c) <- t opts
+                     (r, c, desc) <- t opts
                      putChar c
                      hFlush stdout
-                     return r
+                     return (r, desc)
                   ) tests
-  let alldone = sum . map numTests $ results
+  let alldone = sum . map (numTests . fst) $ results
   _ <- printf "%*s(%d)\n" (max_count - length tests + 1) " " alldone
-  mapM_ (\(idx, r) ->
+  mapM_ (\(r, desc) ->
              case r of
                Failure { output = o, usedSeed = u, usedSize = size } ->
-                   printf "Test %d failed (seed was %s, test size %d): %s\n"
-                          idx (show u) size o
+                   printf "Test %s failed (seed was %s, test size %d): %s\n"
+                          desc (show u) size o
                GaveUp { numTests = passed } ->
-                   printf "Test %d incomplete: gave up with only %d\
+                   printf "Test %s incomplete: gave up with only %d\
                           \ passes after discarding %d tests\n"
-                          idx passed (maxDiscard opts)
+                          desc passed (maxDiscard opts)
                _ -> return ()
-        ) $ zip ([1..]::[Int]) results
+        ) results
   return results
 
-allTests :: [(String, Args, [Args -> IO Result])]
+allTests :: [(Args, (String, [(Args -> IO Result, String)]))]
 allTests =
-  [ ("Utils", fast, testUtils)
-  , ("PeerMap", fast, testPeerMap)
-  , ("Container", fast, testContainer)
-  , ("Instance", fast, testInstance)
-  , ("Node", fast, testNode)
-  , ("Text", fast, testText)
-  , ("OpCodes", fast, testOpCodes)
-  , ("Jobs", fast, testJobs)
-  , ("Loader", fast, testLoader)
-  , ("Types", fast, testTypes)
-  , ("Cluster", slow, testCluster)
+  [ (fast, testUtils)
+  , (fast, testPeerMap)
+  , (fast, testContainer)
+  , (fast, testInstance)
+  , (fast, testNode)
+  , (fast, testText)
+  , (fast, testSimu)
+  , (fast, testOpCodes)
+  , (fast, testJobs)
+  , (fast, testLoader)
+  , (fast, testTypes)
+  , (fast, testCLI)
+  , (fast, testJSON)
+  , (fast, testLUXI)
+  , (fast, testSsconf)
+  , (slow, testCluster)
   ]
 
+-- | Extracts the name of a test group.
+extractName :: (Args, (String, [(Args -> IO Result, String)])) -> String
+extractName (_, (name, _)) = name
+
+-- | Lowercase a string.
+lower :: String -> String
+lower = map toLower
+
 transformTestOpts :: Args -> Options -> IO Args
 transformTestOpts args opts = do
   r <- case optReplay opts of
          Nothing -> return Nothing
          Just str -> do
            let vs = sepSplit ',' str
-           (case vs of
-              [rng, size] -> return $ Just (read rng, read size)
-              _ -> fail "Invalid state given")
-  return args { chatty = optVerbose opts > 1,
-                replay = r
+           case vs of
+             [rng, size] -> return $ Just (read rng, read size)
+             _ -> fail "Invalid state given"
+  return args { chatty = optVerbose opts > 1
+              , replay = r
+              , maxSuccess = fromMaybe (maxSuccess args) (optTestCount opts)
+              , maxDiscard = fromMaybe (maxDiscard args) (optTestCount opts)
               }
 
 main :: IO ()
 main = do
   errs <- newIORef 0
   let wrap = map (wrapTest errs)
-  cmd_args <- System.getArgs
+  cmd_args <- getArgs
   (opts, args) <- parseOpts cmd_args "test" options
-  let tests = if null args
-              then allTests
-              else filter (\(name, _, _) -> name `elem` args) allTests
-      max_count = maximum $ map (\(_, _, t) -> length t) tests
-  mapM_ (\(name, targs, tl) ->
-             transformTestOpts targs opts >>= \newargs ->
-             runTests name newargs (wrap tl) max_count) tests
+  tests <- if null args
+             then return allTests
+             else let args' = map lower args
+                      selected = filter ((`elem` args') . lower .
+                                         extractName) allTests
+                  in if null selected
+                       then do
+                         hPutStrLn stderr $ "No tests matching '"
+                            ++ unwords args ++ "', available tests: "
+                            ++ intercalate ", " (map extractName allTests)
+                         exitWith $ ExitFailure 1
+                       else return selected
+
+  let max_count = maximum $ map (\(_, (_, t)) -> length t) tests
+  mapM_ (\(targs, (name, tl)) ->
+           transformTestOpts targs opts >>= \newargs ->
+           runTests name newargs (wrap tl) max_count) tests
   terr <- readIORef errs
-  (if terr > 0
-   then do
-     hPutStrLn stderr $ "A total of " ++ show terr ++ " tests failed."
-     exitWith $ ExitFailure 1
-   else putStrLn "All tests succeeded.")
+  if terr > 0
+    then do
+      hPutStrLn stderr $ "A total of " ++ show terr ++ " tests failed."
+      exitWith $ ExitFailure 1
+    else putStrLn "All tests succeeded."
index f86297a..51a06bb 100644 (file)
@@ -60,6 +60,8 @@ from ganeti import ssconf
 from ganeti import serializer
 from ganeti import netutils
 from ganeti import runtime
+from ganeti import mcpu
+from ganeti import compat
 
 
 _BOOT_ID_PATH = "/proc/sys/kernel/random/boot_id"
@@ -79,6 +81,10 @@ _IES_CA_FILE = "ca"
 #: Valid LVS output line regex
 _LVSLINE_REGEX = re.compile("^ *([^|]+)\|([^|]+)\|([0-9.]+)\|([^|]{6,})\|?$")
 
+# Actions for the master setup script
+_MASTER_START = "start"
+_MASTER_STOP = "stop"
+
 
 class RPCFail(Exception):
   """Class denoting RPC failure.
@@ -196,6 +202,8 @@ def _BuildUploadFileList():
     constants.SSH_KNOWN_HOSTS_FILE,
     constants.VNC_PASSWORD_FILE,
     constants.RAPI_CERT_FILE,
+    constants.SPICE_CERT_FILE,
+    constants.SPICE_CACERT_FILE,
     constants.RAPI_USERS_FILE,
     constants.CONFD_HMAC_KEY,
     constants.CLUSTER_DOMAIN_SECRET_FILE,
@@ -203,7 +211,7 @@ def _BuildUploadFileList():
 
   for hv_name in constants.HYPER_TYPES:
     hv_class = hypervisor.GetHypervisorClass(hv_name)
-    allowed_files.update(hv_class.GetAncillaryFiles())
+    allowed_files.update(hv_class.GetAncillaryFiles()[0])
 
   return frozenset(allowed_files)
 
@@ -229,7 +237,8 @@ def GetMasterInfo():
   for consumption here or from the node daemon.
 
   @rtype: tuple
-  @return: master_netdev, master_ip, master_name, primary_ip_family
+  @return: master_netdev, master_ip, master_name, primary_ip_family,
+    master_netmask
   @raise RPCFail: in case of errors
 
   """
@@ -237,125 +246,212 @@ def GetMasterInfo():
     cfg = _GetConfig()
     master_netdev = cfg.GetMasterNetdev()
     master_ip = cfg.GetMasterIP()
+    master_netmask = cfg.GetMasterNetmask()
     master_node = cfg.GetMasterNode()
     primary_ip_family = cfg.GetPrimaryIPFamily()
   except errors.ConfigurationError, err:
     _Fail("Cluster configuration incomplete: %s", err, exc=True)
-  return (master_netdev, master_ip, master_node, primary_ip_family)
+  return (master_netdev, master_ip, master_node, primary_ip_family,
+      master_netmask)
+
+
+def RunLocalHooks(hook_opcode, hooks_path, env_builder_fn):
+  """Decorator that runs hooks before and after the decorated function.
+
+  @type hook_opcode: string
+  @param hook_opcode: opcode of the hook
+  @type hooks_path: string
+  @param hooks_path: path of the hooks
+  @type env_builder_fn: function
+  @param env_builder_fn: function that returns a dictionary containing the
+    environment variables for the hooks. Will get all the parameters of the
+    decorated function.
+  @raise RPCFail: in case of pre-hook failure
+
+  """
+  def decorator(fn):
+    def wrapper(*args, **kwargs):
+      _, myself = ssconf.GetMasterAndMyself()
+      nodes = ([myself], [myself])  # these hooks run locally
+
+      env_fn = compat.partial(env_builder_fn, *args, **kwargs)
+
+      cfg = _GetConfig()
+      hr = HooksRunner()
+      hm = mcpu.HooksMaster(hook_opcode, hooks_path, nodes, hr.RunLocalHooks,
+                            None, env_fn, logging.warning, cfg.GetClusterName(),
+                            cfg.GetMasterNode())
+
+      hm.RunPhase(constants.HOOKS_PHASE_PRE)
+      result = fn(*args, **kwargs)
+      hm.RunPhase(constants.HOOKS_PHASE_POST)
+
+      return result
+    return wrapper
+  return decorator
+
+
+def _BuildMasterIpEnv(master_params, use_external_mip_script=None):
+  """Builds environment variables for master IP hooks.
+
+  @type master_params: L{objects.MasterNetworkParameters}
+  @param master_params: network parameters of the master
+  @type use_external_mip_script: boolean
+  @param use_external_mip_script: whether to use an external master IP
+    address setup script (unused, but necessary per the implementation of the
+    _RunLocalHooks decorator)
+
+  """
+  # pylint: disable=W0613
+  ver = netutils.IPAddress.GetVersionFromAddressFamily(master_params.ip_family)
+  env = {
+    "MASTER_NETDEV": master_params.netdev,
+    "MASTER_IP": master_params.ip,
+    "MASTER_NETMASK": str(master_params.netmask),
+    "CLUSTER_IP_VERSION": str(ver),
+  }
+
+  return env
+
+
+def _RunMasterSetupScript(master_params, action, use_external_mip_script):
+  """Execute the master IP address setup script.
 
+  @type master_params: L{objects.MasterNetworkParameters}
+  @param master_params: network parameters of the master
+  @type action: string
+  @param action: action to pass to the script. Must be one of
+    L{backend._MASTER_START} or L{backend._MASTER_STOP}
+  @type use_external_mip_script: boolean
+  @param use_external_mip_script: whether to use an external master IP
+    address setup script
+  @raise backend.RPCFail: if there are errors during the execution of the
+    script
+
+  """
+  env = _BuildMasterIpEnv(master_params)
+
+  if use_external_mip_script:
+    setup_script = constants.EXTERNAL_MASTER_SETUP_SCRIPT
+  else:
+    setup_script = constants.DEFAULT_MASTER_SETUP_SCRIPT
 
-def StartMaster(start_daemons, no_voting):
+  result = utils.RunCmd([setup_script, action], env=env, reset_env=True)
+
+  if result.failed:
+    _Fail("Failed to %s the master IP. Script return value: %s" %
+          (action, result.exit_code), log=True)
+
+
+@RunLocalHooks(constants.FAKE_OP_MASTER_TURNUP, "master-ip-turnup",
+               _BuildMasterIpEnv)
+def ActivateMasterIp(master_params, use_external_mip_script):
+  """Activate the IP address of the master daemon.
+
+  @type master_params: L{objects.MasterNetworkParameters}
+  @param master_params: network parameters of the master
+  @type use_external_mip_script: boolean
+  @param use_external_mip_script: whether to use an external master IP
+    address setup script
+  @raise RPCFail: in case of errors during the IP startup
+
+  """
+  _RunMasterSetupScript(master_params, _MASTER_START,
+                        use_external_mip_script)
+
+
+def StartMasterDaemons(no_voting):
   """Activate local node as master node.
 
-  The function will either try activate the IP address of the master
-  (unless someone else has it) or also start the master daemons, based
-  on the start_daemons parameter.
+  The function will start the master daemons (ganeti-masterd and ganeti-rapi).
 
-  @type start_daemons: boolean
-  @param start_daemons: whether to start the master daemons
-      (ganeti-masterd and ganeti-rapi), or (if false) activate the
-      master ip
   @type no_voting: boolean
   @param no_voting: whether to start ganeti-masterd without a node vote
-      (if start_daemons is True), but still non-interactively
+      but still non-interactively
   @rtype: None
 
   """
-  # GetMasterInfo will raise an exception if not able to return data
-  master_netdev, master_ip, _, family = GetMasterInfo()
 
-  err_msgs = []
-  # either start the master and rapi daemons
-  if start_daemons:
-    if no_voting:
-      masterd_args = "--no-voting --yes-do-it"
-    else:
-      masterd_args = ""
+  if no_voting:
+    masterd_args = "--no-voting --yes-do-it"
+  else:
+    masterd_args = ""
 
-    env = {
-      "EXTRA_MASTERD_ARGS": masterd_args,
-      }
+  env = {
+    "EXTRA_MASTERD_ARGS": masterd_args,
+    }
+
+  result = utils.RunCmd([constants.DAEMON_UTIL, "start-master"], env=env)
+  if result.failed:
+    msg = "Can't start Ganeti master: %s" % result.output
+    logging.error(msg)
+    _Fail(msg)
 
-    result = utils.RunCmd([constants.DAEMON_UTIL, "start-master"], env=env)
-    if result.failed:
-      msg = "Can't start Ganeti master: %s" % result.output
-      logging.error(msg)
-      err_msgs.append(msg)
-  # or activate the IP
-  else:
-    if netutils.TcpPing(master_ip, constants.DEFAULT_NODED_PORT):
-      if netutils.IPAddress.Own(master_ip):
-        # we already have the ip:
-        logging.debug("Master IP already configured, doing nothing")
-      else:
-        msg = "Someone else has the master ip, not activating"
-        logging.error(msg)
-        err_msgs.append(msg)
-    else:
-      ipcls = netutils.IP4Address
-      if family == netutils.IP6Address.family:
-        ipcls = netutils.IP6Address
-
-      result = utils.RunCmd([constants.IP_COMMAND_PATH, "address", "add",
-                             "%s/%d" % (master_ip, ipcls.iplen),
-                             "dev", master_netdev, "label",
-                             "%s:0" % master_netdev])
-      if result.failed:
-        msg = "Can't activate master IP: %s" % result.output
-        logging.error(msg)
-        err_msgs.append(msg)
-
-      # we ignore the exit code of the following cmds
-      if ipcls == netutils.IP4Address:
-        utils.RunCmd(["arping", "-q", "-U", "-c 3", "-I", master_netdev, "-s",
-                      master_ip, master_ip])
-      elif ipcls == netutils.IP6Address:
-        try:
-          utils.RunCmd(["ndisc6", "-q", "-r 3", master_ip, master_netdev])
-        except errors.OpExecError:
-          # TODO: Better error reporting
-          logging.warning("Can't execute ndisc6, please install if missing")
 
-  if err_msgs:
-    _Fail("; ".join(err_msgs))
+@RunLocalHooks(constants.FAKE_OP_MASTER_TURNDOWN, "master-ip-turndown",
+               _BuildMasterIpEnv)
+def DeactivateMasterIp(master_params, use_external_mip_script):
+  """Deactivate the master IP on this node.
+
+  @type master_params: L{objects.MasterNetworkParameters}
+  @param master_params: network parameters of the master
+  @type use_external_mip_script: boolean
+  @param use_external_mip_script: whether to use an external master IP
+    address setup script
+  @raise RPCFail: in case of errors during the IP turndown
+
+  """
+  _RunMasterSetupScript(master_params, _MASTER_STOP,
+                        use_external_mip_script)
 
 
-def StopMaster(stop_daemons):
-  """Deactivate this node as master.
+def StopMasterDaemons():
+  """Stop the master daemons on this node.
 
-  The function will always try to deactivate the IP address of the
-  master. It will also stop the master daemons depending on the
-  stop_daemons parameter.
+  Stop the master daemons (ganeti-masterd and ganeti-rapi) on this node.
 
-  @type stop_daemons: boolean
-  @param stop_daemons: whether to also stop the master daemons
-      (ganeti-masterd and ganeti-rapi)
   @rtype: None
 
   """
   # TODO: log and report back to the caller the error failures; we
   # need to decide in which case we fail the RPC for this
 
-  # GetMasterInfo will raise an exception if not able to return data
-  master_netdev, master_ip, _, family = GetMasterInfo()
+  result = utils.RunCmd([constants.DAEMON_UTIL, "stop-master"])
+  if result.failed:
+    logging.error("Could not stop Ganeti master, command %s had exitcode %s"
+                  " and error %s",
+                  result.cmd, result.exit_code, result.output)
 
-  ipcls = netutils.IP4Address
-  if family == netutils.IP6Address.family:
-    ipcls = netutils.IP6Address
 
-  result = utils.RunCmd([constants.IP_COMMAND_PATH, "address", "del",
-                         "%s/%d" % (master_ip, ipcls.iplen),
-                         "dev", master_netdev])
+def ChangeMasterNetmask(old_netmask, netmask, master_ip, master_netdev):
+  """Change the netmask of the master IP.
+
+  @param old_netmask: the old value of the netmask
+  @param netmask: the new value of the netmask
+  @param master_ip: the master IP
+  @param master_netdev: the master network device
+
+  """
+  if old_netmask == netmask:
+    return
+
+  if not netutils.IPAddress.Own(master_ip):
+    _Fail("The master IP address is not up, not attempting to change its"
+          " netmask")
+
+  result = utils.RunCmd([constants.IP_COMMAND_PATH, "address", "add",
+                         "%s/%s" % (master_ip, netmask),
+                         "dev", master_netdev, "label",
+                         "%s:0" % master_netdev])
   if result.failed:
-    logging.error("Can't remove the master IP, error: %s", result.output)
-    # but otherwise ignore the failure
+    _Fail("Could not set the new netmask on the master IP address")
 
-  if stop_daemons:
-    result = utils.RunCmd([constants.DAEMON_UTIL, "stop-master"])
-    if result.failed:
-      logging.error("Could not stop Ganeti master, command %s had exitcode %s"
-                    " and error %s",
-                    result.cmd, result.exit_code, result.output)
+  result = utils.RunCmd([constants.IP_COMMAND_PATH, "address", "del",
+                         "%s/%s" % (master_ip, old_netmask),
+                         "dev", master_netdev, "label",
+                         "%s:0" % master_netdev])
+  if result.failed:
+    _Fail("Could not bring down the master IP address with the old netmask")
 
 
 def EtcHostsModify(mode, host, ip):
@@ -411,6 +507,8 @@ def LeaveCluster(modify_ssh_setup):
   try:
     utils.RemoveFile(constants.CONFD_HMAC_KEY)
     utils.RemoveFile(constants.RAPI_CERT_FILE)
+    utils.RemoveFile(constants.SPICE_CERT_FILE)
+    utils.RemoveFile(constants.SPICE_CACERT_FILE)
     utils.RemoveFile(constants.NODED_CERT_FILE)
   except: # pylint: disable=W0702
     logging.exception("Error while removing cluster secrets")
@@ -424,43 +522,71 @@ def LeaveCluster(modify_ssh_setup):
   raise errors.QuitGanetiException(True, "Shutdown scheduled")
 
 
-def GetNodeInfo(vgname, hypervisor_type):
-  """Gives back a hash with different information about the node.
+def _GetVgInfo(name):
+  """Retrieves information about a LVM volume group.
 
-  @type vgname: C{string}
-  @param vgname: the name of the volume group to ask for disk space information
-  @type hypervisor_type: C{str}
-  @param hypervisor_type: the name of the hypervisor to ask for
-      memory information
-  @rtype: C{dict}
-  @return: dictionary with the following keys:
-      - vg_size is the size of the configured volume group in MiB
-      - vg_free is the free size of the volume group in MiB
-      - memory_dom0 is the memory allocated for domain0 in MiB
-      - memory_free is the currently available (free) ram in MiB
-      - memory_total is the total number of ram in MiB
+  """
+  # TODO: GetVGInfo supports returning information for multiple VGs at once
+  vginfo = bdev.LogicalVolume.GetVGInfo([name])
+  if vginfo:
+    vg_free = int(round(vginfo[0][0], 0))
+    vg_size = int(round(vginfo[0][1], 0))
+  else:
+    vg_free = None
+    vg_size = None
+
+  return {
+    "name": name,
+    "vg_free": vg_free,
+    "vg_size": vg_size,
+    }
+
+
+def _GetHvInfo(name):
+  """Retrieves node information from a hypervisor.
+
+  The information returned depends on the hypervisor. Common items:
+
+    - vg_size is the size of the configured volume group in MiB
+    - vg_free is the free size of the volume group in MiB
+    - memory_dom0 is the memory allocated for domain0 in MiB
+    - memory_free is the currently available (free) ram in MiB
+    - memory_total is the total number of ram in MiB
+    - hv_version: the hypervisor version, if available
 
   """
-  outputarray = {}
+  return hypervisor.GetHypervisor(name).GetNodeInfo()
+
+
+def _GetNamedNodeInfo(names, fn):
+  """Calls C{fn} for all names in C{names} and returns a dictionary.
 
-  if vgname is not None:
-    vginfo = bdev.LogicalVolume.GetVGInfo([vgname])
-    vg_free = vg_size = None
-    if vginfo:
-      vg_free = int(round(vginfo[0][0], 0))
-      vg_size = int(round(vginfo[0][1], 0))
-    outputarray["vg_size"] = vg_size
-    outputarray["vg_free"] = vg_free
+  @rtype: None or dict
+
+  """
+  if names is None:
+    return None
+  else:
+    return map(fn, names)
+
+
+def GetNodeInfo(vg_names, hv_names):
+  """Gives back a hash with different information about the node.
 
-  if hypervisor_type is not None:
-    hyper = hypervisor.GetHypervisor(hypervisor_type)
-    hyp_info = hyper.GetNodeInfo()
-    if hyp_info is not None:
-      outputarray.update(hyp_info)
+  @type vg_names: list of string
+  @param vg_names: Names of the volume groups to ask for disk space information
+  @type hv_names: list of string
+  @param hv_names: Names of the hypervisors to ask for node information
+  @rtype: tuple; (string, None/dict, None/dict)
+  @return: Tuple containing boot ID, volume group information and hypervisor
+    information
 
-  outputarray["bootid"] = utils.ReadFile(_BOOT_ID_PATH, size=128).rstrip("\n")
+  """
+  bootid = utils.ReadFile(_BOOT_ID_PATH, size=128).rstrip("\n")
+  vg_info = _GetNamedNodeInfo(vg_names, _GetVgInfo)
+  hv_info = _GetNamedNodeInfo(hv_names, _GetHvInfo)
 
-  return outputarray
+  return (bootid, vg_info, hv_info)
 
 
 def VerifyNode(what, cluster_name):
@@ -574,6 +700,11 @@ def VerifyNode(what, cluster_name):
     result[constants.NV_MASTERIP] = netutils.TcpPing(master_ip, port,
                                                   source=source)
 
+  if constants.NV_USERSCRIPTS in what:
+    result[constants.NV_USERSCRIPTS] = \
+      [script for script in what[constants.NV_USERSCRIPTS]
+       if not (os.path.exists(script) and os.access(script, os.X_OK))]
+
   if constants.NV_OOB_PATHS in what:
     result[constants.NV_OOB_PATHS] = tmp = []
     for path in what[constants.NV_OOB_PATHS]:
@@ -681,7 +812,7 @@ def GetBlockDevSizes(devices):
   blockdevs = {}
 
   for devpath in devices:
-    if os.path.commonprefix([DEV_PREFIX, devpath]) != DEV_PREFIX:
+    if not utils.IsBelowDir(DEV_PREFIX, devpath):
       continue
 
     try:
@@ -1245,6 +1376,27 @@ def InstanceReboot(instance, reboot_type, shutdown_timeout):
     _Fail("Invalid reboot_type received: %s", reboot_type)
 
 
+def InstanceBalloonMemory(instance, memory):
+  """Resize an instance's memory.
+
+  @type instance: L{objects.Instance}
+  @param instance: the instance object
+  @type memory: int
+  @param memory: new memory amount in MB
+  @rtype: None
+
+  """
+  hyper = hypervisor.GetHypervisor(instance.hypervisor)
+  running = hyper.ListInstances()
+  if instance.name not in running:
+    logging.info("Instance %s is not running, cannot balloon", instance.name)
+    return
+  try:
+    hyper.BalloonInstanceMemory(instance, memory)
+  except errors.HypervisorError, err:
+    _Fail("Failed to balloon instance memory: %s", err, exc=True)
+
+
 def MigrationInfo(instance):
   """Gather information about an instance to be migrated.
 
@@ -1289,7 +1441,7 @@ def AcceptInstance(instance, info, target):
     _Fail("Failed to accept instance: %s", err, exc=True)
 
 
-def FinalizeMigration(instance, info, success):
+def FinalizeMigrationDst(instance, info, success):
   """Finalize any preparation to accept an instance.
 
   @type instance: L{objects.Instance}
@@ -1302,9 +1454,9 @@ def FinalizeMigration(instance, info, success):
   """
   hyper = hypervisor.GetHypervisor(instance.hypervisor)
   try:
-    hyper.FinalizeMigration(instance, info, success)
+    hyper.FinalizeMigrationDst(instance, info, success)
   except errors.HypervisorError, err:
-    _Fail("Failed to finalize migration: %s", err, exc=True)
+    _Fail("Failed to finalize migration on the target node: %s", err, exc=True)
 
 
 def MigrateInstance(instance, target, live):
@@ -1317,10 +1469,7 @@ def MigrateInstance(instance, target, live):
   @type live: boolean
   @param live: whether the migration should be done live or not (the
       interpretation of this parameter is left to the hypervisor)
-  @rtype: tuple
-  @return: a tuple of (success, msg) where:
-      - succes is a boolean denoting the success/failure of the operation
-      - msg is a string with details in case of failure
+  @raise RPCFail: if migration fails for some reason
 
   """
   hyper = hypervisor.GetHypervisor(instance.hypervisor)
@@ -1331,6 +1480,46 @@ def MigrateInstance(instance, target, live):
     _Fail("Failed to migrate instance: %s", err, exc=True)
 
 
+def FinalizeMigrationSource(instance, success, live):
+  """Finalize the instance migration on the source node.
+
+  @type instance: L{objects.Instance}
+  @param instance: the instance definition of the migrated instance
+  @type success: bool
+  @param success: whether the migration succeeded or not
+  @type live: bool
+  @param live: whether the user requested a live migration or not
+  @raise RPCFail: If the execution fails for some reason
+
+  """
+  hyper = hypervisor.GetHypervisor(instance.hypervisor)
+
+  try:
+    hyper.FinalizeMigrationSource(instance, success, live)
+  except Exception, err:  # pylint: disable=W0703
+    _Fail("Failed to finalize the migration on the source node: %s", err,
+          exc=True)
+
+
+def GetMigrationStatus(instance):
+  """Get the migration status
+
+  @type instance: L{objects.Instance}
+  @param instance: the instance that is being migrated
+  @rtype: L{objects.MigrationStatus}
+  @return: the status of the current migration (one of
+           L{constants.HV_MIGRATION_VALID_STATUSES}), plus any additional
+           progress info that can be retrieved from the hypervisor
+  @raise RPCFail: If the migration status cannot be retrieved
+
+  """
+  hyper = hypervisor.GetHypervisor(instance.hypervisor)
+  try:
+    return hyper.GetMigrationStatus(instance)
+  except Exception, err:  # pylint: disable=W0703
+    _Fail("Failed to get migration status: %s", err, exc=True)
+
+
 def BlockdevCreate(disk, size, owner, on_primary, info):
   """Creates a block device for an instance.
 
@@ -1372,7 +1561,7 @@ def BlockdevCreate(disk, size, owner, on_primary, info):
       clist.append(crdev)
 
   try:
-    device = bdev.Create(disk.dev_type, disk.physical_id, clist, disk.size)
+    device = bdev.Create(disk, clist)
   except errors.BlockDeviceError, err:
     _Fail("Can't create block device: %s", err)
 
@@ -1381,7 +1570,6 @@ def BlockdevCreate(disk, size, owner, on_primary, info):
       device.Assemble()
     except errors.BlockDeviceError, err:
       _Fail("Can't assemble device after creation, unusual event: %s", err)
-    device.SetSyncSpeed(constants.SYNC_SPEED)
     if on_primary or disk.OpenOnSecondary():
       try:
         device.Open(force=True)
@@ -1555,8 +1743,7 @@ def _RecursiveAssembleBD(disk, owner, as_primary):
       children.append(cdev)
 
   if as_primary or disk.AssembleOnSecondary():
-    r_dev = bdev.Assemble(disk.dev_type, disk.physical_id, children, disk.size)
-    r_dev.SetSyncSpeed(constants.SYNC_SPEED)
+    r_dev = bdev.Assemble(disk, children)
     result = r_dev
     if as_primary or disk.OpenOnSecondary():
       r_dev.Open()
@@ -1749,7 +1936,7 @@ def _RecursiveFindBD(disk):
     for chdisk in disk.children:
       children.append(_RecursiveFindBD(chdisk))
 
-  return bdev.FindDevice(disk.dev_type, disk.physical_id, children, disk.size)
+  return bdev.FindDevice(disk, children)
 
 
 def _OpenRealBD(disk):
@@ -1935,24 +2122,6 @@ def WriteSsconfFiles(values):
   ssconf.SimpleStore().WriteFiles(values)
 
 
-def _ErrnoOrStr(err):
-  """Format an EnvironmentError exception.
-
-  If the L{err} argument has an errno attribute, it will be looked up
-  and converted into a textual C{E...} description. Otherwise the
-  string representation of the error will be returned.
-
-  @type err: L{EnvironmentError}
-  @param err: the exception to format
-
-  """
-  if hasattr(err, "errno"):
-    detail = errno.errorcode[err.errno]
-  else:
-    detail = str(err)
-  return detail
-
-
 def _OSOndiskAPIVersion(os_dir):
   """Compute and return the API version of a given OS.
 
@@ -1972,7 +2141,7 @@ def _OSOndiskAPIVersion(os_dir):
     st = os.stat(api_file)
   except EnvironmentError, err:
     return False, ("Required file '%s' not found under path %s: %s" %
-                   (constants.OS_API_FILE, os_dir, _ErrnoOrStr(err)))
+                   (constants.OS_API_FILE, os_dir, utils.ErrnoOrStr(err)))
 
   if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
     return False, ("File '%s' in %s is not a regular file" %
@@ -1982,7 +2151,7 @@ def _OSOndiskAPIVersion(os_dir):
     api_versions = utils.ReadFile(api_file).splitlines()
   except EnvironmentError, err:
     return False, ("Error while reading the API version file at %s: %s" %
-                   (api_file, _ErrnoOrStr(err)))
+                   (api_file, utils.ErrnoOrStr(err)))
 
   try:
     api_versions = [int(version.strip()) for version in api_versions]
@@ -2095,7 +2264,7 @@ def _TryOSFromDisk(name, base_dir=None):
         del os_files[filename]
         continue
       return False, ("File '%s' under path '%s' is missing (%s)" %
-                     (filename, os_dir, _ErrnoOrStr(err)))
+                     (filename, os_dir, utils.ErrnoOrStr(err)))
 
     if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
       return False, ("File '%s' under path '%s' is not a regular file" %
@@ -2115,7 +2284,7 @@ def _TryOSFromDisk(name, base_dir=None):
       # we accept missing files, but not other errors
       if err.errno != errno.ENOENT:
         return False, ("Error while reading the OS variants file at %s: %s" %
-                       (variants_file, _ErrnoOrStr(err)))
+                       (variants_file, utils.ErrnoOrStr(err)))
 
   parameters = []
   if constants.OS_PARAMETERS_FILE in os_files:
@@ -2124,7 +2293,7 @@ def _TryOSFromDisk(name, base_dir=None):
       parameters = utils.ReadFile(parameters_file).splitlines()
     except EnvironmentError, err:
       return False, ("Error while reading the OS parameters file at %s: %s" %
-                     (parameters_file, _ErrnoOrStr(err)))
+                     (parameters_file, utils.ErrnoOrStr(err)))
     parameters = [v.split(None, 1) for v in parameters]
 
   os_obj = objects.OS(name=name, path=os_dir,
@@ -2359,8 +2528,13 @@ def FinalizeExport(instance, snap_disks):
 
   config.add_section(constants.INISECT_INS)
   config.set(constants.INISECT_INS, "name", instance.name)
+  config.set(constants.INISECT_INS, "maxmem", "%d" %
+             instance.beparams[constants.BE_MAXMEM])
+  config.set(constants.INISECT_INS, "minmem", "%d" %
+             instance.beparams[constants.BE_MINMEM])
+  # "memory" is deprecated, but useful for exporting to old ganeti versions
   config.set(constants.INISECT_INS, "memory", "%d" %
-             instance.beparams[constants.BE_MEMORY])
+             instance.beparams[constants.BE_MAXMEM])
   config.set(constants.INISECT_INS, "vcpus", "%d" %
              instance.beparams[constants.BE_VCPUS])
   config.set(constants.INISECT_INS, "disk_template", instance.disk_template)
@@ -2525,8 +2699,8 @@ def _TransformFileStorageDir(fs_dir):
   fs_dir = os.path.normpath(fs_dir)
   base_fstore = cfg.GetFileStorageDir()
   base_shared = cfg.GetSharedFileStorageDir()
-  if ((os.path.commonprefix([fs_dir, base_fstore]) != base_fstore) and
-      (os.path.commonprefix([fs_dir, base_shared]) != base_shared)):
+  if not (utils.IsBelowDir(base_fstore, fs_dir) or
+          utils.IsBelowDir(base_shared, fs_dir)):
     _Fail("File storage directory '%s' is not under base file"
           " storage directory '%s' or shared storage directory '%s'",
           fs_dir, base_fstore, base_shared)
@@ -2896,12 +3070,12 @@ def _GetImportExportIoCommand(instance, mode, ieio, ieargs):
     if not utils.IsNormAbsPath(filename):
       _Fail("Path '%s' is not normalized or absolute", filename)
 
-    directory = os.path.normpath(os.path.dirname(filename))
+    real_filename = os.path.realpath(filename)
+    directory = os.path.dirname(real_filename)
 
-    if (os.path.commonprefix([constants.EXPORT_DIR, directory]) !=
-        constants.EXPORT_DIR):
-      _Fail("File '%s' is not under exports directory '%s'",
-            filename, constants.EXPORT_DIR)
+    if not utils.IsBelowDir(constants.EXPORT_DIR, real_filename):
+      _Fail("File '%s' is not under exports directory '%s': %s",
+            filename, constants.EXPORT_DIR, real_filename)
 
     # Create directory
     utils.Makedirs(directory, mode=0750)
@@ -3370,6 +3544,20 @@ class HooksRunner(object):
     # constant
     self._BASE_DIR = hooks_base_dir # pylint: disable=C0103
 
+  def RunLocalHooks(self, node_list, hpath, phase, env):
+    """Check that the hooks will be run only locally and then run them.
+
+    """
+    assert len(node_list) == 1
+    node = node_list[0]
+    _, myself = ssconf.GetMasterAndMyself()
+    assert node == myself
+
+    results = self.RunHooks(hpath, phase, env)
+
+    # Return values in the form expected by HooksMaster
+    return {node: (None, False, results)}
+
   def RunHooks(self, hpath, phase, env):
     """Run the scripts in the hooks directory.
 
index 6e37479..1d85867 100644 (file)
@@ -24,6 +24,7 @@
 import re
 import time
 import errno
+import shlex
 import stat
 import pyparsing as pyp
 import os
@@ -130,7 +131,7 @@ class BlockDev(object):
   after assembly we'll have our correct major/minor.
 
   """
-  def __init__(self, unique_id, children, size):
+  def __init__(self, unique_id, children, size, params):
     self._children = children
     self.dev_path = None
     self.unique_id = unique_id
@@ -138,6 +139,7 @@ class BlockDev(object):
     self.minor = None
     self.attached = False
     self.size = size
+    self.params = params
 
   def Assemble(self):
     """Assemble the device from its components.
@@ -166,7 +168,7 @@ class BlockDev(object):
     raise NotImplementedError
 
   @classmethod
-  def Create(cls, unique_id, children, size):
+  def Create(cls, unique_id, children, size, params):
     """Create the device.
 
     If the device cannot be created, it will return None
@@ -219,16 +221,22 @@ class BlockDev(object):
     """
     raise NotImplementedError
 
-  def SetSyncSpeed(self, speed):
-    """Adjust the sync speed of the mirror.
+  def SetSyncParams(self, params):
+    """Adjust the synchronization parameters of the mirror.
 
     In case this is not a mirroring device, this is no-op.
 
+    @param params: dictionary of LD level disk parameters related to the
+    synchronization.
+    @rtype: list
+    @return: a list of error messages, emitted both by the current node and by
+    children. An empty list means no errors.
+
     """
-    result = True
+    result = []
     if self._children:
       for child in self._children:
-        result = result and child.SetSyncSpeed(speed)
+        result.extend(child.SetSyncParams(params))
     return result
 
   def PauseResumeSync(self, pause):
@@ -236,7 +244,7 @@ class BlockDev(object):
 
     In case this is not a mirroring device, this is no-op.
 
-    @param pause: Wheater to pause or resume
+    @param pause: Whether to pause or resume
 
     """
     result = True
@@ -373,13 +381,13 @@ class LogicalVolume(BlockDev):
   _INVALID_NAMES = frozenset([".", "..", "snapshot", "pvmove"])
   _INVALID_SUBSTRINGS = frozenset(["_mlog", "_mimage"])
 
-  def __init__(self, unique_id, children, size):
+  def __init__(self, unique_id, children, size, params):
     """Attaches to a LV device.
 
     The unique_id is a tuple (vg_name, lv_name)
 
     """
-    super(LogicalVolume, self).__init__(unique_id, children, size)
+    super(LogicalVolume, self).__init__(unique_id, children, size, params)
     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
       raise ValueError("Invalid configuration data %s" % str(unique_id))
     self._vg_name, self._lv_name = unique_id
@@ -391,7 +399,7 @@ class LogicalVolume(BlockDev):
     self.Attach()
 
   @classmethod
-  def Create(cls, unique_id, children, size):
+  def Create(cls, unique_id, children, size, params):
     """Create a new logical volume.
 
     """
@@ -414,7 +422,11 @@ class LogicalVolume(BlockDev):
                   " in lvm.conf using either 'filter' or 'preferred_names'")
     free_size = sum([pv[0] for pv in pvs_info])
     current_pvs = len(pvlist)
-    stripes = min(current_pvs, constants.LVM_STRIPECOUNT)
+    desired_stripes = params[constants.LDP_STRIPES]
+    stripes = min(current_pvs, desired_stripes)
+    if stripes < desired_stripes:
+      logging.warning("Could not use %d stripes for VG %s, as only %d PVs are"
+                      " available.", desired_stripes, vg_name, current_pvs)
 
     # The size constraint should have been checked from the master before
     # calling the create function.
@@ -433,7 +445,7 @@ class LogicalVolume(BlockDev):
     if result.failed:
       _ThrowError("LV create failed (%s): %s",
                   result.fail_reason, result.output)
-    return LogicalVolume(unique_id, children, size)
+    return LogicalVolume(unique_id, children, size, params)
 
   @staticmethod
   def _GetVolumeInfo(lvm_cmd, fields):
@@ -718,7 +730,7 @@ class LogicalVolume(BlockDev):
     snap_name = self._lv_name + ".snap"
 
     # remove existing snapshot if found
-    snap = LogicalVolume((self._vg_name, snap_name), None, size)
+    snap = LogicalVolume((self._vg_name, snap_name), None, size, self.params)
     _IgnoreError(snap.Remove)
 
     vg_info = self.GetVGInfo([self._vg_name])
@@ -1046,7 +1058,7 @@ class BaseDRBD(BlockDev): # pylint: disable=W0223
   def _CheckMetaSize(meta_device):
     """Check if the given meta device looks like a valid one.
 
-    This currently only check the size, which must be around
+    This currently only checks the size, which must be around
     128MiB.
 
     """
@@ -1086,10 +1098,10 @@ class DRBD8(BaseDRBD):
   doesn't do anything to the supposed peer. If you need a fully
   connected DRBD pair, you need to use this class on both hosts.
 
-  The unique_id for the drbd device is the (local_ip, local_port,
-  remote_ip, remote_port) tuple, and it must have two children: the
-  data device and the meta_device. The meta device is checked for
-  valid size and is zeroed on create.
+  The unique_id for the drbd device is a (local_ip, local_port,
+  remote_ip, remote_port, local_minor, secret) tuple, and it must have
+  two children: the data device and the meta_device. The meta device
+  is checked for valid size and is zeroed on create.
 
   """
   _MAX_MINORS = 255
@@ -1098,7 +1110,13 @@ class DRBD8(BaseDRBD):
   # timeout constants
   _NET_RECONFIG_TIMEOUT = 60
 
-  def __init__(self, unique_id, children, size):
+  # command line options for barriers
+  _DISABLE_DISK_OPTION = "--no-disk-barrier"  # -a
+  _DISABLE_DRAIN_OPTION = "--no-disk-drain"   # -D
+  _DISABLE_FLUSH_OPTION = "--no-disk-flushes" # -i
+  _DISABLE_META_FLUSH_OPTION = "--no-md-flushes"  # -m
+
+  def __init__(self, unique_id, children, size, params):
     if children and children.count(None) > 0:
       children = []
     if len(children) not in (0, 2):
@@ -1112,7 +1130,7 @@ class DRBD8(BaseDRBD):
       if not _CanReadDevice(children[1].dev_path):
         logging.info("drbd%s: Ignoring unreadable meta device", self._aminor)
         children = []
-    super(DRBD8, self).__init__(unique_id, children, size)
+    super(DRBD8, self).__init__(unique_id, children, size, params)
     self.major = self._DRBD_MAJOR
     version = self._GetVersion(self._GetProcData())
     if version["k_major"] != 8:
@@ -1179,7 +1197,7 @@ class DRBD8(BaseDRBD):
   def _GetShowParser(cls):
     """Return a parser for `drbd show` output.
 
-    This will either create or return an already-create parser for the
+    This will either create or return an already-created parser for the
     output of the command `drbd show`.
 
     """
@@ -1200,10 +1218,10 @@ class DRBD8(BaseDRBD):
     defa = pyp.Literal("_is_default").suppress()
     dbl_quote = pyp.Literal('"').suppress()
 
-    keyword = pyp.Word(pyp.alphanums + '-')
+    keyword = pyp.Word(pyp.alphanums + "-")
 
     # value types
-    value = pyp.Word(pyp.alphanums + '_-/.:')
+    value = pyp.Word(pyp.alphanums + "_-/.:")
     quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
     ipv4_addr = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
                  pyp.Word(pyp.nums + ".") + colon + number)
@@ -1339,41 +1357,110 @@ class DRBD8(BaseDRBD):
               info["remote_addr"] == (self._rhost, self._rport))
     return retval
 
-  @classmethod
-  def _AssembleLocal(cls, minor, backend, meta, size):
+  def _AssembleLocal(self, minor, backend, meta, size):
     """Configure the local part of a DRBD device.
 
     """
-    args = ["drbdsetup", cls._DevPath(minor), "disk",
+    args = ["drbdsetup", self._DevPath(minor), "disk",
             backend, meta, "0",
             "-e", "detach",
             "--create-device"]
     if size:
       args.extend(["-d", "%sm" % size])
-    if not constants.DRBD_BARRIERS: # disable barriers, if configured so
-      version = cls._GetVersion(cls._GetProcData())
-      # various DRBD versions support different disk barrier options;
-      # what we aim here is to revert back to the 'drain' method of
-      # disk flushes and to disable metadata barriers, in effect going
-      # back to pre-8.0.7 behaviour
-      vmaj = version["k_major"]
-      vmin = version["k_minor"]
-      vrel = version["k_point"]
-      assert vmaj == 8
-      if vmin == 0: # 8.0.x
-        if vrel >= 12:
-          args.extend(["-i", "-m"])
-      elif vmin == 2: # 8.2.x
-        if vrel >= 7:
-          args.extend(["-i", "-m"])
-      elif vmaj >= 3: # 8.3.x or newer
-        args.extend(["-i", "-a", "m"])
+
+    version = self._GetVersion(self._GetProcData())
+    vmaj = version["k_major"]
+    vmin = version["k_minor"]
+    vrel = version["k_point"]
+
+    barrier_args = \
+      self._ComputeDiskBarrierArgs(vmaj, vmin, vrel,
+                                   self.params[constants.LDP_BARRIERS],
+                                   self.params[constants.LDP_NO_META_FLUSH])
+    args.extend(barrier_args)
+
+    if self.params[constants.LDP_DISK_CUSTOM]:
+      args.extend(shlex.split(self.params[constants.LDP_DISK_CUSTOM]))
+
     result = utils.RunCmd(args)
     if result.failed:
       _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
 
   @classmethod
-  def _AssembleNet(cls, minor, net_info, protocol,
+  def _ComputeDiskBarrierArgs(cls, vmaj, vmin, vrel, disabled_barriers,
+      disable_meta_flush):
+    """Compute the DRBD command line parameters for disk barriers
+
+    Returns a list of the disk barrier parameters as requested via the
+    disabled_barriers and disable_meta_flush arguments, and according to the
+    supported ones in the DRBD version vmaj.vmin.vrel
+
+    If the desired option is unsupported, raises errors.BlockDeviceError.
+
+    """
+    disabled_barriers_set = frozenset(disabled_barriers)
+    if not disabled_barriers_set in constants.DRBD_VALID_BARRIER_OPT:
+      raise errors.BlockDeviceError("%s is not a valid option set for DRBD"
+                                    " barriers" % disabled_barriers)
+
+    args = []
+
+    # The following code assumes DRBD 8.x, with x < 4 and x != 1 (DRBD 8.1.x
+    # does not exist)
+    if not vmaj == 8 and vmin in (0, 2, 3):
+      raise errors.BlockDeviceError("Unsupported DRBD version: %d.%d.%d" %
+                                    (vmaj, vmin, vrel))
+
+    def _AppendOrRaise(option, min_version):
+      """Helper for DRBD options"""
+      if min_version is not None and vrel >= min_version:
+        args.append(option)
+      else:
+        raise errors.BlockDeviceError("Could not use the option %s as the"
+                                      " DRBD version %d.%d.%d does not support"
+                                      " it." % (option, vmaj, vmin, vrel))
+
+    # the minimum version for each feature is encoded via pairs of (minor
+    # version -> x) where x is version in which support for the option was
+    # introduced.
+    meta_flush_supported = disk_flush_supported = {
+      0: 12,
+      2: 7,
+      3: 0,
+      }
+
+    disk_drain_supported = {
+      2: 7,
+      3: 0,
+      }
+
+    disk_barriers_supported = {
+      3: 0,
+      }
+
+    # meta flushes
+    if disable_meta_flush:
+      _AppendOrRaise(cls._DISABLE_META_FLUSH_OPTION,
+                     meta_flush_supported.get(vmin, None))
+
+    # disk flushes
+    if constants.DRBD_B_DISK_FLUSH in disabled_barriers_set:
+      _AppendOrRaise(cls._DISABLE_FLUSH_OPTION,
+                     disk_flush_supported.get(vmin, None))
+
+    # disk drain
+    if constants.DRBD_B_DISK_DRAIN in disabled_barriers_set:
+      _AppendOrRaise(cls._DISABLE_DRAIN_OPTION,
+                     disk_drain_supported.get(vmin, None))
+
+    # disk barriers
+    if constants.DRBD_B_DISK_BARRIERS in disabled_barriers_set:
+      _AppendOrRaise(cls._DISABLE_DISK_OPTION,
+                     disk_barriers_supported.get(vmin, None))
+
+    return args
+
+  def _AssembleNet(self, minor, net_info, protocol,
                    dual_pri=False, hmac=None, secret=None):
     """Configure the network part of the device.
 
@@ -1382,7 +1469,7 @@ class DRBD8(BaseDRBD):
     if None in net_info:
       # we don't want network connection and actually want to make
       # sure its shutdown
-      cls._ShutdownNet(minor)
+      self._ShutdownNet(minor)
       return
 
     # Workaround for a race condition. When DRBD is doing its dance to
@@ -1391,7 +1478,10 @@ class DRBD8(BaseDRBD):
     # sync speed only after setting up both sides can race with DRBD
     # connecting, hence we set it here before telling DRBD anything
     # about its peer.
-    cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED)
+    sync_errors = self._SetMinorSyncParams(minor, self.params)
+    if sync_errors:
+      _ThrowError("drbd%d: can't set the synchronization parameters: %s" %
+                  (minor, utils.CommaJoin(sync_errors)))
 
     if netutils.IP6Address.IsValid(lhost):
       if not netutils.IP6Address.IsValid(rhost):
@@ -1406,7 +1496,7 @@ class DRBD8(BaseDRBD):
     else:
       _ThrowError("drbd%d: Invalid ip %s" % (minor, lhost))
 
-    args = ["drbdsetup", cls._DevPath(minor), "net",
+    args = ["drbdsetup", self._DevPath(minor), "net",
             "%s:%s:%s" % (family, lhost, lport),
             "%s:%s:%s" % (family, rhost, rport), protocol,
             "-A", "discard-zero-changes",
@@ -1417,13 +1507,17 @@ class DRBD8(BaseDRBD):
       args.append("-m")
     if hmac and secret:
       args.extend(["-a", hmac, "-x", secret])
+
+    if self.params[constants.LDP_NET_CUSTOM]:
+      args.extend(shlex.split(self.params[constants.LDP_NET_CUSTOM]))
+
     result = utils.RunCmd(args)
     if result.failed:
       _ThrowError("drbd%d: can't setup network: %s - %s",
                   minor, result.fail_reason, result.output)
 
     def _CheckNetworkConfig():
-      info = cls._GetDevInfo(cls._GetShowData(minor))
+      info = self._GetDevInfo(self._GetShowData(minor))
       if not "local_addr" in info or not "remote_addr" in info:
         raise utils.RetryAgain()
 
@@ -1487,40 +1581,80 @@ class DRBD8(BaseDRBD):
     self._children = []
 
   @classmethod
-  def _SetMinorSyncSpeed(cls, minor, kbytes):
-    """Set the speed of the DRBD syncer.
+  def _SetMinorSyncParams(cls, minor, params):
+    """Set the parameters of the DRBD syncer.
 
     This is the low-level implementation.
 
     @type minor: int
     @param minor: the drbd minor whose settings we change
-    @type kbytes: int
-    @param kbytes: the speed in kbytes/second
-    @rtype: boolean
-    @return: the success of the operation
+    @type params: dict
+    @param params: LD level disk parameters related to the synchronization
+    @rtype: list
+    @return: a list of error messages
 
     """
-    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
-                           "-r", "%d" % kbytes, "--create-device"])
+
+    args = ["drbdsetup", cls._DevPath(minor), "syncer"]
+    if params[constants.LDP_DYNAMIC_RESYNC]:
+      version = cls._GetVersion(cls._GetProcData())
+      vmin = version["k_minor"]
+      vrel = version["k_point"]
+
+      # By definition we are using 8.x, so just check the rest of the version
+      # number
+      if vmin != 3 or vrel < 9:
+        msg = ("The current DRBD version (8.%d.%d) does not support the "
+               "dynamic resync speed controller" % (vmin, vrel))
+        logging.error(msg)
+        return [msg]
+
+      if params[constants.LDP_PLAN_AHEAD] == 0:
+        msg = ("A value of 0 for c-plan-ahead disables the dynamic sync speed"
+               " controller at DRBD level. If you want to disable it, please"
+               " set the dynamic-resync disk parameter to False.")
+        logging.error(msg)
+        return [msg]
+
+      # add the c-* parameters to args
+      args.extend(["--c-plan-ahead", params[constants.LDP_PLAN_AHEAD],
+                   "--c-fill-target", params[constants.LDP_FILL_TARGET],
+                   "--c-delay-target", params[constants.LDP_DELAY_TARGET],
+                   "--c-max-rate", params[constants.LDP_MAX_RATE],
+                   "--c-min-rate", params[constants.LDP_MIN_RATE],
+                  ])
+
+    else:
+      args.extend(["-r", "%d" % params[constants.LDP_RESYNC_RATE]])
+
+    args.append("--create-device")
+    result = utils.RunCmd(args)
     if result.failed:
-      logging.error("Can't change syncer rate: %s - %s",
-                    result.fail_reason, result.output)
-    return not result.failed
+      msg = ("Can't change syncer rate: %s - %s" %
+             (result.fail_reason, result.output))
+      logging.error(msg)
+      return [msg]
 
-  def SetSyncSpeed(self, kbytes):
-    """Set the speed of the DRBD syncer.
+    return []
 
-    @type kbytes: int
-    @param kbytes: the speed in kbytes/second
-    @rtype: boolean
-    @return: the success of the operation
+  def SetSyncParams(self, params):
+    """Set the synchronization parameters of the DRBD syncer.
+
+    @type params: dict
+    @param params: LD level disk parameters related to the synchronization
+    @rtype: list
+    @return: a list of error messages, emitted both by the current node and by
+    children. An empty list means no errors
 
     """
     if self.minor is None:
-      logging.info("Not attached during SetSyncSpeed")
-      return False
-    children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
-    return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
+      err = "Not attached during SetSyncParams"
+      logging.info(err)
+      return [err]
+
+    children_result = super(DRBD8, self).SetSyncParams(params)
+    children_result.extend(self._SetMinorSyncParams(self.minor, params))
+    return children_result
 
   def PauseResumeSync(self, pause):
     """Pauses or resumes the sync of a DRBD device.
@@ -1743,6 +1877,7 @@ class DRBD8(BaseDRBD):
       - if we have a configured device, we try to ensure that it matches
         our config
       - if not, we create it from zero
+      - anyway, set the device parameters
 
     """
     super(DRBD8, self).Assemble()
@@ -1756,6 +1891,11 @@ class DRBD8(BaseDRBD):
       # the device
       self._SlowAssemble()
 
+    sync_errors = self.SetSyncParams(self.params)
+    if sync_errors:
+      _ThrowError("drbd%d: can't set the synchronization parameters: %s" %
+                  (self.minor, utils.CommaJoin(sync_errors)))
+
   def _SlowAssemble(self):
     """Assembles the DRBD device from a (partially) configured device.
 
@@ -1902,7 +2042,7 @@ class DRBD8(BaseDRBD):
     self.Shutdown()
 
   @classmethod
-  def Create(cls, unique_id, children, size):
+  def Create(cls, unique_id, children, size, params):
     """Create a new DRBD8 device.
 
     Since DRBD devices are not created per se, just assembled, this
@@ -1928,7 +2068,7 @@ class DRBD8(BaseDRBD):
                   aminor, meta)
     cls._CheckMetaSize(meta.dev_path)
     cls._InitMeta(aminor, meta.dev_path)
-    return cls(unique_id, children, size)
+    return cls(unique_id, children, size, params)
 
   def Grow(self, amount, dryrun):
     """Resize the DRBD device and its backing storage.
@@ -1956,13 +2096,13 @@ class FileStorage(BlockDev):
   The unique_id for the file device is a (file_driver, file_path) tuple.
 
   """
-  def __init__(self, unique_id, children, size):
+  def __init__(self, unique_id, children, size, params):
     """Initalizes a file device backend.
 
     """
     if children:
       raise errors.BlockDeviceError("Invalid setup for file device")
-    super(FileStorage, self).__init__(unique_id, children, size)
+    super(FileStorage, self).__init__(unique_id, children, size, params)
     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
       raise ValueError("Invalid configuration data %s" % str(unique_id))
     self.driver = unique_id[0]
@@ -2070,7 +2210,7 @@ class FileStorage(BlockDev):
       _ThrowError("Can't stat %s: %s", self.dev_path, err)
 
   @classmethod
-  def Create(cls, unique_id, children, size):
+  def Create(cls, unique_id, children, size, params):
     """Create a new file.
 
     @param size: the size of file in MiB
@@ -2092,7 +2232,7 @@ class FileStorage(BlockDev):
         _ThrowError("File already existing: %s", dev_path)
       _ThrowError("Error in file creation: %", str(err))
 
-    return FileStorage(unique_id, children, size)
+    return FileStorage(unique_id, children, size, params)
 
 
 class PersistentBlockDevice(BlockDev):
@@ -2105,13 +2245,14 @@ class PersistentBlockDevice(BlockDev):
   For the time being, pathnames are required to lie under /dev.
 
   """
-  def __init__(self, unique_id, children, size):
+  def __init__(self, unique_id, children, size, params):
     """Attaches to a static block device.
 
     The unique_id is a path under /dev.
 
     """
-    super(PersistentBlockDevice, self).__init__(unique_id, children, size)
+    super(PersistentBlockDevice, self).__init__(unique_id, children, size,
+                                                params)
     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
       raise ValueError("Invalid configuration data %s" % str(unique_id))
     self.dev_path = unique_id[1]
@@ -2130,13 +2271,13 @@ class PersistentBlockDevice(BlockDev):
     self.Attach()
 
   @classmethod
-  def Create(cls, unique_id, children, size):
+  def Create(cls, unique_id, children, size, params):
     """Create a new device
 
     This is a noop, we only return a PersistentBlockDevice instance
 
     """
-    return PersistentBlockDevice(unique_id, children, 0)
+    return PersistentBlockDevice(unique_id, children, 0, params)
 
   def Remove(self):
     """Remove a device
@@ -2205,50 +2346,377 @@ class PersistentBlockDevice(BlockDev):
     _ThrowError("Grow is not supported for PersistentBlockDev storage")
 
 
+class RADOSBlockDevice(BlockDev):
+  """A RADOS Block Device (rbd).
+
+  This class implements the RADOS Block Device for the backend. You need
+  the rbd kernel driver, the RADOS Tools and a working RADOS cluster for
+  this to be functional.
+
+  """
+  def __init__(self, unique_id, children, size, params):
+    """Attaches to an rbd device.
+
+    """
+    super(RADOSBlockDevice, self).__init__(unique_id, children, size, params)
+    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
+      raise ValueError("Invalid configuration data %s" % str(unique_id))
+
+    self.driver, self.rbd_name = unique_id
+
+    self.major = self.minor = None
+    self.Attach()
+
+  @classmethod
+  def Create(cls, unique_id, children, size, params):
+    """Create a new rbd device.
+
+    Provision a new rbd volume inside a RADOS pool.
+
+    """
+    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
+      raise errors.ProgrammerError("Invalid configuration data %s" %
+                                   str(unique_id))
+    rbd_pool = params[constants.LDP_POOL]
+    rbd_name = unique_id[1]
+
+    # Provision a new rbd volume (Image) inside the RADOS cluster.
+    cmd = [constants.RBD_CMD, "create", "-p", rbd_pool,
+           rbd_name, "--size", "%s" % size]
+    result = utils.RunCmd(cmd)
+    if result.failed:
+      _ThrowError("rbd creation failed (%s): %s",
+                  result.fail_reason, result.output)
+
+    return RADOSBlockDevice(unique_id, children, size, params)
+
+  def Remove(self):
+    """Remove the rbd device.
+
+    """
+    rbd_pool = self.params[constants.LDP_POOL]
+    rbd_name = self.unique_id[1]
+
+    if not self.minor and not self.Attach():
+      # The rbd device doesn't exist.
+      return
+
+    # First shutdown the device (remove mappings).
+    self.Shutdown()
+
+    # Remove the actual Volume (Image) from the RADOS cluster.
+    cmd = [constants.RBD_CMD, "rm", "-p", rbd_pool, rbd_name]
+    result = utils.RunCmd(cmd)
+    if result.failed:
+      _ThrowError("Can't remove Volume from cluster with rbd rm: %s - %s",
+                  result.fail_reason, result.output)
+
+  def Rename(self, new_id):
+    """Rename this device.
+
+    """
+    pass
+
+  def Attach(self):
+    """Attach to an existing rbd device.
+
+    This method maps the rbd volume that matches our name with
+    an rbd device and then attaches to this device.
+
+    """
+    self.attached = False
+
+    # Map the rbd volume to a block device under /dev
+    self.dev_path = self._MapVolumeToBlockdev(self.unique_id)
+
+    try:
+      st = os.stat(self.dev_path)
+    except OSError, err:
+      logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
+      return False
+
+    if not stat.S_ISBLK(st.st_mode):
+      logging.error("%s is not a block device", self.dev_path)
+      return False
+
+    self.major = os.major(st.st_rdev)
+    self.minor = os.minor(st.st_rdev)
+    self.attached = True
+
+    return True
+
+  def _MapVolumeToBlockdev(self, unique_id):
+    """Maps existing rbd volumes to block devices.
+
+    This method should be idempotent if the mapping already exists.
+
+    @rtype: string
+    @return: the block device path that corresponds to the volume
+
+    """
+    pool = self.params[constants.LDP_POOL]
+    name = unique_id[1]
+
+    # Check if the mapping already exists.
+    showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
+    result = utils.RunCmd(showmap_cmd)
+    if result.failed:
+      _ThrowError("rbd showmapped failed (%s): %s",
+                  result.fail_reason, result.output)
+
+    rbd_dev = self._ParseRbdShowmappedOutput(result.output, name)
+
+    if rbd_dev:
+      # The mapping exists. Return it.
+      return rbd_dev
+
+    # The mapping doesn't exist. Create it.
+    map_cmd = [constants.RBD_CMD, "map", "-p", pool, name]
+    result = utils.RunCmd(map_cmd)
+    if result.failed:
+      _ThrowError("rbd map failed (%s): %s",
+                  result.fail_reason, result.output)
+
+    # Find the corresponding rbd device.
+    showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
+    result = utils.RunCmd(showmap_cmd)
+    if result.failed:
+      _ThrowError("rbd map succeeded, but showmapped failed (%s): %s",
+                  result.fail_reason, result.output)
+
+    rbd_dev = self._ParseRbdShowmappedOutput(result.output, name)
+
+    if not rbd_dev:
+      _ThrowError("rbd map succeeded, but could not find the rbd block"
+                  " device in output of showmapped, for volume: %s", name)
+
+    # The device was successfully mapped. Return it.
+    return rbd_dev
+
+  @staticmethod
+  def _ParseRbdShowmappedOutput(output, volume_name):
+    """Parse the output of `rbd showmapped'.
+
+    This method parses the output of `rbd showmapped' and returns
+    the rbd block device path (e.g. /dev/rbd0) that matches the
+    given rbd volume.
+
+    @type output: string
+    @param output: the whole output of `rbd showmapped'
+    @type volume_name: string
+    @param volume_name: the name of the volume whose device we search for
+    @rtype: string or None
+    @return: block device path if the volume is mapped, else None
+
+    """
+    allfields = 5
+    volumefield = 2
+    devicefield = 4
+
+    field_sep = "\t"
+
+    lines = output.splitlines()
+    splitted_lines = map(lambda l: l.split(field_sep), lines)
+
+    # Check empty output.
+    if not splitted_lines:
+      _ThrowError("rbd showmapped returned empty output")
+
+    # Check showmapped header line, to determine number of fields.
+    field_cnt = len(splitted_lines[0])
+    if field_cnt != allfields:
+      _ThrowError("Cannot parse rbd showmapped output because its format"
+                  " seems to have changed; expected %s fields, found %s",
+                  allfields, field_cnt)
+
+    matched_lines = \
+      filter(lambda l: len(l) == allfields and l[volumefield] == volume_name,
+             splitted_lines)
+
+    if len(matched_lines) > 1:
+      _ThrowError("The rbd volume %s is mapped more than once."
+                  " This shouldn't happen, try to unmap the extra"
+                  " devices manually.", volume_name)
+
+    if matched_lines:
+      # rbd block device found. Return it.
+      rbd_dev = matched_lines[0][devicefield]
+      return rbd_dev
+
+    # The given volume is not mapped.
+    return None
+
+  def Assemble(self):
+    """Assemble the device.
+
+    """
+    pass
+
+  def Shutdown(self):
+    """Shutdown the device.
+
+    """
+    if not self.minor and not self.Attach():
+      # The rbd device doesn't exist.
+      return
+
+    # Unmap the block device from the Volume.
+    self._UnmapVolumeFromBlockdev(self.unique_id)
+
+    self.minor = None
+    self.dev_path = None
+
+  def _UnmapVolumeFromBlockdev(self, unique_id):
+    """Unmaps the rbd device from the Volume it is mapped.
+
+    Unmaps the rbd device from the Volume it was previously mapped to.
+    This method should be idempotent if the Volume isn't mapped.
+
+    """
+    pool = self.params[constants.LDP_POOL]
+    name = unique_id[1]
+
+    # Check if the mapping already exists.
+    showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
+    result = utils.RunCmd(showmap_cmd)
+    if result.failed:
+      _ThrowError("rbd showmapped failed [during unmap](%s): %s",
+                  result.fail_reason, result.output)
+
+    rbd_dev = self._ParseRbdShowmappedOutput(result.output, name)
+
+    if rbd_dev:
+      # The mapping exists. Unmap the rbd device.
+      unmap_cmd = [constants.RBD_CMD, "unmap", "%s" % rbd_dev]
+      result = utils.RunCmd(unmap_cmd)
+      if result.failed:
+        _ThrowError("rbd unmap failed (%s): %s",
+                    result.fail_reason, result.output)
+
+  def Open(self, force=False):
+    """Make the device ready for I/O.
+
+    """
+    pass
+
+  def Close(self):
+    """Notifies that the device will no longer be used for I/O.
+
+    """
+    pass
+
+  def Grow(self, amount, dryrun):
+    """Grow the Volume.
+
+    @type amount: integer
+    @param amount: the amount (in mebibytes) to grow with
+    @type dryrun: boolean
+    @param dryrun: whether to execute the operation in simulation mode
+        only, without actually increasing the size
+
+    """
+    if not self.Attach():
+      _ThrowError("Can't attach to rbd device during Grow()")
+
+    if dryrun:
+      # the rbd tool does not support dry runs of resize operations.
+      # Since rbd volumes are thinly provisioned, we assume
+      # there is always enough free space for the operation.
+      return
+
+    rbd_pool = self.params[constants.LDP_POOL]
+    rbd_name = self.unique_id[1]
+    new_size = self.size + amount
+
+    # Resize the rbd volume (Image) inside the RADOS cluster.
+    cmd = [constants.RBD_CMD, "resize", "-p", rbd_pool,
+           rbd_name, "--size", "%s" % new_size]
+    result = utils.RunCmd(cmd)
+    if result.failed:
+      _ThrowError("rbd resize failed (%s): %s",
+                  result.fail_reason, result.output)
+
+
 DEV_MAP = {
   constants.LD_LV: LogicalVolume,
   constants.LD_DRBD8: DRBD8,
   constants.LD_BLOCKDEV: PersistentBlockDevice,
+  constants.LD_RBD: RADOSBlockDevice,
   }
 
 if constants.ENABLE_FILE_STORAGE or constants.ENABLE_SHARED_FILE_STORAGE:
   DEV_MAP[constants.LD_FILE] = FileStorage
 
 
-def FindDevice(dev_type, unique_id, children, size):
+def _VerifyDiskType(dev_type):
+  if dev_type not in DEV_MAP:
+    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
+
+
+def _VerifyDiskParams(disk):
+  """Verifies if all disk parameters are set.
+
+  """
+  missing = set(constants.DISK_LD_DEFAULTS[disk.dev_type]) - set(disk.params)
+  if missing:
+    raise errors.ProgrammerError("Block device is missing disk parameters: %s" %
+                                 missing)
+
+
+def FindDevice(disk, children):
   """Search for an existing, assembled device.
 
   This will succeed only if the device exists and is assembled, but it
   does not do any actions in order to activate the device.
 
+  @type disk: L{objects.Disk}
+  @param disk: the disk object to find
+  @type children: list of L{bdev.BlockDev}
+  @param children: the list of block devices that are children of the device
+                  represented by the disk parameter
+
   """
-  if dev_type not in DEV_MAP:
-    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
-  device = DEV_MAP[dev_type](unique_id, children, size)
+  _VerifyDiskType(disk.dev_type)
+  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
+                                  disk.params)
   if not device.attached:
     return None
   return device
 
 
-def Assemble(dev_type, unique_id, children, size):
+def Assemble(disk, children):
   """Try to attach or assemble an existing device.
 
   This will attach to assemble the device, as needed, to bring it
   fully up. It must be safe to run on already-assembled devices.
 
+  @type disk: L{objects.Disk}
+  @param disk: the disk object to assemble
+  @type children: list of L{bdev.BlockDev}
+  @param children: the list of block devices that are children of the device
+                  represented by the disk parameter
+
   """
-  if dev_type not in DEV_MAP:
-    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
-  device = DEV_MAP[dev_type](unique_id, children, size)
+  _VerifyDiskType(disk.dev_type)
+  _VerifyDiskParams(disk)
+  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
+                                  disk.params)
   device.Assemble()
   return device
 
 
-def Create(dev_type, unique_id, children, size):
+def Create(disk, children):
   """Create a device.
 
+  @type disk: L{objects.Disk}
+  @param disk: the disk object to create
+  @type children: list of L{bdev.BlockDev}
+  @param children: the list of block devices that are children of the device
+                  represented by the disk parameter
+
   """
-  if dev_type not in DEV_MAP:
-    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
-  device = DEV_MAP[dev_type].Create(unique_id, children, size)
+  _VerifyDiskType(disk.dev_type)
+  _VerifyDiskParams(disk)
+  device = DEV_MAP[disk.dev_type].Create(disk.physical_id, children, disk.size,
+                                         disk.params)
   return device
index 733a8ba..440f568 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007, 2008, 2010, 2011 Google Inc.
+# Copyright (C) 2006, 2007, 2008, 2010, 2011, 2012 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
@@ -88,10 +88,14 @@ def GenerateHmacKey(file_name):
                   backup=True)
 
 
-def GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, new_confd_hmac_key,
-                          new_cds, rapi_cert_pem=None, cds=None,
+def GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, new_spice_cert,
+                          new_confd_hmac_key, new_cds,
+                          rapi_cert_pem=None, spice_cert_pem=None,
+                          spice_cacert_pem=None, cds=None,
                           nodecert_file=constants.NODED_CERT_FILE,
                           rapicert_file=constants.RAPI_CERT_FILE,
+                          spicecert_file=constants.SPICE_CERT_FILE,
+                          spicecacert_file=constants.SPICE_CACERT_FILE,
                           hmackey_file=constants.CONFD_HMAC_KEY,
                           cds_file=constants.CLUSTER_DOMAIN_SECRET_FILE):
   """Updates the cluster certificates, keys and secrets.
@@ -100,18 +104,29 @@ def GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, new_confd_hmac_key,
   @param new_cluster_cert: Whether to generate a new cluster certificate
   @type new_rapi_cert: bool
   @param new_rapi_cert: Whether to generate a new RAPI certificate
+  @type new_spice_cert: bool
+  @param new_spice_cert: Whether to generate a new SPICE certificate
   @type new_confd_hmac_key: bool
   @param new_confd_hmac_key: Whether to generate a new HMAC key
   @type new_cds: bool
   @param new_cds: Whether to generate a new cluster domain secret
   @type rapi_cert_pem: string
   @param rapi_cert_pem: New RAPI certificate in PEM format
+  @type spice_cert_pem: string
+  @param spice_cert_pem: New SPICE certificate in PEM format
+  @type spice_cacert_pem: string
+  @param spice_cacert_pem: Certificate of the CA that signed the SPICE
+                           certificate, in PEM format
   @type cds: string
   @param cds: New cluster domain secret
   @type nodecert_file: string
   @param nodecert_file: optional override of the node cert file path
   @type rapicert_file: string
   @param rapicert_file: optional override of the rapi cert file path
+  @type spicecert_file: string
+  @param spicecert_file: optional override of the spice cert file path
+  @type spicecacert_file: string
+  @param spicecacert_file: optional override of the spice CA cert file path
   @type hmackey_file: string
   @param hmackey_file: optional override of the hmac key file path
 
@@ -145,6 +160,31 @@ def GenerateClusterCrypto(new_cluster_cert, new_rapi_cert, new_confd_hmac_key,
     logging.debug("Generating new RAPI certificate at %s", rapicert_file)
     utils.GenerateSelfSignedSslCert(rapicert_file)
 
+  # SPICE
+  spice_cert_exists = os.path.exists(spicecert_file)
+  spice_cacert_exists = os.path.exists(spicecacert_file)
+  if spice_cert_pem:
+    # spice_cert_pem implies also spice_cacert_pem
+    logging.debug("Writing SPICE certificate at %s", spicecert_file)
+    utils.WriteFile(spicecert_file, data=spice_cert_pem, backup=True)
+    logging.debug("Writing SPICE CA certificate at %s", spicecacert_file)
+    utils.WriteFile(spicecacert_file, data=spice_cacert_pem, backup=True)
+  elif new_spice_cert or not spice_cert_exists:
+    if spice_cert_exists:
+      utils.CreateBackup(spicecert_file)
+    if spice_cacert_exists:
+      utils.CreateBackup(spicecacert_file)
+
+    logging.debug("Generating new self-signed SPICE certificate at %s",
+                  spicecert_file)
+    (_, cert_pem) = utils.GenerateSelfSignedSslCert(spicecert_file)
+
+    # Self-signed certificate -> the public certificate is also the CA public
+    # certificate
+    logging.debug("Writing the public certificate to %s",
+                  spicecert_file)
+    utils.io.WriteFile(spicecacert_file, mode=0400, data=cert_pem)
+
   # Cluster domain secret
   if cds:
     logging.debug("Writing cluster domain secret to %s", cds_file)
@@ -166,7 +206,7 @@ def _InitGanetiServerSetup(master_name):
 
   """
   # Generate cluster secrets
-  GenerateClusterCrypto(True, False, False, False)
+  GenerateClusterCrypto(True, False, False, False, False)
 
   result = utils.RunCmd([constants.DAEMON_UTIL, "start", constants.NODED])
   if result.failed:
@@ -182,7 +222,9 @@ def _WaitForNodeDaemon(node_name):
 
   """
   def _CheckNodeDaemon():
-    result = rpc.RpcRunner.call_version([node_name])[node_name]
+    # Pylint bug <http://www.logilab.org/ticket/35642>
+    # pylint: disable=E1101
+    result = rpc.BootstrapRunner().call_version([node_name])[node_name]
     if result.fail_msg:
       raise utils.RetryAgain()
 
@@ -242,14 +284,16 @@ def _InitFileStorage(file_storage_dir):
   return file_storage_dir
 
 
-def InitCluster(cluster_name, mac_prefix, # pylint: disable=R0913
-                master_netdev, file_storage_dir, shared_file_storage_dir,
-                candidate_pool_size, secondary_ip=None, vg_name=None,
-                beparams=None, nicparams=None, ndparams=None, hvparams=None,
-                enabled_hypervisors=None, modify_etc_hosts=True,
-                modify_ssh_setup=True, maintain_node_health=False,
-                drbd_helper=None, uid_pool=None, default_iallocator=None,
-                primary_ip_version=None, prealloc_wipe_disks=False):
+def InitCluster(cluster_name, mac_prefix, # pylint: disable=R0913, R0914
+                master_netmask, master_netdev, file_storage_dir,
+                shared_file_storage_dir, candidate_pool_size, secondary_ip=None,
+                vg_name=None, beparams=None, nicparams=None, ndparams=None,
+                hvparams=None, diskparams=None, enabled_hypervisors=None,
+                modify_etc_hosts=True, modify_ssh_setup=True,
+                maintain_node_health=False, drbd_helper=None, uid_pool=None,
+                default_iallocator=None, primary_ip_version=None, ipolicy=None,
+                prealloc_wipe_disks=False, use_external_mip_script=False,
+                hv_state=None, disk_state=None):
   """Initialise the cluster.
 
   @type candidate_pool_size: int
@@ -270,12 +314,9 @@ def InitCluster(cluster_name, mac_prefix, # pylint: disable=R0913
                                " entries: %s" % invalid_hvs,
                                errors.ECODE_INVAL)
 
-  ipcls = None
-  if primary_ip_version == constants.IP4_VERSION:
-    ipcls = netutils.IP4Address
-  elif primary_ip_version == constants.IP6_VERSION:
-    ipcls = netutils.IP6Address
-  else:
+  try:
+    ipcls = netutils.IPAddress.GetClassFromIpVersion(primary_ip_version)
+  except errors.ProgrammerError:
     raise errors.OpPrereqError("Invalid primary ip version: %d." %
                                primary_ip_version)
 
@@ -319,6 +360,13 @@ def InitCluster(cluster_name, mac_prefix, # pylint: disable=R0913
                                " but it does not belong to this host." %
                                secondary_ip, errors.ECODE_ENVIRON)
 
+  if master_netmask is not None:
+    if not ipcls.ValidateNetmask(master_netmask):
+      raise errors.OpPrereqError("CIDR netmask (%s) not valid for IPv%s " %
+                                  (master_netmask, primary_ip_version))
+  else:
+    master_netmask = ipcls.iplen
+
   if vg_name is not None:
     # Check if volume group is valid
     vgstatus = utils.CheckVolumeGroupSize(utils.ListVolumeGroups(), vg_name,
@@ -365,21 +413,62 @@ def InitCluster(cluster_name, mac_prefix, # pylint: disable=R0913
   dirs = [(constants.RUN_GANETI_DIR, constants.RUN_DIRS_MODE)]
   utils.EnsureDirs(dirs)
 
+  objects.UpgradeBeParams(beparams)
   utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
   utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
+
   objects.NIC.CheckParameterSyntax(nicparams)
 
+  full_ipolicy = objects.FillIPolicy(constants.IPOLICY_DEFAULTS, ipolicy)
+
   if ndparams is not None:
     utils.ForceDictType(ndparams, constants.NDS_PARAMETER_TYPES)
   else:
     ndparams = dict(constants.NDC_DEFAULTS)
 
+  # This is ugly, as we modify the dict itself
+  # FIXME: Make utils.ForceDictType pure functional or write a wrapper
+  # around it
+  if hv_state:
+    for hvname, hvs_data in hv_state.items():
+      utils.ForceDictType(hvs_data, constants.HVSTS_PARAMETER_TYPES)
+      hv_state[hvname] = objects.Cluster.SimpleFillHvState(hvs_data)
+  else:
+    hv_state = dict((hvname, constants.HVST_DEFAULTS)
+                    for hvname in enabled_hypervisors)
+
+  # FIXME: disk_state has no default values yet
+  if disk_state:
+    for storage, ds_data in disk_state.items():
+      if storage not in constants.DS_VALID_TYPES:
+        raise errors.OpPrereqError("Invalid storage type in disk state: %s" %
+                                   storage, errors.ECODE_INVAL)
+      for ds_name, state in ds_data.items():
+        utils.ForceDictType(state, constants.DSS_PARAMETER_TYPES)
+        ds_data[ds_name] = objects.Cluster.SimpleFillDiskState(state)
+
   # hvparams is a mapping of hypervisor->hvparams dict
   for hv_name, hv_params in hvparams.iteritems():
     utils.ForceDictType(hv_params, constants.HVS_PARAMETER_TYPES)
     hv_class = hypervisor.GetHypervisor(hv_name)
     hv_class.CheckParameterSyntax(hv_params)
 
+  # diskparams is a mapping of disk-template->diskparams dict
+  for template, dt_params in diskparams.items():
+    param_keys = set(dt_params.keys())
+    default_param_keys = set(constants.DISK_DT_DEFAULTS[template].keys())
+    if not (param_keys <= default_param_keys):
+      unknown_params = param_keys - default_param_keys
+      raise errors.OpPrereqError("Invalid parameters for disk template %s:"
+                                 " %s" % (template,
+                                          utils.CommaJoin(unknown_params)))
+    utils.ForceDictType(dt_params, constants.DISK_DT_TYPES)
+  try:
+    utils.VerifyDictOptions(diskparams, constants.DISK_DT_DEFAULTS)
+  except errors.OpPrereqError, err:
+    raise errors.OpPrereqError("While verify diskparam options: %s" % err,
+                               errors.ECODE_INVAL)
+
   # set up ssh config and /etc/hosts
   sshline = utils.ReadFile(constants.SSH_HOST_RSA_PUB)
   sshkey = sshline.split(" ")[1]
@@ -417,6 +506,7 @@ def InitCluster(cluster_name, mac_prefix, # pylint: disable=R0913
     tcpudp_port_pool=set(),
     master_node=hostname.name,
     master_ip=clustername.ip,
+    master_netmask=master_netmask,
     master_netdev=master_netdev,
     cluster_name=clustername.name,
     file_storage_dir=file_storage_dir,
@@ -426,6 +516,7 @@ def InitCluster(cluster_name, mac_prefix, # pylint: disable=R0913
     nicparams={constants.PP_DEFAULT: nicparams},
     ndparams=ndparams,
     hvparams=hvparams,
+    diskparams=diskparams,
     candidate_pool_size=candidate_pool_size,
     modify_etc_hosts=modify_etc_hosts,
     modify_ssh_setup=modify_ssh_setup,
@@ -437,6 +528,10 @@ def InitCluster(cluster_name, mac_prefix, # pylint: disable=R0913
     default_iallocator=default_iallocator,
     primary_ip_family=ipcls.family,
     prealloc_wipe_disks=prealloc_wipe_disks,
+    use_external_mip_script=use_external_mip_script,
+    ipolicy=full_ipolicy,
+    hv_state_static=hv_state,
+    disk_state_static=disk_state,
     )
   master_node_config = objects.Node(name=hostname.name,
                                     primary_ip=hostname.ip,
@@ -494,6 +589,7 @@ def InitConfig(version, cluster_config, master_node_config,
     uuid=uuid_generator.Generate([], utils.NewUUID, _INITCONF_ECID),
     name=constants.INITIAL_NODE_GROUP_NAME,
     members=[master_node_config.name],
+    diskparams={},
     )
   nodegroups = {
     default_nodegroup.uuid: default_nodegroup,
@@ -520,11 +616,24 @@ def FinalizeClusterDestroy(master):
   """
   cfg = config.ConfigWriter()
   modify_ssh_setup = cfg.GetClusterInfo().modify_ssh_setup
-  result = rpc.RpcRunner.call_node_stop_master(master, True)
+  runner = rpc.BootstrapRunner()
+
+  master_params = cfg.GetMasterNetworkParameters()
+  master_params.name = master
+  ems = cfg.GetUseExternalMipScript()
+  result = runner.call_node_deactivate_master_ip(master_params.name,
+                                                 master_params, ems)
+
+  msg = result.fail_msg
+  if msg:
+    logging.warning("Could not disable the master IP: %s", msg)
+
+  result = runner.call_node_stop_master(master)
   msg = result.fail_msg
   if msg:
     logging.warning("Could not disable the master role: %s", msg)
-  result = rpc.RpcRunner.call_node_leave_cluster(master, modify_ssh_setup)
+
+  result = runner.call_node_leave_cluster(master, modify_ssh_setup)
   msg = result.fail_msg
   if msg:
     logging.warning("Could not shutdown the node daemon and cleanup"
@@ -557,12 +666,14 @@ def SetupNodeDaemon(cluster_name, node, ssh_key_check):
   # either by being constants or by the checks above
   sshrunner.CopyFileToNode(node, constants.NODED_CERT_FILE)
   sshrunner.CopyFileToNode(node, constants.RAPI_CERT_FILE)
+  sshrunner.CopyFileToNode(node, constants.SPICE_CERT_FILE)
+  sshrunner.CopyFileToNode(node, constants.SPICE_CACERT_FILE)
   sshrunner.CopyFileToNode(node, constants.CONFD_HMAC_KEY)
   mycommand = ("%s stop-all; %s start %s -b %s" %
                (constants.DAEMON_UTIL, constants.DAEMON_UTIL, constants.NODED,
                 utils.ShellQuote(bind_address)))
 
-  result = sshrunner.Run(node, 'root', mycommand, batch=False,
+  result = sshrunner.Run(node, "root", mycommand, batch=False,
                          ask_key=ssh_key_check,
                          use_cluster_key=True,
                          strict_host_check=ssh_key_check)
@@ -604,7 +715,7 @@ def MasterFailover(no_voting=False):
                                " as master candidates. Only these nodes"
                                " can become masters. Current list of"
                                " master candidates is:\n"
-                               "%s" % ('\n'.join(mc_no_master)),
+                               "%s" % ("\n".join(mc_no_master)),
                                errors.ECODE_STATE)
 
   if not no_voting:
@@ -650,7 +761,18 @@ def MasterFailover(no_voting=False):
 
   logging.info("Stopping the master daemon on node %s", old_master)
 
-  result = rpc.RpcRunner.call_node_stop_master(old_master, True)
+  runner = rpc.BootstrapRunner()
+  master_params = cfg.GetMasterNetworkParameters()
+  master_params.name = old_master
+  ems = cfg.GetUseExternalMipScript()
+  result = runner.call_node_deactivate_master_ip(master_params.name,
+                                                 master_params, ems)
+
+  msg = result.fail_msg
+  if msg:
+    logging.warning("Could not disable the master IP: %s", msg)
+
+  result = runner.call_node_stop_master(old_master)
   msg = result.fail_msg
   if msg:
     logging.error("Could not disable the master role on the old master"
@@ -679,7 +801,8 @@ def MasterFailover(no_voting=False):
 
   logging.info("Starting the master daemons on the new master")
 
-  result = rpc.RpcRunner.call_node_start_master(new_master, True, no_voting)
+  result = rpc.BootstrapRunner().call_node_start_master_daemons(new_master,
+                                                                no_voting)
   msg = result.fail_msg
   if msg:
     logging.error("Could not start the master role on the new master"
@@ -735,7 +858,7 @@ def GatherMasterVotes(node_list):
   if not node_list:
     # no nodes left (eventually after removing myself)
     return []
-  results = rpc.RpcRunner.call_master_info(node_list)
+  results = rpc.BootstrapRunner().call_master_info(node_list)
   if not isinstance(results, dict):
     # this should not happen (unless internal error in rpc)
     logging.critical("Can't complete rpc call, aborting master startup")
@@ -749,7 +872,8 @@ def GatherMasterVotes(node_list):
     if msg:
       logging.warning("Error contacting node %s: %s", node, msg)
       fail = True
-    # for now we accept both length 3 and 4 (data[3] is primary ip version)
+    # for now we accept both length 3, 4 and 5 (data[3] is primary ip version
+    # and data[4] is the master netmask)
     elif not isinstance(data, (tuple, list)) or len(data) < 3:
       logging.warning("Invalid data received from node %s: %s", node, data)
       fail = True
diff --git a/lib/build/shell_example_lexer.py b/lib/build/shell_example_lexer.py
new file mode 100644 (file)
index 0000000..401ad65
--- /dev/null
@@ -0,0 +1,73 @@
+#
+#
+
+# Copyright (C) 2012 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.
+
+
+"""Pygments lexer for our custom shell example sessions.
+
+The lexer support the following custom markup:
+
+  - comments: # this is a comment
+  - command lines: '$ ' at the beginning of a line denotes a command
+  - variable input: %input% (works in both commands and screen output)
+  - otherwise, regular text output from commands will be plain
+
+"""
+
+from pygments.lexer import RegexLexer, bygroups, include
+from pygments.token import Name, Text, Generic, Comment
+
+
+class ShellExampleLexer(RegexLexer):
+  name = "ShellExampleLexer"
+  aliases = "shell-example"
+  filenames = []
+
+  tokens = {
+    "root": [
+      include("comments"),
+      include("userinput"),
+      # switch to state input on '$ ' at the start of the line
+      (r"^\$ ", Text, "input"),
+      (r"\s+", Text),
+      (r"[^#%\s\\]+", Text),
+      (r"\\", Text),
+      ],
+    "input": [
+      include("comments"),
+      include("userinput"),
+      (r"[^#%\s\\]+", Generic.Strong),
+      (r"\\\n", Generic.Strong),
+      (r"\\", Generic.Strong),
+      # switch to prev state at non-escaped new-line
+      (r"\n", Text, "#pop"),
+      (r"\s+", Text),
+      ],
+    "comments": [
+      (r"#.*\n", Comment.Single),
+      ],
+    "userinput": [
+      (r"(\\)(%)", bygroups(None, Text)),
+      (r"(%)([^%]*)(%)", bygroups(None, Name.Variable, None)),
+      ],
+    }
+
+
+def setup(app):
+  app.add_lexer("shell-example", ShellExampleLexer())
index 0d2d2e3..bd579ef 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2011 Google Inc.
+# Copyright (C) 2011, 2012 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
@@ -41,15 +41,37 @@ from ganeti import utils
 from ganeti import opcodes
 from ganeti import ht
 from ganeti import rapi
+from ganeti import luxi
 
 import ganeti.rapi.rlib2 # pylint: disable=W0611
 
 
-COMMON_PARAM_NAMES = map(compat.fst, opcodes.OpCode.OP_PARAMS)
+def _GetCommonParamNames():
+  """Builds a list of parameters common to all opcodes.
+
+  """
+  names = set(map(compat.fst, opcodes.OpCode.OP_PARAMS))
+
+  # The "depends" attribute should be listed
+  names.remove(opcodes.DEPEND_ATTR)
+
+  return names
+
+
+COMMON_PARAM_NAMES = _GetCommonParamNames()
 
 #: Namespace for evaluating expressions
 EVAL_NS = dict(compat=compat, constants=constants, utils=utils, errors=errors,
-               rlib2=rapi.rlib2)
+               rlib2=rapi.rlib2, luxi=luxi)
+
+# Constants documentation for man pages
+CV_ECODES_DOC = "ecodes"
+# We don't care about the leak of variables _, name and doc here.
+# pylint: disable=W0621
+CV_ECODES_DOC_LIST = [(name, doc) for (_, name, doc) in constants.CV_ALL_ECODES]
+DOCUMENTED_CONSTANTS = {
+  CV_ECODES_DOC: CV_ECODES_DOC_LIST,
+  }
 
 
 class OpcodeError(sphinx.errors.SphinxError):
@@ -270,11 +292,22 @@ def BuildQueryFields(fields):
   @type fields: dict (field name as key, field details as value)
 
   """
-  for (_, (fdef, _, _, _)) in utils.NiceSort(fields.items(),
-                                             key=compat.fst):
-    assert len(fdef.doc.splitlines()) == 1
-    yield "``%s``" % fdef.name
-    yield "  %s" % fdef.doc
+  defs = [(fdef.name, fdef.doc)
+           for (_, (fdef, _, _, _)) in utils.NiceSort(fields.items(),
+                                                      key=compat.fst)]
+  return BuildValuesDoc(defs)
+
+
+def BuildValuesDoc(values):
+  """Builds documentation for a list of values
+
+  @type values: list of tuples in the form (value, documentation)
+
+  """
+  for name, doc in values:
+    assert len(doc.splitlines()) == 1
+    yield "``%s``" % name
+    yield "  %s" % doc
 
 
 # TODO: Implement Sphinx directive for query fields
index 38d2b64..feb0fe4 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 Google Inc.
+# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 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
@@ -29,6 +29,7 @@ import time
 import logging
 import errno
 import itertools
+import shlex
 from cStringIO import StringIO
 
 from ganeti import utils
@@ -42,6 +43,7 @@ from ganeti import ssh
 from ganeti import compat
 from ganeti import netutils
 from ganeti import qlang
+from ganeti import objects
 
 from optparse import (OptionParser, TitledHelpFormatter,
                       Option, OptionValueError)
@@ -49,6 +51,7 @@ from optparse import (OptionParser, TitledHelpFormatter,
 
 __all__ = [
   # Command line options
+  "ABSOLUTE_OPT",
   "ADD_UIDS_OPT",
   "ALLOCATABLE_OPT",
   "ALLOC_POLICY_OPT",
@@ -68,6 +71,7 @@ __all__ = [
   "DEBUG_SIMERR_OPT",
   "DISKIDX_OPT",
   "DISK_OPT",
+  "DISK_PARAMS_OPT",
   "DISK_TEMPLATE_OPT",
   "DRAINED_OPT",
   "DRY_RUN_OPT",
@@ -92,6 +96,7 @@ __all__ = [
   "DEFAULT_IALLOCATOR_OPT",
   "IDENTIFY_DEFAULTS_OPT",
   "IGNORE_CONSIST_OPT",
+  "IGNORE_ERRORS_OPT",
   "IGNORE_FAILURES_OPT",
   "IGNORE_OFFLINE_OPT",
   "IGNORE_REMOVE_FAILURES_OPT",
@@ -101,6 +106,7 @@ __all__ = [
   "MAC_PREFIX_OPT",
   "MAINTAIN_NODE_HEALTH_OPT",
   "MASTER_NETDEV_OPT",
+  "MASTER_NETMASK_OPT",
   "MC_OPT",
   "MIGRATION_MODE_OPT",
   "NET_OPT",
@@ -109,6 +115,7 @@ __all__ = [
   "NEW_CONFD_HMAC_KEY_OPT",
   "NEW_RAPI_CERT_OPT",
   "NEW_SECONDARY_OPT",
+  "NEW_SPICE_CERT_OPT",
   "NIC_PARAMS_OPT",
   "NODE_FORCE_JOIN_OPT",
   "NODE_LIST_OPT",
@@ -127,12 +134,15 @@ __all__ = [
   "NONICS_OPT",
   "NONLIVE_OPT",
   "NONPLUS1_OPT",
+  "NORUNTIME_CHGS_OPT",
   "NOSHUTDOWN_OPT",
   "NOSTART_OPT",
   "NOSSH_KEYCHECK_OPT",
   "NOVOTING_OPT",
   "NO_REMEMBER_OPT",
   "NWSYNC_OPT",
+  "OFFLINE_INST_OPT",
+  "ONLINE_INST_OPT",
   "ON_PRIMARY_OPT",
   "ON_SECONDARY_OPT",
   "OFFLINE_OPT",
@@ -151,6 +161,7 @@ __all__ = [
   "REMOVE_INSTANCE_OPT",
   "REMOVE_UIDS_OPT",
   "RESERVED_LVS_OPT",
+  "RUNTIME_MEM_OPT",
   "ROMAN_OPT",
   "SECONDARY_IP_OPT",
   "SECONDARY_ONLY_OPT",
@@ -159,6 +170,15 @@ __all__ = [
   "SHOWCMD_OPT",
   "SHUTDOWN_TIMEOUT_OPT",
   "SINGLE_NODE_OPT",
+  "SPECS_CPU_COUNT_OPT",
+  "SPECS_DISK_COUNT_OPT",
+  "SPECS_DISK_SIZE_OPT",
+  "SPECS_MEM_SIZE_OPT",
+  "SPECS_NIC_COUNT_OPT",
+  "IPOLICY_DISK_TEMPLATES",
+  "IPOLICY_VCPU_RATIO",
+  "SPICE_CACERT_OPT",
+  "SPICE_CERT_OPT",
   "SRC_DIR_OPT",
   "SRC_NODE_OPT",
   "SUBMIT_OPT",
@@ -171,12 +191,18 @@ __all__ = [
   "TO_GROUP_OPT",
   "UIDPOOL_OPT",
   "USEUNITS_OPT",
+  "USE_EXTERNAL_MIP_SCRIPT",
   "USE_REPL_NET_OPT",
   "VERBOSE_OPT",
   "VG_NAME_OPT",
   "YES_DOIT_OPT",
+  "DISK_STATE_OPT",
+  "HV_STATE_OPT",
+  "IGNORE_IPOLICY_OPT",
+  "INSTANCE_POLICY_OPTS",
   # Generic functions for CLI programs
   "ConfirmOperation",
+  "CreateIPolicyFromOpts",
   "GenericMain",
   "GenericInstanceCreate",
   "GenericList",
@@ -261,6 +287,19 @@ _PRIONAME_TO_VALUE = dict(_PRIORITY_NAMES)
 _CHOOSE_BATCH = 25
 
 
+# constants used to create InstancePolicy dictionary
+TISPECS_GROUP_TYPES = {
+  constants.ISPECS_MIN: constants.VTYPE_INT,
+  constants.ISPECS_MAX: constants.VTYPE_INT,
+  }
+
+TISPECS_CLUSTER_TYPES = {
+  constants.ISPECS_MIN: constants.VTYPE_INT,
+  constants.ISPECS_MAX: constants.VTYPE_INT,
+  constants.ISPECS_STD: constants.VTYPE_INT,
+  }
+
+
 class _Argument:
   def __init__(self, min=0, max=None): # pylint: disable=W0622
     self.min = min
@@ -445,7 +484,7 @@ def AddTags(opts, args):
   if not args:
     raise errors.OpPrereqError("No tags to be added")
   op = opcodes.OpTagsSet(kind=kind, name=name, tags=args)
-  SubmitOpCode(op, opts=opts)
+  SubmitOrSend(op, opts)
 
 
 def RemoveTags(opts, args):
@@ -462,7 +501,7 @@ def RemoveTags(opts, args):
   if not args:
     raise errors.OpPrereqError("No tags to be removed")
   op = opcodes.OpTagsDel(kind=kind, name=name, tags=args)
-  SubmitOpCode(op, opts=opts)
+  SubmitOrSend(op, opts)
 
 
 def check_unit(option, opt, value): # pylint: disable=W0613
@@ -529,7 +568,9 @@ def check_ident_key_val(option, opt, value):  # pylint: disable=W0613
       msg = "Cannot pass options when removing parameter groups: %s" % value
       raise errors.ParameterError(msg)
     retval = (ident[len(NO_PREFIX):], False)
-  elif ident.startswith(UN_PREFIX):
+  elif (ident.startswith(UN_PREFIX) and
+        (len(ident) <= len(UN_PREFIX) or
+         not ident[len(UN_PREFIX)][0].isdigit())):
     if rest:
       msg = "Cannot pass options when removing parameter groups: %s" % value
       raise errors.ParameterError(msg)
@@ -564,6 +605,30 @@ def check_bool(option, opt, value): # pylint: disable=W0613
     raise errors.ParameterError("Invalid boolean value '%s'" % value)
 
 
+def check_list(option, opt, value): # pylint: disable=W0613
+  """Custom parser for comma-separated lists.
+
+  """
+  # we have to make this explicit check since "".split(",") is [""],
+  # not an empty list :(
+  if not value:
+    return []
+  else:
+    return utils.UnescapeAndSplit(value)
+
+
+def check_maybefloat(option, opt, value): # pylint: disable=W0613
+  """Custom parser for float numbers which might be also defaults.
+
+  """
+  value = value.lower()
+
+  if value == constants.VALUE_DEFAULT:
+    return value
+  else:
+    return float(value)
+
+
 # completion_suggestion is normally a list. Using numeric values not evaluating
 # to False for dynamic completion.
 (OPT_COMPL_MANY_NODES,
@@ -597,12 +662,16 @@ class CliOption(Option):
     "keyval",
     "unit",
     "bool",
+    "list",
+    "maybefloat",
     )
   TYPE_CHECKER = Option.TYPE_CHECKER.copy()
   TYPE_CHECKER["identkeyval"] = check_ident_key_val
   TYPE_CHECKER["keyval"] = check_key_val
   TYPE_CHECKER["unit"] = check_unit
   TYPE_CHECKER["bool"] = check_bool
+  TYPE_CHECKER["list"] = check_list
+  TYPE_CHECKER["maybefloat"] = check_maybefloat
 
 
 # optparse.py sets make_option, so we do it for our own option class, too
@@ -678,6 +747,14 @@ NWSYNC_OPT = cli_option("--no-wait-for-sync", dest="wait_for_sync",
                         default=True, action="store_false",
                         help="Don't wait for sync (DANGEROUS!)")
 
+ONLINE_INST_OPT = cli_option("--online", dest="online_inst",
+                             action="store_true", default=False,
+                             help="Enable offline instance")
+
+OFFLINE_INST_OPT = cli_option("--offline", dest="offline_inst",
+                              action="store_true", default=False,
+                              help="Disable down instance")
+
 DISK_TEMPLATE_OPT = cli_option("-t", "--disk-template", dest="disk_template",
                                help=("Custom disk setup (%s)" %
                                      utils.CommaJoin(constants.DISK_TEMPLATES)),
@@ -727,6 +804,11 @@ NO_INSTALL_OPT = cli_option("--no-install", dest="no_install",
                             help="Do not install the OS (will"
                             " enable no-start)")
 
+NORUNTIME_CHGS_OPT = cli_option("--no-runtime-changes",
+                                dest="allow_runtime_chgs",
+                                default=True, action="store_false",
+                                help="Don't allow runtime changes")
+
 BACKEND_OPT = cli_option("-B", "--backend-parameters", dest="beparams",
                          type="keyval", default={},
                          help="Backend parameters")
@@ -735,6 +817,56 @@ HVOPTS_OPT = cli_option("-H", "--hypervisor-parameters", type="keyval",
                         default={}, dest="hvparams",
                         help="Hypervisor parameters")
 
+DISK_PARAMS_OPT = cli_option("-D", "--disk-parameters", dest="diskparams",
+                             help="Disk template parameters, in the format"
+                             " template:option=value,option=value,...",
+                             type="identkeyval", action="append", default=[])
+
+SPECS_MEM_SIZE_OPT = cli_option("--specs-mem-size", dest="ispecs_mem_size",
+                                 type="keyval", default={},
+                                 help="Memory size specs: list of key=value,"
+                                " where key is one of min, max, std"
+                                 " (in MB or using a unit)")
+
+SPECS_CPU_COUNT_OPT = cli_option("--specs-cpu-count", dest="ispecs_cpu_count",
+                                 type="keyval", default={},
+                                 help="CPU count specs: list of key=value,"
+                                 " where key is one of min, max, std")
+
+SPECS_DISK_COUNT_OPT = cli_option("--specs-disk-count",
+                                  dest="ispecs_disk_count",
+                                  type="keyval", default={},
+                                  help="Disk count specs: list of key=value,"
+                                  " where key is one of min, max, std")
+
+SPECS_DISK_SIZE_OPT = cli_option("--specs-disk-size", dest="ispecs_disk_size",
+                                 type="keyval", default={},
+                                 help="Disk size specs: list of key=value,"
+                                " where key is one of min, max, std"
+                                 " (in MB or using a unit)")
+
+SPECS_NIC_COUNT_OPT = cli_option("--specs-nic-count", dest="ispecs_nic_count",
+                                 type="keyval", default={},
+                                 help="NIC count specs: list of key=value,"
+                                 " where key is one of min, max, std")
+
+IPOLICY_DISK_TEMPLATES = cli_option("--ipolicy-disk-templates",
+                                 dest="ipolicy_disk_templates",
+                                 type="list", default=None,
+                                 help="Comma-separated list of"
+                                 " enabled disk templates")
+
+IPOLICY_VCPU_RATIO = cli_option("--ipolicy-vcpu-ratio",
+                                 dest="ipolicy_vcpu_ratio",
+                                 type="maybefloat", default=None,
+                                 help="The maximum allowed vcpu-to-cpu ratio")
+
+IPOLICY_SPINDLE_RATIO = cli_option("--ipolicy-spindle-ratio",
+                                   dest="ipolicy_spindle_ratio",
+                                   type="maybefloat", default=None,
+                                   help=("The maximum allowed instances to"
+                                         " spindle ratio"))
+
 HYPERVISOR_OPT = cli_option("-H", "--hypervisor-parameters", dest="hypervisor",
                             help="Hypervisor and hypervisor options, in the"
                             " format hypervisor:option=value,option=value,...",
@@ -1005,6 +1137,18 @@ MASTER_NETDEV_OPT = cli_option("--master-netdev", dest="master_netdev",
                                metavar="NETDEV",
                                default=None)
 
+MASTER_NETMASK_OPT = cli_option("--master-netmask", dest="master_netmask",
+                                help="Specify the netmask of the master IP",
+                                metavar="NETMASK",
+                                default=None)
+
+USE_EXTERNAL_MIP_SCRIPT = cli_option("--use-external-mip-script",
+                                dest="use_external_mip_script",
+                                help="Specify whether to run a user-provided"
+                                " script for the master IP address turnup and"
+                                " turndown operations",
+                                type="bool", metavar=_YORNO, default=None)
+
 GLOBAL_FILEDIR_OPT = cli_option("--file-storage-dir", dest="file_storage_dir",
                                 help="Specify the default directory (cluster-"
                                 "wide) for storing the file-based disks [%s]" %
@@ -1086,6 +1230,21 @@ NEW_RAPI_CERT_OPT = cli_option("--new-rapi-certificate", dest="new_rapi_cert",
                                help=("Generate a new self-signed RAPI"
                                      " certificate"))
 
+SPICE_CERT_OPT = cli_option("--spice-certificate", dest="spice_cert",
+                           default=None,
+                           help="File containing new SPICE certificate")
+
+SPICE_CACERT_OPT = cli_option("--spice-ca-certificate", dest="spice_cacert",
+                           default=None,
+                           help="File containing the certificate of the CA"
+                                " which signed the SPICE certificate")
+
+NEW_SPICE_CERT_OPT = cli_option("--new-spice-certificate",
+                               dest="new_spice_cert", default=None,
+                               action="store_true",
+                               help=("Generate a new self-signed SPICE"
+                                     " certificate"))
+
 NEW_CONFD_HMAC_KEY_OPT = cli_option("--new-confd-hmac-key",
                                     dest="new_confd_hmac_key",
                                     default=False, action="store_true",
@@ -1238,6 +1397,39 @@ TO_GROUP_OPT = cli_option("--to", dest="to", metavar="<group>",
                           default=None, action="append",
                           completion_suggest=OPT_COMPL_ONE_NODEGROUP)
 
+IGNORE_ERRORS_OPT = cli_option("-I", "--ignore-errors", default=[],
+                               action="append", dest="ignore_errors",
+                               choices=list(constants.CV_ALL_ECODES_STRINGS),
+                               help="Error code to be ignored")
+
+DISK_STATE_OPT = cli_option("--disk-state", default=[], dest="disk_state",
+                            action="append",
+                            help=("Specify disk state information in the"
+                                  " format"
+                                  " storage_type/identifier:option=value,...;"
+                                  " note this is unused for now"),
+                            type="identkeyval")
+
+HV_STATE_OPT = cli_option("--hypervisor-state", default=[], dest="hv_state",
+                          action="append",
+                          help=("Specify hypervisor state information in the"
+                                " format hypervisor:option=value,...;"
+                                " note this is unused for now"),
+                          type="identkeyval")
+
+IGNORE_IPOLICY_OPT = cli_option("--ignore-ipolicy", dest="ignore_ipolicy",
+                                action="store_true", default=False,
+                                help="Ignore instance policy violations")
+
+RUNTIME_MEM_OPT = cli_option("-m", "--runtime-memory", dest="runtime_mem",
+                             help="Sets the instance's runtime memory,"
+                             " ballooning it up or down to the new value",
+                             default=None, type="unit", metavar="<size>")
+
+ABSOLUTE_OPT = cli_option("--absolute", dest="absolute",
+                          action="store_true", default=False,
+                          help="Marks the grow as absolute instead of the"
+                          " (default) relative mode")
 
 #: Options provided by all commands
 COMMON_OPTS = [DEBUG_OPT]
@@ -1266,8 +1458,20 @@ COMMON_CREATE_OPTS = [
   PRIORITY_OPT,
   ]
 
+# common instance policy options
+INSTANCE_POLICY_OPTS = [
+  SPECS_CPU_COUNT_OPT,
+  SPECS_DISK_COUNT_OPT,
+  SPECS_DISK_SIZE_OPT,
+  SPECS_MEM_SIZE_OPT,
+  SPECS_NIC_COUNT_OPT,
+  IPOLICY_DISK_TEMPLATES,
+  IPOLICY_VCPU_RATIO,
+  IPOLICY_SPINDLE_RATIO,
+  ]
 
-def _ParseArgs(argv, commands, aliases):
+
+def _ParseArgs(argv, commands, aliases, env_override):
   """Parser for the command line arguments.
 
   This function parses the arguments and returns the function which
@@ -1277,8 +1481,11 @@ def _ParseArgs(argv, commands, aliases):
   @param commands: dictionary with special contents, see the design
       doc for cmdline handling
   @param aliases: dictionary with command aliases {'alias': 'target, ...}
+  @param env_override: list of env variables allowed for default args
 
   """
+  assert not (env_override - set(commands))
+
   if len(argv) == 0:
     binary = "<command>"
   else:
@@ -1332,13 +1539,19 @@ def _ParseArgs(argv, commands, aliases):
 
     cmd = aliases[cmd]
 
+  if cmd in env_override:
+    args_env_name = ("%s_%s" % (binary.replace("-", "_"), cmd)).upper()
+    env_args = os.environ.get(args_env_name)
+    if env_args:
+      argv = utils.InsertAtPos(argv, 1, shlex.split(env_args))
+
   func, args_def, parser_opts, usage, description = commands[cmd]
   parser = OptionParser(option_list=parser_opts + COMMON_OPTS,
                         description=description,
                         formatter=TitledHelpFormatter(),
                         usage="%%prog %s %s" % (cmd, usage))
   parser.disable_interspersed_args()
-  options, args = parser.parse_args()
+  options, args = parser.parse_args(args=argv[1:])
 
   if not _CheckArguments(cmd, args_def, args):
     return None, None, None
@@ -1972,35 +2185,41 @@ def FormatError(err):
   return retcode, obuf.getvalue().rstrip("\n")
 
 
-def GenericMain(commands, override=None, aliases=None):
+def GenericMain(commands, override=None, aliases=None,
+                env_override=frozenset()):
   """Generic main function for all the gnt-* commands.
 
-  Arguments:
-    - commands: a dictionary with a special structure, see the design doc
-                for command line handling.
-    - override: if not None, we expect a dictionary with keys that will
-                override command line options; this can be used to pass
-                options from the scripts to generic functions
-    - aliases: dictionary with command aliases {'alias': 'target, ...}
+  @param commands: a dictionary with a special structure, see the design doc
+                   for command line handling.
+  @param override: if not None, we expect a dictionary with keys that will
+                   override command line options; this can be used to pass
+                   options from the scripts to generic functions
+  @param aliases: dictionary with command aliases {'alias': 'target, ...}
+  @param env_override: list of environment names which are allowed to submit
+                       default args for commands
 
   """
   # save the program name and the entire command line for later logging
   if sys.argv:
-    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
+    binary = os.path.basename(sys.argv[0])
+    if not binary:
+      binary = sys.argv[0]
+
     if len(sys.argv) >= 2:
-      binary += " " + sys.argv[1]
-      old_cmdline = " ".join(sys.argv[2:])
+      logname = utils.ShellQuoteArgs([binary, sys.argv[1]])
     else:
-      old_cmdline = ""
+      logname = binary
+
+    cmdline = utils.ShellQuoteArgs([binary] + sys.argv[1:])
   else:
     binary = "<unknown program>"
-    old_cmdline = ""
+    cmdline = "<unknown>"
 
   if aliases is None:
     aliases = {}
 
   try:
-    func, options, args = _ParseArgs(sys.argv, commands, aliases)
+    func, options, args = _ParseArgs(sys.argv, commands, aliases, env_override)
   except errors.ParameterError, err:
     result, err_msg = FormatError(err)
     ToStderr(err_msg)
@@ -2013,13 +2232,10 @@ def GenericMain(commands, override=None, aliases=None):
     for key, val in override.iteritems():
       setattr(options, key, val)
 
-  utils.SetupLogging(constants.LOG_COMMANDS, binary, debug=options.debug,
+  utils.SetupLogging(constants.LOG_COMMANDS, logname, debug=options.debug,
                      stderr_logging=True)
 
-  if old_cmdline:
-    logging.info("run with arguments '%s'", old_cmdline)
-  else:
-    logging.info("run with no arguments")
+  logging.info("Command line: %s", cmdline)
 
   try:
     result = func(options, args)
@@ -2152,7 +2368,7 @@ def GenericInstanceCreate(mode, opts, args):
   else:
     tags = []
 
-  utils.ForceDictType(opts.beparams, constants.BES_PARAMETER_TYPES)
+  utils.ForceDictType(opts.beparams, constants.BES_PARAMETER_COMPAT)
   utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES)
 
   if mode == constants.INSTANCE_CREATE:
@@ -2197,7 +2413,8 @@ def GenericInstanceCreate(mode, opts, args):
                                 src_path=src_path,
                                 tags=tags,
                                 no_install=no_install,
-                                identify_defaults=identify_defaults)
+                                identify_defaults=identify_defaults,
+                                ignore_ipolicy=opts.ignore_ipolicy)
 
   SubmitOrSend(op, opts)
   return 0
@@ -2633,7 +2850,8 @@ def _WarnUnknownFields(fdefs):
 
 
 def GenericList(resource, fields, names, unit, separator, header, cl=None,
-                format_override=None, verbose=False, force_filter=False):
+                format_override=None, verbose=False, force_filter=False,
+                namefield=None, qfilter=None):
   """Generic implementation for listing all items of a resource.
 
   @param resource: One of L{constants.QR_VIA_LUXI}
@@ -2656,17 +2874,27 @@ def GenericList(resource, fields, names, unit, separator, header, cl=None,
     indexed by field name, contents like L{_DEFAULT_FORMAT_QUERY}
   @type verbose: boolean
   @param verbose: whether to use verbose field descriptions or not
+  @type namefield: string
+  @param namefield: Name of field to use for simple filters (see
+    L{qlang.MakeFilter} for details)
+  @type qfilter: list or None
+  @param qfilter: Query filter (in addition to names)
 
   """
-  if cl is None:
-    cl = GetClient()
-
   if not names:
     names = None
 
-  filter_ = qlang.MakeFilter(names, force_filter)
+  namefilter = qlang.MakeFilter(names, force_filter, namefield=namefield)
 
-  response = cl.Query(resource, fields, filter_)
+  if qfilter is None:
+    qfilter = namefilter
+  elif namefilter is not None:
+    qfilter = [qlang.OP_AND, namefilter, qfilter]
+
+  if cl is None:
+    cl = GetClient()
+
+  response = cl.Query(resource, fields, qfilter)
 
   found_unknown = _WarnUnknownFields(response.fields)
 
@@ -2820,8 +3048,9 @@ def FormatTimestamp(ts):
   """
   if not isinstance(ts, (tuple, list)) or len(ts) != 2:
     return "?"
-  sec, usec = ts
-  return time.strftime("%F %T", time.localtime(sec)) + ".%06d" % usec
+
+  (sec, usecs) = ts
+  return utils.FormatTime(sec, usecs=usecs)
 
 
 def ParseTimespec(value):
@@ -2895,24 +3124,24 @@ def GetOnlineNodes(nodes, cl=None, nowarn=False, secondary_ips=False,
   if cl is None:
     cl = GetClient()
 
-  filter_ = []
+  qfilter = []
 
   if nodes:
-    filter_.append(qlang.MakeSimpleFilter("name", nodes))
+    qfilter.append(qlang.MakeSimpleFilter("name", nodes))
 
   if nodegroup is not None:
-    filter_.append([qlang.OP_OR, [qlang.OP_EQUAL, "group", nodegroup],
+    qfilter.append([qlang.OP_OR, [qlang.OP_EQUAL, "group", nodegroup],
                                  [qlang.OP_EQUAL, "group.uuid", nodegroup]])
 
   if filter_master:
-    filter_.append([qlang.OP_NOT, [qlang.OP_TRUE, "master"]])
+    qfilter.append([qlang.OP_NOT, [qlang.OP_TRUE, "master"]])
 
-  if filter_:
-    if len(filter_) > 1:
-      final_filter = [qlang.OP_AND] + filter_
+  if qfilter:
+    if len(qfilter) > 1:
+      final_filter = [qlang.OP_AND] + qfilter
     else:
-      assert len(filter_) == 1
-      final_filter = filter_[0]
+      assert len(qfilter) == 1
+      final_filter = qfilter[0]
   else:
     final_filter = None
 
@@ -3046,7 +3275,7 @@ class JobExecutor(object):
       for (_, _, ops) in self.queue:
         # SubmitJob will remove the success status, but raise an exception if
         # the submission fails, so we'll notice that anyway.
-        results.append([True, self.cl.SubmitJob(ops)])
+        results.append([True, self.cl.SubmitJob(ops)[0]])
     else:
       results = self.cl.SubmitManyJobs([ops for (_, _, ops) in self.queue])
     for ((status, data), (idx, name, _)) in zip(results, self.queue):
@@ -3158,9 +3387,18 @@ def FormatParameterDict(buf, param_dict, actual, level=1):
 
   """
   indent = "  " * level
+
   for key in sorted(actual):
-    val = param_dict.get(key, "default (%s)" % actual[key])
-    buf.write("%s- %s: %s\n" % (indent, key, val))
+    data = actual[key]
+    buf.write("%s- %s:" % (indent, key))
+
+    if isinstance(data, dict) and data:
+      buf.write("\n")
+      FormatParameterDict(buf, param_dict.get(key, {}), data,
+                          level=level + 1)
+    else:
+      val = param_dict.get(key, "default (%s)" % data)
+      buf.write(" %s\n" % val)
 
 
 def ConfirmOperation(names, list_type, text, extra=""):
@@ -3200,3 +3438,92 @@ def ConfirmOperation(names, list_type, text, extra=""):
     choices.pop(1)
     choice = AskUser(msg + affected, choices)
   return choice
+
+
+def _MaybeParseUnit(elements):
+  """Parses and returns an array of potential values with units.
+
+  """
+  parsed = {}
+  for k, v in elements.items():
+    if v == constants.VALUE_DEFAULT:
+      parsed[k] = v
+    else:
+      parsed[k] = utils.ParseUnit(v)
+  return parsed
+
+
+def CreateIPolicyFromOpts(ispecs_mem_size=None,
+                          ispecs_cpu_count=None,
+                          ispecs_disk_count=None,
+                          ispecs_disk_size=None,
+                          ispecs_nic_count=None,
+                          ipolicy_disk_templates=None,
+                          ipolicy_vcpu_ratio=None,
+                          ipolicy_spindle_ratio=None,
+                          group_ipolicy=False,
+                          allowed_values=None,
+                          fill_all=False):
+  """Creation of instance policy based on command line options.
+
+  @param fill_all: whether for cluster policies we should ensure that
+    all values are filled
+
+
+  """
+  try:
+    if ispecs_mem_size:
+      ispecs_mem_size = _MaybeParseUnit(ispecs_mem_size)
+    if ispecs_disk_size:
+      ispecs_disk_size = _MaybeParseUnit(ispecs_disk_size)
+  except (TypeError, ValueError, errors.UnitParseError), err:
+    raise errors.OpPrereqError("Invalid disk (%s) or memory (%s) size"
+                               " in policy: %s" %
+                               (ispecs_disk_size, ispecs_mem_size, err),
+                               errors.ECODE_INVAL)
+
+  # prepare ipolicy dict
+  ipolicy_transposed = {
+    constants.ISPEC_MEM_SIZE: ispecs_mem_size,
+    constants.ISPEC_CPU_COUNT: ispecs_cpu_count,
+    constants.ISPEC_DISK_COUNT: ispecs_disk_count,
+    constants.ISPEC_DISK_SIZE: ispecs_disk_size,
+    constants.ISPEC_NIC_COUNT: ispecs_nic_count,
+    }
+
+  # first, check that the values given are correct
+  if group_ipolicy:
+    forced_type = TISPECS_GROUP_TYPES
+  else:
+    forced_type = TISPECS_CLUSTER_TYPES
+
+  for specs in ipolicy_transposed.values():
+    utils.ForceDictType(specs, forced_type, allowed_values=allowed_values)
+
+  # then transpose
+  ipolicy_out = objects.MakeEmptyIPolicy()
+  for name, specs in ipolicy_transposed.iteritems():
+    assert name in constants.ISPECS_PARAMETERS
+    for key, val in specs.items(): # {min: .. ,max: .., std: ..}
+      ipolicy_out[key][name] = val
+
+  # no filldict for non-dicts
+  if not group_ipolicy and fill_all:
+    if ipolicy_disk_templates is None:
+      ipolicy_disk_templates = constants.DISK_TEMPLATES
+    if ipolicy_vcpu_ratio is None:
+      ipolicy_vcpu_ratio = \
+        constants.IPOLICY_DEFAULTS[constants.IPOLICY_VCPU_RATIO]
+    if ipolicy_spindle_ratio is None:
+      ipolicy_spindle_ratio = \
+        constants.IPOLICY_DEFAULTS[constants.IPOLICY_SPINDLE_RATIO]
+  if ipolicy_disk_templates is not None:
+    ipolicy_out[constants.IPOLICY_DTS] = list(ipolicy_disk_templates)
+  if ipolicy_vcpu_ratio is not None:
+    ipolicy_out[constants.IPOLICY_VCPU_RATIO] = ipolicy_vcpu_ratio
+  if ipolicy_spindle_ratio is not None:
+    ipolicy_out[constants.IPOLICY_SPINDLE_RATIO] = ipolicy_spindle_ratio
+
+  assert not (frozenset(ipolicy_out.keys()) - constants.IPOLICY_ALL_KEYS)
+
+  return ipolicy_out
index 5999e32..edc8b44 100644 (file)
@@ -30,6 +30,10 @@ from ganeti.cli import *
 from ganeti import opcodes
 from ganeti import constants
 from ganeti import errors
+from ganeti import qlang
+
+
+_LIST_DEF_FIELDS = ["node", "export"]
 
 
 def PrintExportList(opts, args):
@@ -42,18 +46,27 @@ def PrintExportList(opts, args):
   @return: the desired exit code
 
   """
-  exports = GetClient().QueryExports(opts.nodes, False)
-  retcode = 0
-  for node in exports:
-    ToStdout("Node: %s", node)
-    ToStdout("Exports:")
-    if isinstance(exports[node], list):
-      for instance_name in exports[node]:
-        ToStdout("\t%s", instance_name)
-    else:
-      ToStdout("  Could not get exports list")
-      retcode = 1
-  return retcode
+  selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
+
+  qfilter = qlang.MakeSimpleFilter("node", opts.nodes)
+
+  return GenericList(constants.QR_EXPORT, selected_fields, None, opts.units,
+                     opts.separator, not opts.no_headers,
+                     verbose=opts.verbose, qfilter=qfilter)
+
+
+def ListExportFields(opts, args):
+  """List export fields.
+
+  @param opts: the command line options selected by the user
+  @type args: list
+  @param args: fields to list, or empty for all
+  @rtype: int
+  @return: the desired exit code
+
+  """
+  return GenericListFields(constants.QR_EXPORT, args, opts.separator,
+                           not opts.no_headers)
 
 
 def ExportInstance(opts, args):
@@ -80,7 +93,7 @@ def ExportInstance(opts, args):
                               remove_instance=opts.remove_instance,
                               ignore_remove_failures=ignore_remove_failures)
 
-  SubmitOpCode(op, opts=opts)
+  SubmitOrSend(op, opts)
   return 0
 
 
@@ -106,7 +119,7 @@ def RemoveExport(opts, args):
   """
   op = opcodes.OpBackupRemove(instance_name=args[0])
 
-  SubmitOpCode(op, opts=opts)
+  SubmitOrSend(op, opts)
   return 0
 
 
@@ -115,27 +128,34 @@ import_opts = [
   IDENTIFY_DEFAULTS_OPT,
   SRC_DIR_OPT,
   SRC_NODE_OPT,
+  IGNORE_IPOLICY_OPT,
   ]
 
 
 commands = {
-  'list': (
+  "list": (
     PrintExportList, ARGS_NONE,
-    [NODE_LIST_OPT],
+    [NODE_LIST_OPT, NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, VERBOSE_OPT],
     "", "Lists instance exports available in the ganeti cluster"),
-  'export': (
+  "list-fields": (
+    ListExportFields, [ArgUnknown()],
+    [NOHDR_OPT, SEP_OPT],
+    "[fields...]",
+    "Lists all available fields for exports"),
+  "export": (
     ExportInstance, ARGS_ONE_INSTANCE,
     [FORCE_OPT, SINGLE_NODE_OPT, NOSHUTDOWN_OPT, SHUTDOWN_TIMEOUT_OPT,
      REMOVE_INSTANCE_OPT, IGNORE_REMOVE_FAILURES_OPT, DRY_RUN_OPT,
-     PRIORITY_OPT],
+     PRIORITY_OPT, SUBMIT_OPT],
     "-n <target_node> [opts...] <name>",
     "Exports an instance to an image"),
-  'import': (
+  "import": (
     ImportInstance, ARGS_ONE_INSTANCE, COMMON_CREATE_OPTS + import_opts,
     "[...] -t disk-type -n node[:secondary-node] <name>",
     "Imports an instance from an exported image"),
-  'remove': (
-    RemoveExport, [ArgUnknown(min=1, max=1)], [DRY_RUN_OPT, PRIORITY_OPT],
+  "remove": (
+    RemoveExport, [ArgUnknown(min=1, max=1)],
+    [DRY_RUN_OPT, PRIORITY_OPT, SUBMIT_OPT],
     "<name>", "Remove exports of named instance from the filesystem."),
   }
 
index 04c8bbe..de5bfb7 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007, 2010, 2011 Google Inc.
+# Copyright (C) 2006, 2007, 2010, 2011, 2012 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
@@ -52,6 +52,10 @@ GROUPS_OPT = cli_option("--groups", default=False,
                     action="store_true", dest="groups",
                     help="Arguments are node groups instead of nodes")
 
+SHOW_MACHINE_OPT = cli_option("-M", "--show-machine-names", default=False,
+                              action="store_true",
+                              help="Show machine name for every line in output")
+
 _EPO_PING_INTERVAL = 30 # 30 seconds between pings
 _EPO_PING_TIMEOUT = 1 # 1 second
 _EPO_REACHABLE_TIMEOUT = 15 * 60 # 15 minutes
@@ -98,9 +102,19 @@ def InitCluster(opts, args):
   beparams = opts.beparams
   nicparams = opts.nicparams
 
+  diskparams = dict(opts.diskparams)
+
+  # check the disk template types here, as we cannot rely on the type check done
+  # by the opcode parameter types
+  diskparams_keys = set(diskparams.keys())
+  if not (diskparams_keys <= constants.DISK_TEMPLATES):
+    unknown = utils.NiceSort(diskparams_keys - constants.DISK_TEMPLATES)
+    ToStderr("Disk templates unknown: %s" % utils.CommaJoin(unknown))
+    return 1
+
   # prepare beparams dict
   beparams = objects.FillDict(constants.BEC_DEFAULTS, beparams)
-  utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
+  utils.ForceDictType(beparams, constants.BES_PARAMETER_COMPAT)
 
   # prepare nicparams dict
   nicparams = objects.FillDict(constants.NICC_DEFAULTS, nicparams)
@@ -120,6 +134,27 @@ def InitCluster(opts, args):
     hvparams[hv] = objects.FillDict(constants.HVC_DEFAULTS[hv], hvparams[hv])
     utils.ForceDictType(hvparams[hv], constants.HVS_PARAMETER_TYPES)
 
+  # prepare diskparams dict
+  for templ in constants.DISK_TEMPLATES:
+    if templ not in diskparams:
+      diskparams[templ] = {}
+    diskparams[templ] = objects.FillDict(constants.DISK_DT_DEFAULTS[templ],
+                                         diskparams[templ])
+    utils.ForceDictType(diskparams[templ], constants.DISK_DT_TYPES)
+
+  # prepare ipolicy dict
+  ipolicy_raw = CreateIPolicyFromOpts(
+    ispecs_mem_size=opts.ispecs_mem_size,
+    ispecs_cpu_count=opts.ispecs_cpu_count,
+    ispecs_disk_count=opts.ispecs_disk_count,
+    ispecs_disk_size=opts.ispecs_disk_size,
+    ispecs_nic_count=opts.ispecs_nic_count,
+    ipolicy_disk_templates=opts.ipolicy_disk_templates,
+    ipolicy_vcpu_ratio=opts.ipolicy_vcpu_ratio,
+    ipolicy_spindle_ratio=opts.ipolicy_spindle_ratio,
+    fill_all=True)
+  ipolicy = objects.FillIPolicy(constants.IPOLICY_DEFAULTS, ipolicy_raw)
+
   if opts.candidate_pool_size is None:
     opts.candidate_pool_size = constants.MASTER_POOL_SIZE_DEFAULT
 
@@ -133,16 +168,36 @@ def InitCluster(opts, args):
   if opts.prealloc_wipe_disks is None:
     opts.prealloc_wipe_disks = False
 
+  external_ip_setup_script = opts.use_external_mip_script
+  if external_ip_setup_script is None:
+    external_ip_setup_script = False
+
   try:
     primary_ip_version = int(opts.primary_ip_version)
   except (ValueError, TypeError), err:
     ToStderr("Invalid primary ip version value: %s" % str(err))
     return 1
 
+  master_netmask = opts.master_netmask
+  try:
+    if master_netmask is not None:
+      master_netmask = int(master_netmask)
+  except (ValueError, TypeError), err:
+    ToStderr("Invalid master netmask value: %s" % str(err))
+    return 1
+
+  if opts.disk_state:
+    disk_state = utils.FlatToDict(opts.disk_state)
+  else:
+    disk_state = {}
+
+  hv_state = dict(opts.hv_state)
+
   bootstrap.InitCluster(cluster_name=args[0],
                         secondary_ip=opts.secondary_ip,
                         vg_name=vg_name,
                         mac_prefix=opts.mac_prefix,
+                        master_netmask=master_netmask,
                         master_netdev=master_netdev,
                         file_storage_dir=opts.file_storage_dir,
                         shared_file_storage_dir=opts.shared_file_storage_dir,
@@ -151,6 +206,8 @@ def InitCluster(opts, args):
                         beparams=beparams,
                         nicparams=nicparams,
                         ndparams=ndparams,
+                        diskparams=diskparams,
+                        ipolicy=ipolicy,
                         candidate_pool_size=opts.candidate_pool_size,
                         modify_etc_hosts=opts.modify_etc_hosts,
                         modify_ssh_setup=opts.modify_ssh_setup,
@@ -160,6 +217,9 @@ def InitCluster(opts, args):
                         default_iallocator=opts.default_iallocator,
                         primary_ip_version=primary_ip_version,
                         prealloc_wipe_disks=opts.prealloc_wipe_disks,
+                        use_external_mip_script=external_ip_setup_script,
+                        hv_state=hv_state,
+                        disk_state=disk_state,
                         )
   op = opcodes.OpClusterPostInit()
   SubmitOpCode(op, opts=opts)
@@ -223,6 +283,32 @@ def RenameCluster(opts, args):
   return 0
 
 
+def ActivateMasterIp(opts, args):
+  """Activates the master IP.
+
+  """
+  op = opcodes.OpClusterActivateMasterIp()
+  SubmitOpCode(op)
+  return 0
+
+
+def DeactivateMasterIp(opts, args):
+  """Deactivates the master IP.
+
+  """
+  if not opts.confirm:
+    usertext = ("This will disable the master IP. All the open connections to"
+                " the master IP will be closed. To reach the master you will"
+                " need to use its node IP."
+                " Continue?")
+    if not AskUser(usertext):
+      return 1
+
+  op = opcodes.OpClusterDeactivateMasterIp()
+  SubmitOpCode(op)
+  return 0
+
+
 def RedistributeConfig(opts, args):
   """Forces push of the cluster configuration.
 
@@ -345,6 +431,9 @@ def ShowClusterConfig(opts, args):
             compat.TryToRoman(result["candidate_pool_size"],
                               convert=opts.roman_integers))
   ToStdout("  - master netdev: %s", result["master_netdev"])
+  ToStdout("  - master netmask: %s", result["master_netmask"])
+  ToStdout("  - use external master IP address setup script: %s",
+           result["use_external_mip_script"])
   ToStdout("  - lvm volume group: %s", result["volume_group_name"])
   if result["reserved_lvs"]:
     reserved_lvs = utils.CommaJoin(result["reserved_lvs"])
@@ -374,6 +463,18 @@ def ShowClusterConfig(opts, args):
   ToStdout("Default nic parameters:")
   _PrintGroupedParams(result["nicparams"], roman=opts.roman_integers)
 
+  ToStdout("Default disk parameters:")
+  _PrintGroupedParams(result["diskparams"], roman=opts.roman_integers)
+
+  ToStdout("Instance policy - limits for instances:")
+  for key in constants.IPOLICY_ISPECS:
+    ToStdout("  - %s", key)
+    _PrintGroupedParams(result["ipolicy"][key], roman=opts.roman_integers)
+  ToStdout("  - enabled disk templates: %s",
+           utils.CommaJoin(result["ipolicy"][constants.IPOLICY_DTS]))
+  for key in constants.IPOLICY_PARAMETERS:
+    ToStdout("  - %s: %s", key, result["ipolicy"][key])
+
   return 0
 
 
@@ -438,8 +539,12 @@ def RunClusterCommand(opts, args):
   for name in nodes:
     result = srun.Run(name, "root", command)
     ToStdout("------------------------------------------------")
-    ToStdout("node: %s", name)
-    ToStdout("%s", result.output)
+    if opts.show_machine_names:
+      for line in result.output.splitlines():
+        ToStdout("%s: %s", name, line)
+    else:
+      ToStdout("node: %s", name)
+      ToStdout("%s", result.output)
     ToStdout("return code = %s", result.exit_code)
 
   return 0
@@ -466,6 +571,7 @@ def VerifyCluster(opts, args):
                                error_codes=opts.error_codes,
                                debug_simulate_errors=opts.simulate_errors,
                                skip_checks=skip_checks,
+                               ignore_errors=opts.ignore_errors,
                                group_name=opts.nodegroup)
   result = SubmitOpCode(op, cl=cl, opts=opts)
 
@@ -646,9 +752,45 @@ def SearchTags(opts, args):
     ToStdout("%s %s", path, tag)
 
 
-def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename,
-                 new_confd_hmac_key, new_cds, cds_filename,
-                 force):
+def _ReadAndVerifyCert(cert_filename, verify_private_key=False):
+  """Reads and verifies an X509 certificate.
+
+  @type cert_filename: string
+  @param cert_filename: the path of the file containing the certificate to
+                        verify encoded in PEM format
+  @type verify_private_key: bool
+  @param verify_private_key: whether to verify the private key in addition to
+                             the public certificate
+  @rtype: string
+  @return: a string containing the PEM-encoded certificate.
+
+  """
+  try:
+    pem = utils.ReadFile(cert_filename)
+  except IOError, err:
+    raise errors.X509CertError(cert_filename,
+                               "Unable to read certificate: %s" % str(err))
+
+  try:
+    OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, pem)
+  except Exception, err:
+    raise errors.X509CertError(cert_filename,
+                               "Unable to load certificate: %s" % str(err))
+
+  if verify_private_key:
+    try:
+      OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, pem)
+    except Exception, err:
+      raise errors.X509CertError(cert_filename,
+                                 "Unable to load private key: %s" % str(err))
+
+  return pem
+
+
+def _RenewCrypto(new_cluster_cert, new_rapi_cert, #pylint: disable=R0911
+                 rapi_cert_filename, new_spice_cert, spice_cert_filename,
+                 spice_cacert_filename, new_confd_hmac_key, new_cds,
+                 cds_filename, force):
   """Renews cluster certificates, keys and secrets.
 
   @type new_cluster_cert: bool
@@ -657,6 +799,13 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename,
   @param new_rapi_cert: Whether to generate a new RAPI certificate
   @type rapi_cert_filename: string
   @param rapi_cert_filename: Path to file containing new RAPI certificate
+  @type new_spice_cert: bool
+  @param new_spice_cert: Whether to generate a new SPICE certificate
+  @type spice_cert_filename: string
+  @param spice_cert_filename: Path to file containing new SPICE certificate
+  @type spice_cacert_filename: string
+  @param spice_cacert_filename: Path to file containing the certificate of the
+                                CA that signed the SPICE certificate
   @type new_confd_hmac_key: bool
   @param new_confd_hmac_key: Whether to generate a new HMAC key
   @type new_cds: bool
@@ -668,7 +817,7 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename,
 
   """
   if new_rapi_cert and rapi_cert_filename:
-    ToStderr("Only one of the --new-rapi-certficate and --rapi-certificate"
+    ToStderr("Only one of the --new-rapi-certificate and --rapi-certificate"
              " options can be specified at the same time.")
     return 1
 
@@ -678,27 +827,26 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename,
              " the same time.")
     return 1
 
-  if rapi_cert_filename:
-    # Read and verify new certificate
-    try:
-      rapi_cert_pem = utils.ReadFile(rapi_cert_filename)
-
-      OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM,
-                                      rapi_cert_pem)
-    except Exception, err: # pylint: disable=W0703
-      ToStderr("Can't load new RAPI certificate from %s: %s" %
-               (rapi_cert_filename, str(err)))
-      return 1
+  if new_spice_cert and (spice_cert_filename or spice_cacert_filename):
+    ToStderr("When using --new-spice-certificate, the --spice-certificate"
+             " and --spice-ca-certificate must not be used.")
+    return 1
 
-    try:
-      OpenSSL.crypto.load_privatekey(OpenSSL.crypto.FILETYPE_PEM, rapi_cert_pem)
-    except Exception, err: # pylint: disable=W0703
-      ToStderr("Can't load new RAPI private key from %s: %s" %
-               (rapi_cert_filename, str(err)))
-      return 1
+  if bool(spice_cacert_filename) ^ bool(spice_cert_filename):
+    ToStderr("Both --spice-certificate and --spice-ca-certificate must be"
+             " specified.")
+    return 1
 
-  else:
-    rapi_cert_pem = None
+  rapi_cert_pem, spice_cert_pem, spice_cacert_pem = (None, None, None)
+  try:
+    if rapi_cert_filename:
+      rapi_cert_pem = _ReadAndVerifyCert(rapi_cert_filename, True)
+    if spice_cert_filename:
+      spice_cert_pem = _ReadAndVerifyCert(spice_cert_filename, True)
+      spice_cacert_pem = _ReadAndVerifyCert(spice_cacert_filename)
+  except errors.X509CertError, err:
+    ToStderr("Unable to load X509 certificate from %s: %s", err[0], err[1])
+    return 1
 
   if cds_filename:
     try:
@@ -718,10 +866,14 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename,
 
   def _RenewCryptoInner(ctx):
     ctx.feedback_fn("Updating certificates and keys")
-    bootstrap.GenerateClusterCrypto(new_cluster_cert, new_rapi_cert,
+    bootstrap.GenerateClusterCrypto(new_cluster_cert,
+                                    new_rapi_cert,
+                                    new_spice_cert,
                                     new_confd_hmac_key,
                                     new_cds,
                                     rapi_cert_pem=rapi_cert_pem,
+                                    spice_cert_pem=spice_cert_pem,
+                                    spice_cacert_pem=spice_cacert_pem,
                                     cds=cds)
 
     files_to_copy = []
@@ -732,6 +884,10 @@ def _RenewCrypto(new_cluster_cert, new_rapi_cert, rapi_cert_filename,
     if new_rapi_cert or rapi_cert_pem:
       files_to_copy.append(constants.RAPI_CERT_FILE)
 
+    if new_spice_cert or spice_cert_pem:
+      files_to_copy.append(constants.SPICE_CERT_FILE)
+      files_to_copy.append(constants.SPICE_CACERT_FILE)
+
     if new_confd_hmac_key:
       files_to_copy.append(constants.CONFD_HMAC_KEY)
 
@@ -760,6 +916,9 @@ def RenewCrypto(opts, args):
   return _RenewCrypto(opts.new_cluster_cert,
                       opts.new_rapi_cert,
                       opts.rapi_cert,
+                      opts.new_spice_cert,
+                      opts.spice_cert,
+                      opts.spice_cacert,
                       opts.new_confd_hmac_key,
                       opts.new_cluster_domain_secret,
                       opts.cluster_domain_secret,
@@ -779,7 +938,8 @@ def SetClusterParams(opts, args):
   if not (not opts.lvm_storage or opts.vg_name or
           not opts.drbd_storage or opts.drbd_helper or
           opts.enabled_hypervisors or opts.hvparams or
-          opts.beparams or opts.nicparams or opts.ndparams or
+          opts.beparams or opts.nicparams or
+          opts.ndparams or opts.diskparams or
           opts.candidate_pool_size is not None or
           opts.uid_pool is not None or
           opts.maintain_node_health is not None or
@@ -788,7 +948,19 @@ def SetClusterParams(opts, args):
           opts.default_iallocator is not None or
           opts.reserved_lvs is not None or
           opts.master_netdev is not None or
-          opts.prealloc_wipe_disks is not None):
+          opts.master_netmask is not None or
+          opts.use_external_mip_script is not None or
+          opts.prealloc_wipe_disks is not None or
+          opts.hv_state or
+          opts.disk_state or
+          opts.ispecs_mem_size or
+          opts.ispecs_cpu_count or
+          opts.ispecs_disk_count or
+          opts.ispecs_disk_size or
+          opts.ispecs_nic_count or
+          opts.ipolicy_disk_templates is not None or
+          opts.ipolicy_vcpu_ratio is not None or
+          opts.ipolicy_spindle_ratio is not None):
     ToStderr("Please give at least one of the parameters.")
     return 1
 
@@ -817,8 +989,13 @@ def SetClusterParams(opts, args):
   for hv_params in hvparams.values():
     utils.ForceDictType(hv_params, constants.HVS_PARAMETER_TYPES)
 
+  diskparams = dict(opts.diskparams)
+
+  for dt_params in diskparams.values():
+    utils.ForceDictType(dt_params, constants.DISK_DT_TYPES)
+
   beparams = opts.beparams
-  utils.ForceDictType(beparams, constants.BES_PARAMETER_TYPES)
+  utils.ForceDictType(beparams, constants.BES_PARAMETER_COMPAT)
 
   nicparams = opts.nicparams
   utils.ForceDictType(nicparams, constants.NICS_PARAMETER_TYPES)
@@ -827,6 +1004,17 @@ def SetClusterParams(opts, args):
   if ndparams is not None:
     utils.ForceDictType(ndparams, constants.NDS_PARAMETER_TYPES)
 
+  ipolicy = CreateIPolicyFromOpts(
+    ispecs_mem_size=opts.ispecs_mem_size,
+    ispecs_cpu_count=opts.ispecs_cpu_count,
+    ispecs_disk_count=opts.ispecs_disk_count,
+    ispecs_disk_size=opts.ispecs_disk_size,
+    ispecs_nic_count=opts.ispecs_nic_count,
+    ipolicy_disk_templates=opts.ipolicy_disk_templates,
+    ipolicy_vcpu_ratio=opts.ipolicy_vcpu_ratio,
+    ipolicy_spindle_ratio=opts.ipolicy_spindle_ratio,
+    )
+
   mnh = opts.maintain_node_health
 
   uid_pool = opts.uid_pool
@@ -847,6 +1035,22 @@ def SetClusterParams(opts, args):
     else:
       opts.reserved_lvs = utils.UnescapeAndSplit(opts.reserved_lvs, sep=",")
 
+  if opts.master_netmask is not None:
+    try:
+      opts.master_netmask = int(opts.master_netmask)
+    except ValueError:
+      ToStderr("The --master-netmask option expects an int parameter.")
+      return 1
+
+  ext_ip_script = opts.use_external_mip_script
+
+  if opts.disk_state:
+    disk_state = utils.FlatToDict(opts.disk_state)
+  else:
+    disk_state = {}
+
+  hv_state = dict(opts.hv_state)
+
   op = opcodes.OpClusterSetParams(vg_name=vg_name,
                                   drbd_helper=drbd_helper,
                                   enabled_hypervisors=hvlist,
@@ -855,6 +1059,8 @@ def SetClusterParams(opts, args):
                                   beparams=beparams,
                                   nicparams=nicparams,
                                   ndparams=ndparams,
+                                  diskparams=diskparams,
+                                  ipolicy=ipolicy,
                                   candidate_pool_size=opts.candidate_pool_size,
                                   maintain_node_health=mnh,
                                   uid_pool=uid_pool,
@@ -863,8 +1069,13 @@ def SetClusterParams(opts, args):
                                   default_iallocator=opts.default_iallocator,
                                   prealloc_wipe_disks=opts.prealloc_wipe_disks,
                                   master_netdev=opts.master_netdev,
-                                  reserved_lvs=opts.reserved_lvs)
-  SubmitOpCode(op, opts=opts)
+                                  master_netmask=opts.master_netmask,
+                                  reserved_lvs=opts.reserved_lvs,
+                                  use_external_mip_script=ext_ip_script,
+                                  hv_state=hv_state,
+                                  disk_state=disk_state,
+                                  )
+  SubmitOrSend(op, opts)
   return 0
 
 
@@ -976,12 +1187,13 @@ def _OobPower(opts, node_list, power):
   return True
 
 
-def _InstanceStart(opts, inst_list, start):
+def _InstanceStart(opts, inst_list, start, no_remember=False):
   """Puts the instances in the list to desired state.
 
   @param opts: The command line options selected by the user
   @param inst_list: The list of instances to operate on
   @param start: True if they should be started, False for shutdown
+  @param no_remember: If the instance state should be remembered
   @return: The success of the operation (none failed)
 
   """
@@ -990,7 +1202,8 @@ def _InstanceStart(opts, inst_list, start):
     text_submit, text_success, text_failed = ("startup", "started", "starting")
   else:
     opcls = compat.partial(opcodes.OpInstanceShutdown,
-                           timeout=opts.shutdown_timeout)
+                           timeout=opts.shutdown_timeout,
+                           no_remember=no_remember)
     text_submit, text_success, text_failed = ("shutdown", "stopped", "stopping")
 
   jex = JobExecutor(opts=opts)
@@ -1170,7 +1383,7 @@ def _EpoOff(opts, node_list, inst_map):
   @return: The desired exit status
 
   """
-  if not _InstanceStart(opts, inst_map.keys(), False):
+  if not _InstanceStart(opts, inst_map.keys(), False, no_remember=True):
     ToStderr("Please investigate and stop instances manually before continuing")
     return constants.EXIT_FAILURE
 
@@ -1255,17 +1468,17 @@ def Epo(opts, args):
   else:
     return _EpoOff(opts, node_list, inst_map)
 
-
 commands = {
   "init": (
     InitCluster, [ArgHost(min=1, max=1)],
     [BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, GLOBAL_FILEDIR_OPT,
-     HVLIST_OPT, MAC_PREFIX_OPT, MASTER_NETDEV_OPT, NIC_PARAMS_OPT,
-     NOLVM_STORAGE_OPT, NOMODIFY_ETCHOSTS_OPT, NOMODIFY_SSH_SETUP_OPT,
-     SECONDARY_IP_OPT, VG_NAME_OPT, MAINTAIN_NODE_HEALTH_OPT,
-     UIDPOOL_OPT, DRBD_HELPER_OPT, NODRBD_STORAGE_OPT,
+     HVLIST_OPT, MAC_PREFIX_OPT, MASTER_NETDEV_OPT, MASTER_NETMASK_OPT,
+     NIC_PARAMS_OPT, NOLVM_STORAGE_OPT, NOMODIFY_ETCHOSTS_OPT,
+     NOMODIFY_SSH_SETUP_OPT, SECONDARY_IP_OPT, VG_NAME_OPT,
+     MAINTAIN_NODE_HEALTH_OPT, UIDPOOL_OPT, DRBD_HELPER_OPT, NODRBD_STORAGE_OPT,
      DEFAULT_IALLOCATOR_OPT, PRIMARY_IP_VERSION_OPT, PREALLOC_WIPE_DISKS_OPT,
-     NODE_PARAMS_OPT, GLOBAL_SHARED_FILEDIR_OPT],
+     NODE_PARAMS_OPT, GLOBAL_SHARED_FILEDIR_OPT, USE_EXTERNAL_MIP_SCRIPT,
+     DISK_PARAMS_OPT, HV_STATE_OPT, DISK_STATE_OPT] + INSTANCE_POLICY_OPTS,
     "[opts...] <cluster_name>", "Initialises a new cluster configuration"),
   "destroy": (
     DestroyCluster, ARGS_NONE, [YES_DOIT_OPT],
@@ -1282,7 +1495,7 @@ commands = {
   "verify": (
     VerifyCluster, ARGS_NONE,
     [VERBOSE_OPT, DEBUG_SIMERR_OPT, ERROR_CODES_OPT, NONPLUS1_OPT,
-     DRY_RUN_OPT, PRIORITY_OPT, NODEGROUP_OPT],
+     DRY_RUN_OPT, PRIORITY_OPT, NODEGROUP_OPT, IGNORE_ERRORS_OPT],
     "", "Does a check on the cluster configuration"),
   "verify-disks": (
     VerifyDisks, ARGS_NONE, [PRIORITY_OPT],
@@ -1308,7 +1521,7 @@ commands = {
     "[-n node...] <filename>", "Copies a file to all (or only some) nodes"),
   "command": (
     RunClusterCommand, [ArgCommand(min=1)],
-    [NODE_LIST_OPT, NODEGROUP_OPT],
+    [NODE_LIST_OPT, NODEGROUP_OPT, SHOW_MACHINE_OPT],
     "[-n node...] <command>", "Runs a command on all (or only some) nodes"),
   "info": (
     ShowClusterConfig, ARGS_NONE, [ROMAN_OPT],
@@ -1316,10 +1529,10 @@ commands = {
   "list-tags": (
     ListTags, ARGS_NONE, [], "", "List the tags of the cluster"),
   "add-tags": (
-    AddTags, [ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
+    AddTags, [ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT, SUBMIT_OPT],
     "tag...", "Add tags to the cluster"),
   "remove-tags": (
-    RemoveTags, [ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
+    RemoveTags, [ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT, SUBMIT_OPT],
     "tag...", "Remove tags from the cluster"),
   "search-tags": (
     SearchTags, [ArgUnknown(min=1, max=1)], [PRIORITY_OPT], "",
@@ -1338,17 +1551,21 @@ commands = {
   "modify": (
     SetClusterParams, ARGS_NONE,
     [BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, HVLIST_OPT, MASTER_NETDEV_OPT,
-     NIC_PARAMS_OPT, NOLVM_STORAGE_OPT, VG_NAME_OPT, MAINTAIN_NODE_HEALTH_OPT,
-     UIDPOOL_OPT, ADD_UIDS_OPT, REMOVE_UIDS_OPT, DRBD_HELPER_OPT,
-     NODRBD_STORAGE_OPT, DEFAULT_IALLOCATOR_OPT, RESERVED_LVS_OPT,
-     DRY_RUN_OPT, PRIORITY_OPT, PREALLOC_WIPE_DISKS_OPT, NODE_PARAMS_OPT],
+     MASTER_NETMASK_OPT, NIC_PARAMS_OPT, NOLVM_STORAGE_OPT, VG_NAME_OPT,
+     MAINTAIN_NODE_HEALTH_OPT, UIDPOOL_OPT, ADD_UIDS_OPT, REMOVE_UIDS_OPT,
+     DRBD_HELPER_OPT, NODRBD_STORAGE_OPT, DEFAULT_IALLOCATOR_OPT,
+     RESERVED_LVS_OPT, DRY_RUN_OPT, PRIORITY_OPT, PREALLOC_WIPE_DISKS_OPT,
+     NODE_PARAMS_OPT, USE_EXTERNAL_MIP_SCRIPT, DISK_PARAMS_OPT, HV_STATE_OPT,
+     DISK_STATE_OPT, SUBMIT_OPT] +
+    INSTANCE_POLICY_OPTS,
     "[opts...]",
     "Alters the parameters of the cluster"),
   "renew-crypto": (
     RenewCrypto, ARGS_NONE,
     [NEW_CLUSTER_CERT_OPT, NEW_RAPI_CERT_OPT, RAPI_CERT_OPT,
      NEW_CONFD_HMAC_KEY_OPT, FORCE_OPT,
-     NEW_CLUSTER_DOMAIN_SECRET_OPT, CLUSTER_DOMAIN_SECRET_OPT],
+     NEW_CLUSTER_DOMAIN_SECRET_OPT, CLUSTER_DOMAIN_SECRET_OPT,
+     NEW_SPICE_CERT_OPT, SPICE_CERT_OPT, SPICE_CACERT_OPT],
     "[opts...]",
     "Renews cluster certificates, keys and secrets"),
   "epo": (
@@ -1357,12 +1574,18 @@ commands = {
      SHUTDOWN_TIMEOUT_OPT, POWER_DELAY_OPT],
     "[opts...] [args]",
     "Performs an emergency power-off on given args"),
+  "activate-master-ip": (
+    ActivateMasterIp, ARGS_NONE, [], "", "Activates the master IP"),
+  "deactivate-master-ip": (
+    DeactivateMasterIp, ARGS_NONE, [CONFIRM_OPT], "",
+    "Deactivates the master IP"),
   }
 
 
 #: dictionary with aliases for commands
 aliases = {
   "masterfailover": "master-failover",
+  "show": "info",
 }
 
 
index 585db0d..ceee598 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2010, 2011 Google Inc.
+# Copyright (C) 2010, 2011, 2012 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
@@ -28,12 +28,16 @@ from ganeti.cli import *
 from ganeti import constants
 from ganeti import opcodes
 from ganeti import utils
+from cStringIO import StringIO
 
 
 #: default list of fields for L{ListGroups}
 _LIST_DEF_FIELDS = ["name", "node_cnt", "pinst_cnt", "alloc_policy", "ndparams"]
 
 
+_ENV_OVERRIDE = frozenset(["list"])
+
+
 def AddGroup(opts, args):
   """Add a node group to the cluster.
 
@@ -44,10 +48,31 @@ def AddGroup(opts, args):
   @return: the desired exit code
 
   """
+  ipolicy = CreateIPolicyFromOpts(
+    ispecs_mem_size=opts.ispecs_mem_size,
+    ispecs_cpu_count=opts.ispecs_cpu_count,
+    ispecs_disk_count=opts.ispecs_disk_count,
+    ispecs_disk_size=opts.ispecs_disk_size,
+    ispecs_nic_count=opts.ispecs_nic_count,
+    ipolicy_vcpu_ratio=opts.ipolicy_vcpu_ratio,
+    ipolicy_spindle_ratio=opts.ipolicy_spindle_ratio,
+    group_ipolicy=True)
+
   (group_name,) = args
+  diskparams = dict(opts.diskparams)
+
+  if opts.disk_state:
+    disk_state = utils.FlatToDict(opts.disk_state)
+  else:
+    disk_state = {}
+  hv_state = dict(opts.hv_state)
+
   op = opcodes.OpGroupAdd(group_name=group_name, ndparams=opts.ndparams,
-                          alloc_policy=opts.alloc_policy)
-  SubmitOpCode(op, opts=opts)
+                          alloc_policy=opts.alloc_policy,
+                          diskparams=diskparams, ipolicy=ipolicy,
+                          hv_state=hv_state,
+                          disk_state=disk_state)
+  SubmitOrSend(op, opts)
 
 
 def AssignNodes(opts, args):
@@ -65,7 +90,7 @@ def AssignNodes(opts, args):
 
   op = opcodes.OpGroupAssignNodes(group_name=group_name, nodes=node_names,
                                   force=opts.force)
-  SubmitOpCode(op, opts=opts)
+  SubmitOrSend(op, opts)
 
 
 def _FmtDict(data):
@@ -130,13 +155,58 @@ def SetGroupParams(opts, args):
   @return: the desired exit code
 
   """
-  if opts.ndparams is None and opts.alloc_policy is None:
+  allmods = [opts.ndparams, opts.alloc_policy, opts.diskparams, opts.hv_state,
+             opts.disk_state, opts.ispecs_mem_size, opts.ispecs_cpu_count,
+             opts.ispecs_disk_count, opts.ispecs_disk_size,
+             opts.ispecs_nic_count, opts.ipolicy_vcpu_ratio,
+             opts.ipolicy_spindle_ratio, opts.diskparams]
+  if allmods.count(None) == len(allmods):
     ToStderr("Please give at least one of the parameters.")
     return 1
 
+  if opts.disk_state:
+    disk_state = utils.FlatToDict(opts.disk_state)
+  else:
+    disk_state = {}
+
+  hv_state = dict(opts.hv_state)
+
+  diskparams = dict(opts.diskparams)
+
+  # set the default values
+  to_ipolicy = [
+    opts.ispecs_mem_size,
+    opts.ispecs_cpu_count,
+    opts.ispecs_disk_count,
+    opts.ispecs_disk_size,
+    opts.ispecs_nic_count,
+    ]
+  for ispec in to_ipolicy:
+    for param in ispec:
+      if isinstance(ispec[param], basestring):
+        if ispec[param].lower() == "default":
+          ispec[param] = constants.VALUE_DEFAULT
+  # create ipolicy object
+  ipolicy = CreateIPolicyFromOpts(
+    ispecs_mem_size=opts.ispecs_mem_size,
+    ispecs_cpu_count=opts.ispecs_cpu_count,
+    ispecs_disk_count=opts.ispecs_disk_count,
+    ispecs_disk_size=opts.ispecs_disk_size,
+    ispecs_nic_count=opts.ispecs_nic_count,
+    ipolicy_disk_templates=opts.ipolicy_disk_templates,
+    ipolicy_vcpu_ratio=opts.ipolicy_vcpu_ratio,
+    ipolicy_spindle_ratio=opts.ipolicy_spindle_ratio,
+    group_ipolicy=True,
+    allowed_values=[constants.VALUE_DEFAULT])
+
   op = opcodes.OpGroupSetParams(group_name=args[0],
                                 ndparams=opts.ndparams,
-                                alloc_policy=opts.alloc_policy)
+                                alloc_policy=opts.alloc_policy,
+                                hv_state=hv_state,
+                                disk_state=disk_state,
+                                diskparams=diskparams,
+                                ipolicy=ipolicy)
+
   result = SubmitOrSend(op, opts)
 
   if result:
@@ -159,7 +229,7 @@ def RemoveGroup(opts, args):
   """
   (group_name,) = args
   op = opcodes.OpGroupRemove(group_name=group_name)
-  SubmitOpCode(op, opts=opts)
+  SubmitOrSend(op, opts)
 
 
 def RenameGroup(opts, args):
@@ -174,7 +244,7 @@ def RenameGroup(opts, args):
   """
   group_name, new_name = args
   op = opcodes.OpGroupRename(group_name=group_name, new_name=new_name)
-  SubmitOpCode(op, opts=opts)
+  SubmitOrSend(op, opts)
 
 
 def EvacuateGroup(opts, args):
@@ -189,7 +259,7 @@ def EvacuateGroup(opts, args):
                                iallocator=opts.iallocator,
                                target_groups=opts.to,
                                early_release=opts.early_release)
-  result = SubmitOpCode(op, cl=cl, opts=opts)
+  result = SubmitOrSend(op, opts, cl=cl)
 
   # Keep track of submitted jobs
   jex = JobExecutor(cl=cl, opts=opts)
@@ -209,12 +279,53 @@ def EvacuateGroup(opts, args):
   return rcode
 
 
+def _FormatDict(custom, actual, level=2):
+  """Helper function to L{cli.FormatParameterDict}.
+
+  @param custom: The customized dict
+  @param actual: The fully filled dict
+
+  """
+  buf = StringIO()
+  FormatParameterDict(buf, custom, actual, level=level)
+  return buf.getvalue().rstrip("\n")
+
+
+def GroupInfo(_, args):
+  """Shows info about node group.
+
+  """
+  cl = GetClient()
+  selected_fields = ["name",
+                     "ndparams", "custom_ndparams",
+                     "diskparams", "custom_diskparams",
+                     "ipolicy", "custom_ipolicy"]
+  result = cl.QueryGroups(names=args, fields=selected_fields,
+                          use_locking=False)
+
+  for (name,
+       ndparams, custom_ndparams,
+       diskparams, custom_diskparams,
+       ipolicy, custom_ipolicy) in result:
+    ToStdout("Node group: %s" % name)
+    ToStdout("  Node parameters:")
+    ToStdout(_FormatDict(custom_ndparams, ndparams))
+    ToStdout("  Disk parameters:")
+    ToStdout(_FormatDict(custom_diskparams, diskparams))
+    ToStdout("  Instance policy:")
+    ToStdout(_FormatDict(custom_ipolicy, ipolicy))
+
+
 commands = {
   "add": (
-    AddGroup, ARGS_ONE_GROUP, [DRY_RUN_OPT, ALLOC_POLICY_OPT, NODE_PARAMS_OPT],
+    AddGroup, ARGS_ONE_GROUP,
+    [DRY_RUN_OPT, ALLOC_POLICY_OPT, NODE_PARAMS_OPT, DISK_PARAMS_OPT,
+     HV_STATE_OPT, DISK_STATE_OPT, PRIORITY_OPT,
+     SUBMIT_OPT] + INSTANCE_POLICY_OPTS,
     "<group_name>", "Add a new node group to the cluster"),
   "assign-nodes": (
-    AssignNodes, ARGS_ONE_GROUP + ARGS_MANY_NODES, [DRY_RUN_OPT, FORCE_OPT],
+    AssignNodes, ARGS_ONE_GROUP + ARGS_MANY_NODES,
+    [DRY_RUN_OPT, FORCE_OPT, PRIORITY_OPT, SUBMIT_OPT],
     "<group_name> <node>...", "Assign nodes to a group"),
   "list": (
     ListGroups, ARGS_MANY_GROUPS,
@@ -228,34 +339,39 @@ commands = {
     "Lists all available fields for node groups"),
   "modify": (
     SetGroupParams, ARGS_ONE_GROUP,
-    [DRY_RUN_OPT, SUBMIT_OPT, ALLOC_POLICY_OPT, NODE_PARAMS_OPT],
+    [DRY_RUN_OPT, SUBMIT_OPT, ALLOC_POLICY_OPT, NODE_PARAMS_OPT, HV_STATE_OPT,
+     DISK_STATE_OPT, DISK_PARAMS_OPT, PRIORITY_OPT] + INSTANCE_POLICY_OPTS,
     "<group_name>", "Alters the parameters of a node group"),
   "remove": (
-    RemoveGroup, ARGS_ONE_GROUP, [DRY_RUN_OPT],
+    RemoveGroup, ARGS_ONE_GROUP, [DRY_RUN_OPT, PRIORITY_OPT, SUBMIT_OPT],
     "[--dry-run] <group-name>",
     "Remove an (empty) node group from the cluster"),
   "rename": (
-    RenameGroup, [ArgGroup(min=2, max=2)], [DRY_RUN_OPT],
+    RenameGroup, [ArgGroup(min=2, max=2)],
+    [DRY_RUN_OPT, SUBMIT_OPT, PRIORITY_OPT],
     "[--dry-run] <group-name> <new-name>", "Rename a node group"),
   "evacuate": (
     EvacuateGroup, [ArgGroup(min=1, max=1)],
-    [TO_GROUP_OPT, IALLOCATOR_OPT, EARLY_RELEASE_OPT],
+    [TO_GROUP_OPT, IALLOCATOR_OPT, EARLY_RELEASE_OPT, SUBMIT_OPT, PRIORITY_OPT],
     "[-I <iallocator>] [--to <group>]",
     "Evacuate all instances within a group"),
   "list-tags": (
-    ListTags, ARGS_ONE_GROUP, [PRIORITY_OPT],
+    ListTags, ARGS_ONE_GROUP, [],
     "<group_name>", "List the tags of the given group"),
   "add-tags": (
     AddTags, [ArgGroup(min=1, max=1), ArgUnknown()],
-    [TAG_SRC_OPT, PRIORITY_OPT],
+    [TAG_SRC_OPT, PRIORITY_OPT, SUBMIT_OPT],
     "<group_name> tag...", "Add tags to the given group"),
   "remove-tags": (
     RemoveTags, [ArgGroup(min=1, max=1), ArgUnknown()],
-    [TAG_SRC_OPT, PRIORITY_OPT],
+    [TAG_SRC_OPT, PRIORITY_OPT, SUBMIT_OPT],
     "<group_name> tag...", "Remove tags from the given group"),
+  "info": (
+    GroupInfo, ARGS_MANY_GROUPS, [], "<group_name>", "Show group information"),
   }
 
 
 def Main():
   return GenericMain(commands,
-                     override={"tag_type": constants.TAG_NODEGROUP})
+                     override={"tag_type": constants.TAG_NODEGROUP},
+                     env_override=_ENV_OVERRIDE)
index 457a7c8..c59817b 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 Google Inc.
+# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 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
@@ -25,6 +25,7 @@
 # W0614: Unused import %s from wildcard import (since we need cli)
 # C0103: Invalid name gnt-instance
 
+import copy
 import itertools
 import simplejson
 import logging
@@ -39,6 +40,7 @@ from ganeti import errors
 from ganeti import netutils
 from ganeti import ssh
 from ganeti import objects
+from ganeti import ht
 
 
 _EXPAND_CLUSTER = "cluster"
@@ -64,6 +66,10 @@ _LIST_DEF_FIELDS = [
   ]
 
 
+_MISSING = object()
+_ENV_OVERRIDE = frozenset(["list"])
+
+
 def _ExpandMultiNames(mode, names, client=None):
   """Expand the given names using the passed mode.
 
@@ -354,7 +360,7 @@ def BatchCreate(opts, args):
                                    (elem, name, err), errors.ECODE_INVAL)
       disks.append({"size": size})
 
-    utils.ForceDictType(specs["backend"], constants.BES_PARAMETER_TYPES)
+    utils.ForceDictType(specs["backend"], constants.BES_PARAMETER_COMPAT)
     utils.ForceDictType(hvparams, constants.HVS_PARAMETER_TYPES)
 
     tmp_nics = []
@@ -598,14 +604,29 @@ def RecreateDisks(opts, args):
 
   """
   instance_name = args[0]
+
+  disks = []
+
   if opts.disks:
-    try:
-      opts.disks = [int(v) for v in opts.disks.split(",")]
-    except (ValueError, TypeError), err:
-      ToStderr("Invalid disks value: %s" % str(err))
-      return 1
-  else:
-    opts.disks = []
+    for didx, ddict in opts.disks:
+      didx = int(didx)
+
+      if not ht.TDict(ddict):
+        msg = "Invalid disk/%d value: expected dict, got %s" % (didx, ddict)
+        raise errors.OpPrereqError(msg)
+
+      if constants.IDISK_SIZE in ddict:
+        try:
+          ddict[constants.IDISK_SIZE] = \
+            utils.ParseUnit(ddict[constants.IDISK_SIZE])
+        except ValueError, err:
+          raise errors.OpPrereqError("Invalid disk size for disk %d: %s" %
+                                     (didx, err))
+
+      disks.append((didx, ddict))
+
+    # TODO: Verify modifyable parameters (already done in
+    # LUInstanceRecreateDisks, but it'd be nice to have in the client)
 
   if opts.node:
     pnode, snode = SplitNodeOption(opts.node)
@@ -616,9 +637,9 @@ def RecreateDisks(opts, args):
     nodes = []
 
   op = opcodes.OpInstanceRecreateDisks(instance_name=instance_name,
-                                       disks=opts.disks,
-                                       nodes=nodes)
+                                       disks=disks, nodes=nodes)
   SubmitOrSend(op, opts)
+
   return 0
 
 
@@ -627,8 +648,8 @@ def GrowDisk(opts, args):
 
   @param opts: the command line options selected by the user
   @type args: list
-  @param args: should contain two elements, the instance name
-      whose disks we grow and the disk name, e.g. I{sda}
+  @param args: should contain three elements, the target instance name,
+      the target disk id, and the target growth
   @rtype: int
   @return: the desired exit code
 
@@ -647,7 +668,8 @@ def GrowDisk(opts, args):
                                errors.ECODE_INVAL)
   op = opcodes.OpInstanceGrowDisk(instance_name=instance,
                                   disk=disk, amount=amount,
-                                  wait_for_sync=opts.wait_for_sync)
+                                  wait_for_sync=opts.wait_for_sync,
+                                  absolute=opts.absolute)
   SubmitOrSend(op, opts)
   return 0
 
@@ -751,7 +773,8 @@ def ReplaceDisks(opts, args):
   op = opcodes.OpInstanceReplaceDisks(instance_name=args[0], disks=disks,
                                       remote_node=new_2ndary, mode=mode,
                                       iallocator=iallocator,
-                                      early_release=opts.early_release)
+                                      early_release=opts.early_release,
+                                      ignore_ipolicy=opts.ignore_ipolicy)
   SubmitOrSend(op, opts)
   return 0
 
@@ -792,7 +815,8 @@ def FailoverInstance(opts, args):
                                   ignore_consistency=opts.ignore_consistency,
                                   shutdown_timeout=opts.shutdown_timeout,
                                   iallocator=iallocator,
-                                  target_node=target_node)
+                                  target_node=target_node,
+                                  ignore_ipolicy=opts.ignore_ipolicy)
   SubmitOrSend(op, opts, cl=cl)
   return 0
 
@@ -847,8 +871,10 @@ def MigrateInstance(opts, args):
   op = opcodes.OpInstanceMigrate(instance_name=instance_name, mode=mode,
                                  cleanup=opts.cleanup, iallocator=iallocator,
                                  target_node=target_node,
-                                 allow_failover=opts.allow_failover)
-  SubmitOpCode(op, cl=cl, opts=opts)
+                                 allow_failover=opts.allow_failover,
+                                 allow_runtime_changes=opts.allow_runtime_chgs,
+                                 ignore_ipolicy=opts.ignore_ipolicy)
+  SubmitOrSend(op, cl=cl, opts=opts)
   return 0
 
 
@@ -876,7 +902,8 @@ def MoveInstance(opts, args):
   op = opcodes.OpInstanceMove(instance_name=instance_name,
                               target_node=opts.node,
                               shutdown_timeout=opts.shutdown_timeout,
-                              ignore_consistency=opts.ignore_consistency)
+                              ignore_consistency=opts.ignore_consistency,
+                              ignore_ipolicy=opts.ignore_ipolicy)
   SubmitOrSend(op, opts, cl=cl)
   return 0
 
@@ -938,6 +965,9 @@ def _DoConsole(console, show_command, cluster_name, feedback_fn=ToStdout,
                 " URL <vnc://%s:%s/>",
                 console.instance, console.host, console.port,
                 console.display, console.host, console.port)
+  elif console.kind == constants.CONS_SPICE:
+    feedback_fn("Instance %s has SPICE listening on %s:%s", console.instance,
+                console.host, console.port)
   elif console.kind == constants.CONS_SSH:
     # Convert to string if not already one
     if isinstance(console.command, basestring):
@@ -1179,7 +1209,15 @@ def ShowInstanceConfig(opts, args):
     ##          instance["auto_balance"])
     buf.write("  Nodes:\n")
     buf.write("    - primary: %s\n" % instance["pnode"])
-    buf.write("    - secondaries: %s\n" % utils.CommaJoin(instance["snodes"]))
+    buf.write("      group: %s (UUID %s)\n" %
+              (instance["pnode_group_name"], instance["pnode_group_uuid"]))
+    buf.write("    - secondaries: %s\n" %
+              utils.CommaJoin("%s (group %s, group UUID %s)" %
+                                (name, group_name, group_uuid)
+                              for (name, group_name, group_uuid) in
+                                zip(instance["snodes"],
+                                    instance["snodes_group_names"],
+                                    instance["snodes_group_uuids"])))
     buf.write("  Operating system: %s\n" % instance["os"])
     FormatParameterDict(buf, instance["os_instance"], instance["os_actual"],
                         level=2)
@@ -1212,12 +1250,12 @@ def ShowInstanceConfig(opts, args):
     FormatParameterDict(buf, instance["hv_instance"], instance["hv_actual"],
                         level=2)
     buf.write("  Hardware:\n")
-    buf.write("    - VCPUs: %s\n" %
-              compat.TryToRoman(instance["be_actual"][constants.BE_VCPUS],
-                                convert=opts.roman_integers))
-    buf.write("    - memory: %sMiB\n" %
-              compat.TryToRoman(instance["be_actual"][constants.BE_MEMORY],
-                                convert=opts.roman_integers))
+    # deprecated "memory" value, kept for one version for compatibility
+    # TODO(ganeti 2.7) remove.
+    be_actual = copy.deepcopy(instance["be_actual"])
+    be_actual["memory"] = be_actual[constants.BE_MAXMEM]
+    FormatParameterDict(buf, instance["be_instance"], be_actual, level=2)
+    # TODO(ganeti 2.7) rework the NICs as well
     buf.write("    - NICs:\n")
     for idx, (ip, mac, mode, link) in enumerate(instance["nics"]):
       buf.write("      - nic/%d: MAC: %s, IP: %s, mode: %s, link: %s\n" %
@@ -1233,6 +1271,87 @@ def ShowInstanceConfig(opts, args):
   return retcode
 
 
+def _ConvertNicDiskModifications(mods):
+  """Converts NIC/disk modifications from CLI to opcode.
+
+  When L{opcodes.OpInstanceSetParams} was changed to support adding/removing
+  disks at arbitrary indices, its parameter format changed. This function
+  converts legacy requests (e.g. "--net add" or "--disk add:size=4G") to the
+  newer format and adds support for new-style requests (e.g. "--new 4:add").
+
+  @type mods: list of tuples
+  @param mods: Modifications as given by command line parser
+  @rtype: list of tuples
+  @return: Modifications as understood by L{opcodes.OpInstanceSetParams}
+
+  """
+  result = []
+
+  for (idx, params) in mods:
+    if idx == constants.DDM_ADD:
+      # Add item as last item (legacy interface)
+      action = constants.DDM_ADD
+      idxno = -1
+    elif idx == constants.DDM_REMOVE:
+      # Remove last item (legacy interface)
+      action = constants.DDM_REMOVE
+      idxno = -1
+    else:
+      # Modifications and adding/removing at arbitrary indices
+      try:
+        idxno = int(idx)
+      except (TypeError, ValueError):
+        raise errors.OpPrereqError("Non-numeric index '%s'" % idx,
+                                   errors.ECODE_INVAL)
+
+      add = params.pop(constants.DDM_ADD, _MISSING)
+      remove = params.pop(constants.DDM_REMOVE, _MISSING)
+      modify = params.pop(constants.DDM_MODIFY, _MISSING)
+
+      if modify is _MISSING:
+        if not (add is _MISSING or remove is _MISSING):
+          raise errors.OpPrereqError("Cannot add and remove at the same time",
+                                     errors.ECODE_INVAL)
+        elif add is not _MISSING:
+          action = constants.DDM_ADD
+        elif remove is not _MISSING:
+          action = constants.DDM_REMOVE
+        else:
+          action = constants.DDM_MODIFY
+
+      else:
+        if add is _MISSING and remove is _MISSING:
+          action = constants.DDM_MODIFY
+        else:
+          raise errors.OpPrereqError("Cannot modify and add/remove at the"
+                                     " same time", errors.ECODE_INVAL)
+
+      assert not (constants.DDMS_VALUES_WITH_MODIFY & set(params.keys()))
+
+    if action == constants.DDM_REMOVE and params:
+      raise errors.OpPrereqError("Not accepting parameters on removal",
+                                 errors.ECODE_INVAL)
+
+    result.append((action, idxno, params))
+
+  return result
+
+
+def _ParseDiskSizes(mods):
+  """Parses disk sizes in parameters.
+
+  """
+  for (action, _, params) in mods:
+    if params and constants.IDISK_SIZE in params:
+      params[constants.IDISK_SIZE] = \
+        utils.ParseUnit(params[constants.IDISK_SIZE])
+    elif action == constants.DDM_ADD:
+      raise errors.OpPrereqError("Missing required parameter 'size'",
+                                 errors.ECODE_INVAL)
+
+  return mods
+
+
 def SetInstanceParams(opts, args):
   """Modifies an instance.
 
@@ -1246,7 +1365,8 @@ def SetInstanceParams(opts, args):
 
   """
   if not (opts.nics or opts.disks or opts.disk_template or
-          opts.hvparams or opts.beparams or opts.os or opts.osparams):
+          opts.hvparams or opts.beparams or opts.os or opts.osparams or
+          opts.offline_inst or opts.online_inst or opts.runtime_mem):
     ToStderr("Please give at least one of the parameters.")
     return 1
 
@@ -1255,7 +1375,7 @@ def SetInstanceParams(opts, args):
       if opts.beparams[param].lower() == "default":
         opts.beparams[param] = constants.VALUE_DEFAULT
 
-  utils.ForceDictType(opts.beparams, constants.BES_PARAMETER_TYPES,
+  utils.ForceDictType(opts.beparams, constants.BES_PARAMETER_COMPAT,
                       allowed_values=[constants.VALUE_DEFAULT])
 
   for param in opts.hvparams:
@@ -1266,24 +1386,8 @@ def SetInstanceParams(opts, args):
   utils.ForceDictType(opts.hvparams, constants.HVS_PARAMETER_TYPES,
                       allowed_values=[constants.VALUE_DEFAULT])
 
-  for idx, (nic_op, nic_dict) in enumerate(opts.nics):
-    try:
-      nic_op = int(nic_op)
-      opts.nics[idx] = (nic_op, nic_dict)
-    except (TypeError, ValueError):
-      pass
-
-  for idx, (disk_op, disk_dict) in enumerate(opts.disks):
-    try:
-      disk_op = int(disk_op)
-      opts.disks[idx] = (disk_op, disk_dict)
-    except (TypeError, ValueError):
-      pass
-    if disk_op == constants.DDM_ADD:
-      if "size" not in disk_dict:
-        raise errors.OpPrereqError("Missing required parameter 'size'",
-                                   errors.ECODE_INVAL)
-      disk_dict["size"] = utils.ParseUnit(disk_dict["size"])
+  nics = _ConvertNicDiskModifications(opts.nics)
+  disks = _ParseDiskSizes(_ConvertNicDiskModifications(opts.disks))
 
   if (opts.disk_template and
       opts.disk_template in constants.DTS_INT_MIRROR and
@@ -1292,18 +1396,28 @@ def SetInstanceParams(opts, args):
              " specifying a secondary node")
     return 1
 
+  if opts.offline_inst:
+    offline = True
+  elif opts.online_inst:
+    offline = False
+  else:
+    offline = None
+
   op = opcodes.OpInstanceSetParams(instance_name=args[0],
-                                   nics=opts.nics,
-                                   disks=opts.disks,
+                                   nics=nics,
+                                   disks=disks,
                                    disk_template=opts.disk_template,
                                    remote_node=opts.node,
                                    hvparams=opts.hvparams,
                                    beparams=opts.beparams,
+                                   runtime_mem=opts.runtime_mem,
                                    os_name=opts.os,
                                    osparams=opts.osparams,
                                    force_variant=opts.force_variant,
                                    force=opts.force,
-                                   wait_for_sync=opts.wait_for_sync)
+                                   wait_for_sync=opts.wait_for_sync,
+                                   offline=offline,
+                                   ignore_ipolicy=opts.ignore_ipolicy)
 
   # even if here we process the result, we allow submit only
   result = SubmitOrSend(op, opts)
@@ -1329,7 +1443,7 @@ def ChangeGroup(opts, args):
                                      iallocator=opts.iallocator,
                                      target_groups=opts.to,
                                      early_release=opts.early_release)
-  result = SubmitOpCode(op, cl=cl, opts=opts)
+  result = SubmitOrSend(op, opts, cl=cl)
 
   # Keep track of submitted jobs
   jex = JobExecutor(cl=cl, opts=opts)
@@ -1402,6 +1516,7 @@ add_opts = [
   OS_OPT,
   FORCE_VARIANT_OPT,
   NO_INSTALL_OPT,
+  IGNORE_IPOLICY_OPT,
   ]
 
 commands = {
@@ -1420,7 +1535,8 @@ commands = {
   "failover": (
     FailoverInstance, ARGS_ONE_INSTANCE,
     [FORCE_OPT, IGNORE_CONSIST_OPT, SUBMIT_OPT, SHUTDOWN_TIMEOUT_OPT,
-     DRY_RUN_OPT, PRIORITY_OPT, DST_NODE_OPT, IALLOCATOR_OPT],
+     DRY_RUN_OPT, PRIORITY_OPT, DST_NODE_OPT, IALLOCATOR_OPT,
+     IGNORE_IPOLICY_OPT],
     "[-f] <instance>", "Stops the instance, changes its primary node and"
     " (if it was originally running) starts it on the new node"
     " (the secondary for mirrored instances or any node"
@@ -1428,13 +1544,14 @@ commands = {
   "migrate": (
     MigrateInstance, ARGS_ONE_INSTANCE,
     [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, CLEANUP_OPT, DRY_RUN_OPT,
-     PRIORITY_OPT, DST_NODE_OPT, IALLOCATOR_OPT, ALLOW_FAILOVER_OPT],
+     PRIORITY_OPT, DST_NODE_OPT, IALLOCATOR_OPT, ALLOW_FAILOVER_OPT,
+     IGNORE_IPOLICY_OPT, NORUNTIME_CHGS_OPT, SUBMIT_OPT],
     "[-f] <instance>", "Migrate instance to its secondary node"
     " (only for mirrored instances)"),
   "move": (
     MoveInstance, ARGS_ONE_INSTANCE,
     [FORCE_OPT, SUBMIT_OPT, SINGLE_NODE_OPT, SHUTDOWN_TIMEOUT_OPT,
-     DRY_RUN_OPT, PRIORITY_OPT, IGNORE_CONSIST_OPT],
+     DRY_RUN_OPT, PRIORITY_OPT, IGNORE_CONSIST_OPT, IGNORE_IPOLICY_OPT],
     "[-f] <instance>", "Move instance to an arbitrary node"
     " (only for instances of type file and lv)"),
   "info": (
@@ -1478,14 +1595,15 @@ commands = {
     ReplaceDisks, ARGS_ONE_INSTANCE,
     [AUTO_REPLACE_OPT, DISKIDX_OPT, IALLOCATOR_OPT, EARLY_RELEASE_OPT,
      NEW_SECONDARY_OPT, ON_PRIMARY_OPT, ON_SECONDARY_OPT, SUBMIT_OPT,
-     DRY_RUN_OPT, PRIORITY_OPT],
+     DRY_RUN_OPT, PRIORITY_OPT, IGNORE_IPOLICY_OPT],
     "[-s|-p|-n NODE|-I NAME] <instance>",
     "Replaces all disks for the instance"),
   "modify": (
     SetInstanceParams, ARGS_ONE_INSTANCE,
     [BACKEND_OPT, DISK_OPT, FORCE_OPT, HVOPTS_OPT, NET_OPT, SUBMIT_OPT,
      DISK_TEMPLATE_OPT, SINGLE_NODE_OPT, OS_OPT, FORCE_VARIANT_OPT,
-     OSPARAMS_OPT, DRY_RUN_OPT, PRIORITY_OPT, NWSYNC_OPT],
+     OSPARAMS_OPT, DRY_RUN_OPT, PRIORITY_OPT, NWSYNC_OPT, OFFLINE_INST_OPT,
+     ONLINE_INST_OPT, IGNORE_IPOLICY_OPT, RUNTIME_MEM_OPT],
     "<instance>", "Alters the parameters of an instance"),
   "shutdown": (
     GenericManyOps("shutdown", _ShutdownInstance), [ArgInstance()],
@@ -1519,28 +1637,28 @@ commands = {
     "[-f] <instance>", "Deactivate an instance's disks"),
   "recreate-disks": (
     RecreateDisks, ARGS_ONE_INSTANCE,
-    [SUBMIT_OPT, DISKIDX_OPT, NODE_PLACEMENT_OPT, DRY_RUN_OPT, PRIORITY_OPT],
+    [SUBMIT_OPT, DISK_OPT, NODE_PLACEMENT_OPT, DRY_RUN_OPT, PRIORITY_OPT],
     "<instance>", "Recreate an instance's disks"),
   "grow-disk": (
     GrowDisk,
     [ArgInstance(min=1, max=1), ArgUnknown(min=1, max=1),
      ArgUnknown(min=1, max=1)],
-    [SUBMIT_OPT, NWSYNC_OPT, DRY_RUN_OPT, PRIORITY_OPT],
+    [SUBMIT_OPT, NWSYNC_OPT, DRY_RUN_OPT, PRIORITY_OPT, ABSOLUTE_OPT],
     "<instance> <disk> <size>", "Grow an instance's disk"),
   "change-group": (
     ChangeGroup, ARGS_ONE_INSTANCE,
-    [TO_GROUP_OPT, IALLOCATOR_OPT, EARLY_RELEASE_OPT],
+    [TO_GROUP_OPT, IALLOCATOR_OPT, EARLY_RELEASE_OPT, PRIORITY_OPT, SUBMIT_OPT],
     "[-I <iallocator>] [--to <group>]", "Change group of instance"),
   "list-tags": (
-    ListTags, ARGS_ONE_INSTANCE, [PRIORITY_OPT],
+    ListTags, ARGS_ONE_INSTANCE, [],
     "<instance_name>", "List the tags of the given instance"),
   "add-tags": (
     AddTags, [ArgInstance(min=1, max=1), ArgUnknown()],
-    [TAG_SRC_OPT, PRIORITY_OPT],
+    [TAG_SRC_OPT, PRIORITY_OPT, SUBMIT_OPT],
     "<instance_name> tag...", "Add tags to the given instance"),
   "remove-tags": (
     RemoveTags, [ArgInstance(min=1, max=1), ArgUnknown()],
-    [TAG_SRC_OPT, PRIORITY_OPT],
+    [TAG_SRC_OPT, PRIORITY_OPT, SUBMIT_OPT],
     "<instance_name> tag...", "Remove tags from given instance"),
   }
 
@@ -1548,9 +1666,11 @@ commands = {
 aliases = {
   "start": "startup",
   "stop": "shutdown",
+  "show": "info",
   }
 
 
 def Main():
   return GenericMain(commands, aliases=aliases,
-                     override={"tag_type": constants.TAG_INSTANCE})
+                     override={"tag_type": constants.TAG_INSTANCE},
+                     env_override=_ENV_OVERRIDE)
index 4d888ad..7d12ab3 100644 (file)
@@ -31,6 +31,7 @@ from ganeti import constants
 from ganeti import errors
 from ganeti import utils
 from ganeti import cli
+from ganeti import qlang
 
 
 #: default list of fields for L{ListJobs}
@@ -49,6 +50,16 @@ _USER_JOB_STATUS = {
   }
 
 
+def _FormatStatus(value):
+  """Formats a job status.
+
+  """
+  try:
+    return _USER_JOB_STATUS[value]
+  except KeyError:
+    raise errors.ProgrammerError("Unknown job status code '%s'" % value)
+
+
 def ListJobs(opts, args):
   """List the jobs
 
@@ -61,60 +72,34 @@ def ListJobs(opts, args):
   """
   selected_fields = ParseFields(opts.output, _LIST_DEF_FIELDS)
 
-  output = GetClient().QueryJobs(args, selected_fields)
-  if not opts.no_headers:
-    # TODO: Implement more fields
-    headers = {
-      "id": "ID",
-      "status": "Status",
-      "priority": "Prio",
-      "ops": "OpCodes",
-      "opresult": "OpCode_result",
-      "opstatus": "OpCode_status",
-      "oplog": "OpCode_log",
-      "summary": "Summary",
-      "opstart": "OpCode_start",
-      "opexec": "OpCode_exec",
-      "opend": "OpCode_end",
-      "oppriority": "OpCode_prio",
-      "start_ts": "Start",
-      "end_ts": "End",
-      "received_ts": "Received",
-      }
-  else:
-    headers = None
+  fmtoverride = {
+    "status": (_FormatStatus, False),
+    "summary": (lambda value: ",".join(str(item) for item in value), False),
+    }
+  fmtoverride.update(dict.fromkeys(["opstart", "opexec", "opend"],
+    (lambda value: map(FormatTimestamp, value), None)))
 
-  numfields = ["priority"]
+  qfilter = qlang.MakeSimpleFilter("status", opts.status_filter)
 
-  # change raw values to nicer strings
-  for row_id, row in enumerate(output):
-    if row is None:
-      ToStderr("No such job: %s" % args[row_id])
-      continue
+  return GenericList(constants.QR_JOB, selected_fields, args, None,
+                     opts.separator, not opts.no_headers,
+                     format_override=fmtoverride, verbose=opts.verbose,
+                     force_filter=opts.force_filter, namefield="id",
+                     qfilter=qfilter)
 
-    for idx, field in enumerate(selected_fields):
-      val = row[idx]
-      if field == "status":
-        if val in _USER_JOB_STATUS:
-          val = _USER_JOB_STATUS[val]
-        else:
-          raise errors.ProgrammerError("Unknown job status code '%s'" % val)
-      elif field == "summary":
-        val = ",".join(val)
-      elif field in ("start_ts", "end_ts", "received_ts"):
-        val = FormatTimestamp(val)
-      elif field in ("opstart", "opexec", "opend"):
-        val = [FormatTimestamp(entry) for entry in val]
-
-      row[idx] = str(val)
-
-  data = GenerateTable(separator=opts.separator, headers=headers,
-                       fields=selected_fields, data=output,
-                       numfields=numfields)
-  for line in data:
-    ToStdout(line)
 
-  return 0
+def ListJobFields(opts, args):
+  """List job fields.
+
+  @param opts: the command line options selected by the user
+  @type args: list
+  @param args: fields to list, or empty for all
+  @rtype: int
+  @return: the desired exit code
+
+  """
+  return GenericListFields(constants.QR_JOB, args, opts.separator,
+                           not opts.no_headers)
 
 
 def ArchiveJobs(opts, args):
@@ -218,27 +203,26 @@ def ShowJobs(opts, args):
     "opstart", "opexec", "opend", "received_ts", "start_ts", "end_ts",
     ]
 
-  result = GetClient().QueryJobs(args, selected_fields)
+  result = GetClient().Query(constants.QR_JOB, selected_fields,
+                             qlang.MakeSimpleFilter("id", args)).data
 
   first = True
 
-  for idx, entry in enumerate(result):
+  for entry in result:
     if not first:
       format_msg(0, "")
     else:
       first = False
 
-    if entry is None:
-      if idx <= len(args):
-        format_msg(0, "Job ID %s not found" % args[idx])
-      else:
-        # this should not happen, when we don't pass args it will be a
-        # valid job returned
-        format_msg(0, "Job ID requested as argument %s not found" % (idx + 1))
+    ((_, job_id), (rs_status, status), (_, ops), (_, opresult), (_, opstatus),
+     (_, oplog), (_, opstart), (_, opexec), (_, opend), (_, recv_ts),
+     (_, start_ts), (_, end_ts)) = entry
+
+    # Detect non-normal results
+    if rs_status != constants.RS_NORMAL:
+      format_msg(0, "Job ID %s not found" % job_id)
       continue
 
-    (job_id, status, ops, opresult, opstatus, oplog,
-     opstart, opexec, opend, recv_ts, start_ts, end_ts) = entry
     format_msg(0, "Job ID: %s" % job_id)
     if status in _USER_JOB_STATUS:
       status = _USER_JOB_STATUS[status]
@@ -356,16 +340,53 @@ def WatchJob(opts, args):
   return retcode
 
 
+_PENDING_OPT = \
+  cli_option("--pending", default=None,
+             action="store_const", dest="status_filter",
+             const=frozenset([
+               constants.JOB_STATUS_QUEUED,
+               constants.JOB_STATUS_WAITING,
+               ]),
+             help="Show only jobs pending execution")
+
+_RUNNING_OPT = \
+  cli_option("--running", default=None,
+             action="store_const", dest="status_filter",
+             const=frozenset([
+               constants.JOB_STATUS_RUNNING,
+               ]),
+             help="Show jobs currently running only")
+
+_ERROR_OPT = \
+  cli_option("--error", default=None,
+             action="store_const", dest="status_filter",
+             const=frozenset([
+               constants.JOB_STATUS_ERROR,
+               ]),
+             help="Show failed jobs only")
+
+_FINISHED_OPT = \
+  cli_option("--finished", default=None,
+             action="store_const", dest="status_filter",
+             const=constants.JOBS_FINALIZED,
+             help="Show finished jobs only")
+
+
 commands = {
   "list": (
     ListJobs, [ArgJobId()],
-    [NOHDR_OPT, SEP_OPT, FIELDS_OPT],
+    [NOHDR_OPT, SEP_OPT, FIELDS_OPT, VERBOSE_OPT, FORCE_FILTER_OPT,
+     _PENDING_OPT, _RUNNING_OPT, _ERROR_OPT, _FINISHED_OPT],
     "[job_id ...]",
-    "List the jobs and their status. The available fields are"
-    " (see the man page for details): id, status, op_list,"
-    " op_status, op_result."
-    " The default field"
-    " list is (in order): %s." % utils.CommaJoin(_LIST_DEF_FIELDS)),
+    "Lists the jobs and their status. The available fields can be shown"
+    " using the \"list-fields\" command (see the man page for details)."
+    " The default field list is (in order): %s." %
+    utils.CommaJoin(_LIST_DEF_FIELDS)),
+  "list-fields": (
+    ListJobFields, [ArgUnknown()],
+    [NOHDR_OPT, SEP_OPT],
+    "[fields...]",
+    "Lists all available fields for jobs"),
   "archive": (
     ArchiveJobs, [ArgJobId(min=1)], [],
     "<job-id> [<job-id> ...]", "Archive specified jobs"),
index 213aac0..cbf9883 100644 (file)
@@ -38,6 +38,8 @@ from ganeti import errors
 from ganeti import netutils
 from cStringIO import StringIO
 
+from ganeti import confd
+from ganeti.confd import client as confd_client
 
 #: default list of field for L{ListNodes}
 _LIST_DEF_FIELDS = [
@@ -106,6 +108,9 @@ _OOB_COMMAND_ASK = frozenset([constants.OOB_POWER_OFF,
                               constants.OOB_POWER_CYCLE])
 
 
+_ENV_OVERRIDE = frozenset(["list"])
+
+
 NONODE_SETUP_OPT = cli_option("--no-node-setup", default=True,
                               action="store_false", dest="node_setup",
                               help=("Do not make initial SSH setup on remote"
@@ -135,6 +140,9 @@ def _RunSetupSSH(options, nodes):
   @param nodes: The nodes to setup
 
   """
+
+  assert nodes, "Empty node list"
+
   cmd = [constants.SETUP_SSH]
 
   # Pass --debug|--verbose to the external script if set on our invocation
@@ -213,10 +221,19 @@ def AddNode(opts, args):
 
   bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
 
+  if opts.disk_state:
+    disk_state = utils.FlatToDict(opts.disk_state)
+  else:
+    disk_state = {}
+
+  hv_state = dict(opts.hv_state)
+
   op = opcodes.OpNodeAdd(node_name=args[0], secondary_ip=sip,
                          readd=opts.readd, group=opts.nodegroup,
                          vm_capable=opts.vm_capable, ndparams=opts.ndparams,
-                         master_capable=opts.master_capable)
+                         master_capable=opts.master_capable,
+                         disk_state=disk_state,
+                         hv_state=hv_state)
   SubmitOpCode(op, opts=opts)
 
 
@@ -276,11 +293,11 @@ def EvacuateNode(opts, args):
                                " --secondary-only options can be passed",
                                errors.ECODE_INVAL)
   elif opts.primary_only:
-    mode = constants.IALLOCATOR_NEVAC_PRI
+    mode = constants.NODE_EVAC_PRI
   elif opts.secondary_only:
-    mode = constants.IALLOCATOR_NEVAC_SEC
+    mode = constants.NODE_EVAC_SEC
   else:
-    mode = constants.IALLOCATOR_NEVAC_ALL
+    mode = constants.NODE_EVAC_ALL
 
   # Determine affected instances
   fields = []
@@ -312,7 +329,7 @@ def EvacuateNode(opts, args):
                               remote_node=opts.dst_node,
                               iallocator=opts.iallocator,
                               early_release=opts.early_release)
-  result = SubmitOpCode(op, cl=cl, opts=opts)
+  result = SubmitOrSend(op, opts, cl=cl)
 
   # Keep track of submitted jobs
   jex = JobExecutor(cl=cl, opts=opts)
@@ -414,9 +431,11 @@ def MigrateNode(opts, args):
 
   op = opcodes.OpNodeMigrate(node_name=args[0], mode=mode,
                              iallocator=opts.iallocator,
-                             target_node=opts.dst_node)
+                             target_node=opts.dst_node,
+                             allow_runtime_changes=opts.allow_runtime_chgs,
+                             ignore_ipolicy=opts.ignore_ipolicy)
 
-  result = SubmitOpCode(op, cl=cl, opts=opts)
+  result = SubmitOrSend(op, opts, cl=cl)
 
   # Keep track of submitted jobs
   jex = JobExecutor(cl=cl, opts=opts)
@@ -523,7 +542,7 @@ def PowercycleNode(opts, args):
     return 2
 
   op = opcodes.OpNodePowercycle(node_name=node, force=opts.force)
-  result = SubmitOpCode(op, opts=opts)
+  result = SubmitOrSend(op, opts)
   if result:
     ToStderr(result)
   return 0
@@ -785,7 +804,7 @@ def ModifyStorage(opts, args):
                                      storage_type=storage_type,
                                      name=volume_name,
                                      changes=changes)
-    SubmitOpCode(op, opts=opts)
+    SubmitOrSend(op, opts)
   else:
     ToStderr("No changes to perform, exiting.")
 
@@ -808,7 +827,7 @@ def RepairStorage(opts, args):
                                    storage_type=storage_type,
                                    name=volume_name,
                                    ignore_consistency=opts.ignore_consistency)
-  SubmitOpCode(op, opts=opts)
+  SubmitOrSend(op, opts)
 
 
 def SetNodeParams(opts, args):
@@ -824,10 +843,18 @@ def SetNodeParams(opts, args):
   all_changes = [opts.master_candidate, opts.drained, opts.offline,
                  opts.master_capable, opts.vm_capable, opts.secondary_ip,
                  opts.ndparams]
-  if all_changes.count(None) == len(all_changes):
+  if (all_changes.count(None) == len(all_changes) and
+      not (opts.hv_state or opts.disk_state)):
     ToStderr("Please give at least one of the parameters.")
     return 1
 
+  if opts.disk_state:
+    disk_state = utils.FlatToDict(opts.disk_state)
+  else:
+    disk_state = {}
+
+  hv_state = dict(opts.hv_state)
+
   op = opcodes.OpNodeSetParams(node_name=args[0],
                                master_candidate=opts.master_candidate,
                                offline=opts.offline,
@@ -838,7 +865,9 @@ def SetNodeParams(opts, args):
                                force=opts.force,
                                ndparams=opts.ndparams,
                                auto_promote=opts.auto_promote,
-                               powered=opts.node_powered)
+                               powered=opts.node_powered,
+                               hv_state=hv_state,
+                               disk_state=disk_state)
 
   # even if here we process the result, we allow submit only
   result = SubmitOrSend(op, opts)
@@ -850,12 +879,108 @@ def SetNodeParams(opts, args):
   return 0
 
 
+class ReplyStatus(object):
+  """Class holding a reply status for synchronous confd clients.
+
+  """
+  def __init__(self):
+    self.failure = True
+    self.answer = False
+
+
+def ListDrbd(opts, args):
+  """Modifies a node.
+
+  @param opts: the command line options selected by the user
+  @type args: list
+  @param args: should contain only one element, the node name
+  @rtype: int
+  @return: the desired exit code
+
+  """
+  if len(args) != 1:
+    ToStderr("Please give one (and only one) node.")
+    return constants.EXIT_FAILURE
+
+  if not constants.ENABLE_CONFD:
+    ToStderr("Error: this command requires confd support, but it has not"
+             " been enabled at build time.")
+    return constants.EXIT_FAILURE
+
+  if not constants.HS_CONFD:
+    ToStderr("Error: this command requires the Haskell version of confd,"
+             " but it has not been enabled at build time.")
+    return constants.EXIT_FAILURE
+
+  status = ReplyStatus()
+
+  def ListDrbdConfdCallback(reply):
+    """Callback for confd queries"""
+    if reply.type == confd_client.UPCALL_REPLY:
+      answer = reply.server_reply.answer
+      reqtype = reply.orig_request.type
+      if reqtype == constants.CONFD_REQ_NODE_DRBD:
+        if reply.server_reply.status != constants.CONFD_REPL_STATUS_OK:
+          ToStderr("Query gave non-ok status '%s': %s" %
+                   (reply.server_reply.status,
+                    reply.server_reply.answer))
+          status.failure = True
+          return
+        if not confd.HTNodeDrbd(answer):
+          ToStderr("Invalid response from server: expected %s, got %s",
+                   confd.HTNodeDrbd, answer)
+          status.failure = True
+        else:
+          status.failure = False
+          status.answer = answer
+      else:
+        ToStderr("Unexpected reply %s!?", reqtype)
+        status.failure = True
+
+  node = args[0]
+  hmac = utils.ReadFile(constants.CONFD_HMAC_KEY)
+  filter_callback = confd_client.ConfdFilterCallback(ListDrbdConfdCallback)
+  counting_callback = confd_client.ConfdCountingCallback(filter_callback)
+  cf_client = confd_client.ConfdClient(hmac, [constants.IP4_ADDRESS_LOCALHOST],
+                                       counting_callback)
+  req = confd_client.ConfdClientRequest(type=constants.CONFD_REQ_NODE_DRBD,
+                                        query=node)
+
+  def DoConfdRequestReply(req):
+    counting_callback.RegisterQuery(req.rsalt)
+    cf_client.SendRequest(req, async=False)
+    while not counting_callback.AllAnswered():
+      if not cf_client.ReceiveReply():
+        ToStderr("Did not receive all expected confd replies")
+        break
+
+  DoConfdRequestReply(req)
+
+  if status.failure:
+    return constants.EXIT_FAILURE
+
+  fields = ["node", "minor", "instance", "disk", "role", "peer"]
+  if opts.no_headers:
+    headers = None
+  else:
+    headers = {"node": "Node", "minor": "Minor", "instance": "Instance",
+               "disk": "Disk", "role": "Role", "peer": "PeerNode"}
+
+  data = GenerateTable(separator=opts.separator, headers=headers,
+                       fields=fields, data=sorted(status.answer),
+                       numfields=["minor"])
+  for line in data:
+    ToStdout(line)
+
+  return constants.EXIT_SUCCESS
+
 commands = {
   "add": (
     AddNode, [ArgHost(min=1, max=1)],
     [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NODE_FORCE_JOIN_OPT,
      NONODE_SETUP_OPT, VERBOSE_OPT, NODEGROUP_OPT, PRIORITY_OPT,
-     CAPAB_MASTER_OPT, CAPAB_VM_OPT, NODE_PARAMS_OPT],
+     CAPAB_MASTER_OPT, CAPAB_VM_OPT, NODE_PARAMS_OPT, HV_STATE_OPT,
+     DISK_STATE_OPT],
     "[-s ip] [--readd] [--no-ssh-key-check] [--force-join]"
     " [--no-node-setup] [--verbose]"
     " <node_name>",
@@ -863,8 +988,8 @@ commands = {
   "evacuate": (
     EvacuateNode, ARGS_ONE_NODE,
     [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT,
-     PRIORITY_OPT, PRIMARY_ONLY_OPT, SECONDARY_ONLY_OPT],
-    "[-f] {-I <iallocator> | -n <dst>} <node>",
+     PRIORITY_OPT, PRIMARY_ONLY_OPT, SECONDARY_ONLY_OPT, SUBMIT_OPT],
+    "[-f] {-I <iallocator> | -n <dst>} [-p | -s] [options...] <node>",
     "Relocate the primary and/or secondary instances from a node"),
   "failover": (
     FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT,
@@ -875,7 +1000,8 @@ commands = {
   "migrate": (
     MigrateNode, ARGS_ONE_NODE,
     [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, DST_NODE_OPT,
-     IALLOCATOR_OPT, PRIORITY_OPT],
+     IALLOCATOR_OPT, PRIORITY_OPT, IGNORE_IPOLICY_OPT,
+     NORUNTIME_CHGS_OPT, SUBMIT_OPT, PRIORITY_OPT],
     "[-f] <node>",
     "Migrate all the primary instance on a node away from it"
     " (only for instances of type drbd)"),
@@ -901,11 +1027,11 @@ commands = {
     [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT,
      CAPAB_MASTER_OPT, CAPAB_VM_OPT, SECONDARY_IP_OPT,
      AUTO_PROMOTE_OPT, DRY_RUN_OPT, PRIORITY_OPT, NODE_PARAMS_OPT,
-     NODE_POWERED_OPT],
+     NODE_POWERED_OPT, HV_STATE_OPT, DISK_STATE_OPT],
     "<node_name>", "Alters the parameters of a node"),
   "powercycle": (
     PowercycleNode, ARGS_ONE_NODE,
-    [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT],
+    [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT, SUBMIT_OPT],
     "<node_name>", "Tries to forcefully powercycle a node"),
   "power": (
     PowerNode,
@@ -934,32 +1060,44 @@ commands = {
     [ArgNode(min=1, max=1),
      ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
      ArgFile(min=1, max=1)],
-    [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT],
+    [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT, SUBMIT_OPT],
     "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
   "repair-storage": (
     RepairStorage,
     [ArgNode(min=1, max=1),
      ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
      ArgFile(min=1, max=1)],
-    [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT],
+    [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT, SUBMIT_OPT],
     "<node_name> <storage_type> <name>",
     "Repairs a storage volume on a node"),
   "list-tags": (
     ListTags, ARGS_ONE_NODE, [],
     "<node_name>", "List the tags of the given node"),
   "add-tags": (
-    AddTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
+    AddTags, [ArgNode(min=1, max=1), ArgUnknown()],
+    [TAG_SRC_OPT, PRIORITY_OPT, SUBMIT_OPT],
     "<node_name> tag...", "Add tags to the given node"),
   "remove-tags": (
     RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()],
-    [TAG_SRC_OPT, PRIORITY_OPT],
+    [TAG_SRC_OPT, PRIORITY_OPT, SUBMIT_OPT],
     "<node_name> tag...", "Remove tags from the given node"),
   "health": (
     Health, ARGS_MANY_NODES,
-    [NOHDR_OPT, SEP_OPT, SUBMIT_OPT, PRIORITY_OPT, OOB_TIMEOUT_OPT],
+    [NOHDR_OPT, SEP_OPT, PRIORITY_OPT, OOB_TIMEOUT_OPT],
     "[<node_name>...]", "List health of node(s) using out-of-band"),
+  "list-drbd": (
+    ListDrbd, ARGS_ONE_NODE,
+    [NOHDR_OPT, SEP_OPT],
+    "[<node_name>]", "Query the list of used DRBD minors on the given node"),
+  }
+
+#: dictionary with aliases for commands
+aliases = {
+  "show": "info",
   }
 
 
 def Main():
-  return GenericMain(commands, override={"tag_type": constants.TAG_NODE})
+  return GenericMain(commands, aliases=aliases,
+                     override={"tag_type": constants.TAG_NODE},
+                     env_override=_ENV_OVERRIDE)
index 3554524..0ffa9ac 100644 (file)
@@ -274,7 +274,7 @@ def ModifyOS(opts, args):
                                   osparams=osp,
                                   hidden_os=ohid,
                                   blacklisted_os=oblk)
-  SubmitOpCode(op, opts=opts)
+  SubmitOrSend(op, opts)
 
   return 0
 
@@ -293,10 +293,15 @@ commands = {
   "modify": (
     ModifyOS, ARGS_ONE_OS,
     [HVLIST_OPT, OSPARAMS_OPT, DRY_RUN_OPT, PRIORITY_OPT,
-     HID_OS_OPT, BLK_OS_OPT],
+     HID_OS_OPT, BLK_OS_OPT, SUBMIT_OPT],
     "", "Modify the OS parameters"),
   }
 
+#: dictionary with aliases for commands
+aliases = {
+  "show": "info",
+  }
+
 
 def Main():
-  return GenericMain(commands)
+  return GenericMain(commands, aliases=aliases)
index 2f31fe9..92a6cd6 100644 (file)
 # W0201 since most LU attributes are defined in CheckPrereq or similar
 # functions
 
-# C0302: since we have waaaay to many lines in this module
+# C0302: since we have waaaay too many lines in this module
 
 import os
 import os.path
 import time
 import re
-import platform
 import logging
 import copy
 import OpenSSL
@@ -59,15 +58,31 @@ from ganeti import query
 from ganeti import qlang
 from ganeti import opcodes
 from ganeti import ht
+from ganeti import rpc
+from ganeti import runtime
 
 import ganeti.masterd.instance # pylint: disable=W0611
 
 
+#: Size of DRBD meta block device
+DRBD_META_SIZE = 128
+
+# States of instance
+INSTANCE_DOWN = [constants.ADMINST_DOWN]
+INSTANCE_ONLINE = [constants.ADMINST_DOWN, constants.ADMINST_UP]
+INSTANCE_NOT_RUNNING = [constants.ADMINST_DOWN, constants.ADMINST_OFFLINE]
+
+#: Instance status in which an instance can be marked as offline/online
+CAN_CHANGE_INSTANCE_OFFLINE = (frozenset(INSTANCE_DOWN) | frozenset([
+  constants.ADMINST_OFFLINE,
+  ]))
+
+
 class ResultWithJobs:
   """Data container for LU results with jobs.
 
   Instances of this class returned from L{LogicalUnit.Exec} will be recognized
-  by L{mcpu.Processor._ProcessResult}. The latter will then submit the jobs
+  by L{mcpu._ProcessResult}. The latter will then submit the jobs
   contained in the C{jobs} attribute and include the job IDs in the opcode
   result.
 
@@ -108,7 +123,7 @@ class LogicalUnit(object):
   HTYPE = None
   REQ_BGL = True
 
-  def __init__(self, processor, op, context, rpc):
+  def __init__(self, processor, op, context, rpc_runner):
     """Constructor for LogicalUnit.
 
     This needs to be overridden in derived classes in order to check op
@@ -122,7 +137,7 @@ class LogicalUnit(object):
     # readability alias
     self.owned_locks = context.glm.list_owned
     self.context = context
-    self.rpc = rpc
+    self.rpc = rpc_runner
     # Dicts used to declare locking needs to mcpu
     self.needed_locks = None
     self.share_locks = dict.fromkeys(locking.LEVELS, 0)
@@ -181,9 +196,15 @@ class LogicalUnit(object):
     as values. Rules:
 
       - use an empty dict if you don't need any lock
-      - if you don't need any lock at a particular level omit that level
+      - if you don't need any lock at a particular level omit that
+        level (note that in this case C{DeclareLocks} won't be called
+        at all for that level)
+      - if you need locks at a level, but you can't calculate it in
+        this function, initialise that level with an empty list and do
+        further processing in L{LogicalUnit.DeclareLocks} (see that
+        function's docstring)
       - don't put anything for the BGL level
-      - if you want all locks at a level use locking.ALL_SET as a value
+      - if you want all locks at a level use L{locking.ALL_SET} as a value
 
     If you need to share locks (rather than acquire them exclusively) at one
     level you can modify self.share_locks, setting a true value (usually 1) for
@@ -230,7 +251,7 @@ class LogicalUnit(object):
     self.needed_locks for the level.
 
     @param level: Locking level which is going to be locked
-    @type level: member of ganeti.locking.LEVELS
+    @type level: member of L{ganeti.locking.LEVELS}
 
     """
 
@@ -344,7 +365,8 @@ class LogicalUnit(object):
                                                 self.op.instance_name)
     self.needed_locks[locking.LEVEL_INSTANCE] = self.op.instance_name
 
-  def _LockInstancesNodes(self, primary_only=False):
+  def _LockInstancesNodes(self, primary_only=False,
+                          level=locking.LEVEL_NODE):
     """Helper function to declare instances' nodes for locking.
 
     This function should be called after locking one or more instances to lock
@@ -365,9 +387,10 @@ class LogicalUnit(object):
 
     @type primary_only: boolean
     @param primary_only: only lock primary nodes of locked instances
+    @param level: Which lock level to use for locking nodes
 
     """
-    assert locking.LEVEL_NODE in self.recalculate_locks, \
+    assert level in self.recalculate_locks, \
       "_LockInstancesNodes helper function called with no nodes to recalculate"
 
     # TODO: check if we're really been called with the instance locks held
@@ -382,12 +405,14 @@ class LogicalUnit(object):
       if not primary_only:
         wanted_nodes.extend(instance.secondary_nodes)
 
-    if self.recalculate_locks[locking.LEVEL_NODE] == constants.LOCKS_REPLACE:
-      self.needed_locks[locking.LEVEL_NODE] = wanted_nodes
-    elif self.recalculate_locks[locking.LEVEL_NODE] == constants.LOCKS_APPEND:
-      self.needed_locks[locking.LEVEL_NODE].extend(wanted_nodes)
+    if self.recalculate_locks[level] == constants.LOCKS_REPLACE:
+      self.needed_locks[level] = wanted_nodes
+    elif self.recalculate_locks[level] == constants.LOCKS_APPEND:
+      self.needed_locks[level].extend(wanted_nodes)
+    else:
+      raise errors.ProgrammerError("Unknown recalculation mode")
 
-    del self.recalculate_locks[locking.LEVEL_NODE]
+    del self.recalculate_locks[level]
 
 
 class NoHooksLU(LogicalUnit): # pylint: disable=W0223
@@ -468,14 +493,17 @@ class _QueryBase:
   #: Attribute holding field definitions
   FIELDS = None
 
-  def __init__(self, filter_, fields, use_locking):
+  #: Field to sort by
+  SORT_FIELD = "name"
+
+  def __init__(self, qfilter, fields, use_locking):
     """Initializes this class.
 
     """
     self.use_locking = use_locking
 
-    self.query = query.Query(self.FIELDS, fields, filter_=filter_,
-                             namefield="name")
+    self.query = query.Query(self.FIELDS, fields, qfilter=qfilter,
+                             namefield=self.SORT_FIELD)
     self.requested_data = self.query.RequestedData()
     self.names = self.query.RequestedNames()
 
@@ -557,6 +585,61 @@ def _ShareAll():
   return dict.fromkeys(locking.LEVELS, 1)
 
 
+def _MakeLegacyNodeInfo(data):
+  """Formats the data returned by L{rpc.RpcRunner.call_node_info}.
+
+  Converts the data into a single dictionary. This is fine for most use cases,
+  but some require information from more than one volume group or hypervisor.
+
+  """
+  (bootid, (vg_info, ), (hv_info, )) = data
+
+  return utils.JoinDisjointDicts(utils.JoinDisjointDicts(vg_info, hv_info), {
+    "bootid": bootid,
+    })
+
+
+def _AnnotateDiskParams(instance, devs, cfg):
+  """Little helper wrapper to the rpc annotation method.
+
+  @param instance: The instance object
+  @type devs: List of L{objects.Disk}
+  @param devs: The root devices (not any of its children!)
+  @param cfg: The config object
+  @returns The annotated disk copies
+  @see L{rpc.AnnotateDiskParams}
+
+  """
+  return rpc.AnnotateDiskParams(instance.disk_template, devs,
+                                cfg.GetInstanceDiskParams(instance))
+
+
+def _CheckInstancesNodeGroups(cfg, instances, owned_groups, owned_nodes,
+                              cur_group_uuid):
+  """Checks if node groups for locked instances are still correct.
+
+  @type cfg: L{config.ConfigWriter}
+  @param cfg: Cluster configuration
+  @type instances: dict; string as key, L{objects.Instance} as value
+  @param instances: Dictionary, instance name as key, instance object as value
+  @type owned_groups: iterable of string
+  @param owned_groups: List of owned groups
+  @type owned_nodes: iterable of string
+  @param owned_nodes: List of owned nodes
+  @type cur_group_uuid: string or None
+  @param cur_group_uuid: Optional group UUID to check against instance's groups
+
+  """
+  for (name, inst) in instances.items():
+    assert owned_nodes.issuperset(inst.all_nodes), \
+      "Instance %s's nodes changed while we kept the lock" % name
+
+    inst_groups = _CheckInstanceNodeGroups(cfg, name, owned_groups)
+
+    assert cur_group_uuid is None or cur_group_uuid in inst_groups, \
+      "Instance %s has no node in group %s" % (name, cur_group_uuid)
+
+
 def _CheckInstanceNodeGroups(cfg, instance_name, owned_groups):
   """Checks if the owned node groups are still correct for an instance.
 
@@ -691,6 +774,119 @@ def _GetUpdatedParams(old_params, update_dict,
   return params_copy
 
 
+def _GetUpdatedIPolicy(old_ipolicy, new_ipolicy, group_policy=False):
+  """Return the new version of a instance policy.
+
+  @param group_policy: whether this policy applies to a group and thus
+    we should support removal of policy entries
+
+  """
+  use_none = use_default = group_policy
+  ipolicy = copy.deepcopy(old_ipolicy)
+  for key, value in new_ipolicy.items():
+    if key not in constants.IPOLICY_ALL_KEYS:
+      raise errors.OpPrereqError("Invalid key in new ipolicy: %s" % key,
+                                 errors.ECODE_INVAL)
+    if key in constants.IPOLICY_ISPECS:
+      utils.ForceDictType(value, constants.ISPECS_PARAMETER_TYPES)
+      ipolicy[key] = _GetUpdatedParams(old_ipolicy.get(key, {}), value,
+                                       use_none=use_none,
+                                       use_default=use_default)
+    else:
+      if (not value or value == [constants.VALUE_DEFAULT] or
+          value == constants.VALUE_DEFAULT):
+        if group_policy:
+          del ipolicy[key]
+        else:
+          raise errors.OpPrereqError("Can't unset ipolicy attribute '%s'"
+                                     " on the cluster'" % key,
+                                     errors.ECODE_INVAL)
+      else:
+        if key in constants.IPOLICY_PARAMETERS:
+          # FIXME: we assume all such values are float
+          try:
+            ipolicy[key] = float(value)
+          except (TypeError, ValueError), err:
+            raise errors.OpPrereqError("Invalid value for attribute"
+                                       " '%s': '%s', error: %s" %
+                                       (key, value, err), errors.ECODE_INVAL)
+        else:
+          # FIXME: we assume all others are lists; this should be redone
+          # in a nicer way
+          ipolicy[key] = list(value)
+  try:
+    objects.InstancePolicy.CheckParameterSyntax(ipolicy, not group_policy)
+  except errors.ConfigurationError, err:
+    raise errors.OpPrereqError("Invalid instance policy: %s" % err,
+                               errors.ECODE_INVAL)
+  return ipolicy
+
+
+def _UpdateAndVerifySubDict(base, updates, type_check):
+  """Updates and verifies a dict with sub dicts of the same type.
+
+  @param base: The dict with the old data
+  @param updates: The dict with the new data
+  @param type_check: Dict suitable to ForceDictType to verify correct types
+  @returns: A new dict with updated and verified values
+
+  """
+  def fn(old, value):
+    new = _GetUpdatedParams(old, value)
+    utils.ForceDictType(new, type_check)
+    return new
+
+  ret = copy.deepcopy(base)
+  ret.update(dict((key, fn(base.get(key, {}), value))
+                  for key, value in updates.items()))
+  return ret
+
+
+def _MergeAndVerifyHvState(op_input, obj_input):
+  """Combines the hv state from an opcode with the one of the object
+
+  @param op_input: The input dict from the opcode
+  @param obj_input: The input dict from the objects
+  @return: The verified and updated dict
+
+  """
+  if op_input:
+    invalid_hvs = set(op_input) - constants.HYPER_TYPES
+    if invalid_hvs:
+      raise errors.OpPrereqError("Invalid hypervisor(s) in hypervisor state:"
+                                 " %s" % utils.CommaJoin(invalid_hvs),
+                                 errors.ECODE_INVAL)
+    if obj_input is None:
+      obj_input = {}
+    type_check = constants.HVSTS_PARAMETER_TYPES
+    return _UpdateAndVerifySubDict(obj_input, op_input, type_check)
+
+  return None
+
+
+def _MergeAndVerifyDiskState(op_input, obj_input):
+  """Combines the disk state from an opcode with the one of the object
+
+  @param op_input: The input dict from the opcode
+  @param obj_input: The input dict from the objects
+  @return: The verified and updated dict
+  """
+  if op_input:
+    invalid_dst = set(op_input) - constants.DS_VALID_TYPES
+    if invalid_dst:
+      raise errors.OpPrereqError("Invalid storage type(s) in disk state: %s" %
+                                 utils.CommaJoin(invalid_dst),
+                                 errors.ECODE_INVAL)
+    type_check = constants.DSS_PARAMETER_TYPES
+    if obj_input is None:
+      obj_input = {}
+    return dict((key, _UpdateAndVerifySubDict(obj_input.get(key, {}), value,
+                                              type_check))
+                for key, value in op_input.items())
+
+  return None
+
+
 def _ReleaseLocks(lu, level, names=None, keep=None):
   """Releases locks owned by an LU.
 
@@ -712,12 +908,17 @@ def _ReleaseLocks(lu, level, names=None, keep=None):
   else:
     should_release = None
 
-  if should_release:
+  owned = lu.owned_locks(level)
+  if not owned:
+    # Not owning any lock at this level, do nothing
+    pass
+
+  elif should_release:
     retain = []
     release = []
 
     # Determine which locks to release
-    for name in lu.owned_locks(level):
+    for name in owned:
       if should_release(name):
         release.append(name)
       else:
@@ -753,7 +954,7 @@ def _RunPostHook(lu, node_name):
   """Runs the post-hook for an opcode on a single node.
 
   """
-  hm = lu.proc.hmclass(lu.rpc.call_hooks_runner, lu)
+  hm = lu.proc.BuildHooksManager(lu)
   try:
     hm.RunPhase(constants.HOOKS_PHASE_POST, nodes=[node_name])
   except:
@@ -889,20 +1090,199 @@ def _GetClusterDomainSecret():
                                strict=True)
 
 
-def _CheckInstanceDown(lu, instance, reason):
-  """Ensure that an instance is not running."""
-  if instance.admin_up:
-    raise errors.OpPrereqError("Instance %s is marked to be up, %s" %
-                               (instance.name, reason), errors.ECODE_STATE)
+def _CheckInstanceState(lu, instance, req_states, msg=None):
+  """Ensure that an instance is in one of the required states.
+
+  @param lu: the LU on behalf of which we make the check
+  @param instance: the instance to check
+  @param msg: if passed, should be a message to replace the default one
+  @raise errors.OpPrereqError: if the instance is not in the required state
+
+  """
+  if msg is None:
+    msg = "can't use instance from outside %s states" % ", ".join(req_states)
+  if instance.admin_state not in req_states:
+    raise errors.OpPrereqError("Instance '%s' is marked to be %s, %s" %
+                               (instance.name, instance.admin_state, msg),
+                               errors.ECODE_STATE)
+
+  if constants.ADMINST_UP not in req_states:
+    pnode = instance.primary_node
+    ins_l = lu.rpc.call_instance_list([pnode], [instance.hypervisor])[pnode]
+    ins_l.Raise("Can't contact node %s for instance information" % pnode,
+                prereq=True, ecode=errors.ECODE_ENVIRON)
+
+    if instance.name in ins_l.payload:
+      raise errors.OpPrereqError("Instance %s is running, %s" %
+                                 (instance.name, msg), errors.ECODE_STATE)
+
+
+def _ComputeMinMaxSpec(name, qualifier, ipolicy, value):
+  """Computes if value is in the desired range.
+
+  @param name: name of the parameter for which we perform the check
+  @param qualifier: a qualifier used in the error message (e.g. 'disk/1',
+      not just 'disk')
+  @param ipolicy: dictionary containing min, max and std values
+  @param value: actual value that we want to use
+  @return: None or element not meeting the criteria
+
+
+  """
+  if value in [None, constants.VALUE_AUTO]:
+    return None
+  max_v = ipolicy[constants.ISPECS_MAX].get(name, value)
+  min_v = ipolicy[constants.ISPECS_MIN].get(name, value)
+  if value > max_v or min_v > value:
+    if qualifier:
+      fqn = "%s/%s" % (name, qualifier)
+    else:
+      fqn = name
+    return ("%s value %s is not in range [%s, %s]" %
+            (fqn, value, min_v, max_v))
+  return None
+
+
+def _ComputeIPolicySpecViolation(ipolicy, mem_size, cpu_count, disk_count,
+                                 nic_count, disk_sizes, spindle_use,
+                                 _compute_fn=_ComputeMinMaxSpec):
+  """Verifies ipolicy against provided specs.
+
+  @type ipolicy: dict
+  @param ipolicy: The ipolicy
+  @type mem_size: int
+  @param mem_size: The memory size
+  @type cpu_count: int
+  @param cpu_count: Used cpu cores
+  @type disk_count: int
+  @param disk_count: Number of disks used
+  @type nic_count: int
+  @param nic_count: Number of nics used
+  @type disk_sizes: list of ints
+  @param disk_sizes: Disk sizes of used disk (len must match C{disk_count})
+  @type spindle_use: int
+  @param spindle_use: The number of spindles this instance uses
+  @param _compute_fn: The compute function (unittest only)
+  @return: A list of violations, or an empty list of no violations are found
+
+  """
+  assert disk_count == len(disk_sizes)
+
+  test_settings = [
+    (constants.ISPEC_MEM_SIZE, "", mem_size),
+    (constants.ISPEC_CPU_COUNT, "", cpu_count),
+    (constants.ISPEC_DISK_COUNT, "", disk_count),
+    (constants.ISPEC_NIC_COUNT, "", nic_count),
+    (constants.ISPEC_SPINDLE_USE, "", spindle_use),
+    ] + [(constants.ISPEC_DISK_SIZE, str(idx), d)
+         for idx, d in enumerate(disk_sizes)]
+
+  return filter(None,
+                (_compute_fn(name, qualifier, ipolicy, value)
+                 for (name, qualifier, value) in test_settings))
+
+
+def _ComputeIPolicyInstanceViolation(ipolicy, instance,
+                                     _compute_fn=_ComputeIPolicySpecViolation):
+  """Compute if instance meets the specs of ipolicy.
+
+  @type ipolicy: dict
+  @param ipolicy: The ipolicy to verify against
+  @type instance: L{objects.Instance}
+  @param instance: The instance to verify
+  @param _compute_fn: The function to verify ipolicy (unittest only)
+  @see: L{_ComputeIPolicySpecViolation}
+
+  """
+  mem_size = instance.beparams.get(constants.BE_MAXMEM, None)
+  cpu_count = instance.beparams.get(constants.BE_VCPUS, None)
+  spindle_use = instance.beparams.get(constants.BE_SPINDLE_USE, None)
+  disk_count = len(instance.disks)
+  disk_sizes = [disk.size for disk in instance.disks]
+  nic_count = len(instance.nics)
+
+  return _compute_fn(ipolicy, mem_size, cpu_count, disk_count, nic_count,
+                     disk_sizes, spindle_use)
+
+
+def _ComputeIPolicyInstanceSpecViolation(ipolicy, instance_spec,
+    _compute_fn=_ComputeIPolicySpecViolation):
+  """Compute if instance specs meets the specs of ipolicy.
+
+  @type ipolicy: dict
+  @param ipolicy: The ipolicy to verify against
+  @param instance_spec: dict
+  @param instance_spec: The instance spec to verify
+  @param _compute_fn: The function to verify ipolicy (unittest only)
+  @see: L{_ComputeIPolicySpecViolation}
+
+  """
+  mem_size = instance_spec.get(constants.ISPEC_MEM_SIZE, None)
+  cpu_count = instance_spec.get(constants.ISPEC_CPU_COUNT, None)
+  disk_count = instance_spec.get(constants.ISPEC_DISK_COUNT, 0)
+  disk_sizes = instance_spec.get(constants.ISPEC_DISK_SIZE, [])
+  nic_count = instance_spec.get(constants.ISPEC_NIC_COUNT, 0)
+  spindle_use = instance_spec.get(constants.ISPEC_SPINDLE_USE, None)
+
+  return _compute_fn(ipolicy, mem_size, cpu_count, disk_count, nic_count,
+                     disk_sizes, spindle_use)
+
+
+def _ComputeIPolicyNodeViolation(ipolicy, instance, current_group,
+                                 target_group,
+                                 _compute_fn=_ComputeIPolicyInstanceViolation):
+  """Compute if instance meets the specs of the new target group.
+
+  @param ipolicy: The ipolicy to verify
+  @param instance: The instance object to verify
+  @param current_group: The current group of the instance
+  @param target_group: The new group of the instance
+  @param _compute_fn: The function to verify ipolicy (unittest only)
+  @see: L{_ComputeIPolicySpecViolation}
+
+  """
+  if current_group == target_group:
+    return []
+  else:
+    return _compute_fn(ipolicy, instance)
+
+
+def _CheckTargetNodeIPolicy(lu, ipolicy, instance, node, ignore=False,
+                            _compute_fn=_ComputeIPolicyNodeViolation):
+  """Checks that the target node is correct in terms of instance policy.
 
-  pnode = instance.primary_node
-  ins_l = lu.rpc.call_instance_list([pnode], [instance.hypervisor])[pnode]
-  ins_l.Raise("Can't contact node %s for instance information" % pnode,
-              prereq=True, ecode=errors.ECODE_ENVIRON)
+  @param ipolicy: The ipolicy to verify
+  @param instance: The instance object to verify
+  @param node: The new node to relocate
+  @param ignore: Ignore violations of the ipolicy
+  @param _compute_fn: The function to verify ipolicy (unittest only)
+  @see: L{_ComputeIPolicySpecViolation}
 
-  if instance.name in ins_l.payload:
-    raise errors.OpPrereqError("Instance %s is running, %s" %
-                               (instance.name, reason), errors.ECODE_STATE)
+  """
+  primary_node = lu.cfg.GetNodeInfo(instance.primary_node)
+  res = _compute_fn(ipolicy, instance, primary_node.group, node.group)
+
+  if res:
+    msg = ("Instance does not meet target node group's (%s) instance"
+           " policy: %s") % (node.group, utils.CommaJoin(res))
+    if ignore:
+      lu.LogWarning(msg)
+    else:
+      raise errors.OpPrereqError(msg, errors.ECODE_INVAL)
+
+
+def _ComputeNewInstanceViolations(old_ipolicy, new_ipolicy, instances):
+  """Computes a set of any instances that would violate the new ipolicy.
+
+  @param old_ipolicy: The current (still in-place) ipolicy
+  @param new_ipolicy: The new (to become) ipolicy
+  @param instances: List of instances to verify
+  @return: A list of instances which violates the new ipolicy but
+      did not before
+
+  """
+  return (_ComputeViolatingInstances(new_ipolicy, instances) -
+          _ComputeViolatingInstances(old_ipolicy, instances))
 
 
 def _ExpandItemName(fn, name, kind):
@@ -933,7 +1313,7 @@ def _ExpandInstanceName(cfg, name):
 
 
 def _BuildInstanceHookEnv(name, primary_node, secondary_nodes, os_type, status,
-                          memory, vcpus, nics, disk_template, disks,
+                          minmem, maxmem, vcpus, nics, disk_template, disks,
                           bep, hvp, hypervisor_name, tags):
   """Builds instance related env variables for hooks
 
@@ -947,10 +1327,12 @@ def _BuildInstanceHookEnv(name, primary_node, secondary_nodes, os_type, status,
   @param secondary_nodes: list of secondary nodes as strings
   @type os_type: string
   @param os_type: the name of the instance's OS
-  @type status: boolean
-  @param status: the should_run status of the instance
-  @type memory: string
-  @param memory: the memory size of the instance
+  @type status: string
+  @param status: the desired status of the instance
+  @type minmem: string
+  @param minmem: the minimum memory size of the instance
+  @type maxmem: string
+  @param maxmem: the maximum memory size of the instance
   @type vcpus: string
   @param vcpus: the count of VCPUs the instance has
   @type nics: list
@@ -972,23 +1354,21 @@ def _BuildInstanceHookEnv(name, primary_node, secondary_nodes, os_type, status,
   @return: the hook environment for this instance
 
   """
-  if status:
-    str_status = "up"
-  else:
-    str_status = "down"
   env = {
     "OP_TARGET": name,
     "INSTANCE_NAME": name,
     "INSTANCE_PRIMARY": primary_node,
     "INSTANCE_SECONDARIES": " ".join(secondary_nodes),
     "INSTANCE_OS_TYPE": os_type,
-    "INSTANCE_STATUS": str_status,
-    "INSTANCE_MEMORY": memory,
+    "INSTANCE_STATUS": status,
+    "INSTANCE_MINMEM": minmem,
+    "INSTANCE_MAXMEM": maxmem,
+    # TODO(2.7) remove deprecated "memory" value
+    "INSTANCE_MEMORY": maxmem,
     "INSTANCE_VCPUS": vcpus,
     "INSTANCE_DISK_TEMPLATE": disk_template,
     "INSTANCE_HYPERVISOR": hypervisor_name,
   }
-
   if nics:
     nic_count = len(nics)
     for idx, (ip, mac, mode, link) in enumerate(nics):
@@ -1074,8 +1454,9 @@ def _BuildInstanceHookEnvByObject(lu, instance, override=None):
     "primary_node": instance.primary_node,
     "secondary_nodes": instance.secondary_nodes,
     "os_type": instance.os,
-    "status": instance.admin_up,
-    "memory": bep[constants.BE_MEMORY],
+    "status": instance.admin_state,
+    "maxmem": bep[constants.BE_MAXMEM],
+    "minmem": bep[constants.BE_MINMEM],
     "vcpus": bep[constants.BE_VCPUS],
     "nics": _NICListToTuple(lu, instance.nics),
     "disk_template": instance.disk_template,
@@ -1117,6 +1498,26 @@ def _DecideSelfPromotion(lu, exceptions=None):
   return mc_now < mc_should
 
 
+def _CalculateGroupIPolicy(cluster, group):
+  """Calculate instance policy for group.
+
+  """
+  return cluster.SimpleFillIPolicy(group.ipolicy)
+
+
+def _ComputeViolatingInstances(ipolicy, instances):
+  """Computes a set of instances who violates given ipolicy.
+
+  @param ipolicy: The ipolicy to verify
+  @type instances: object.Instance
+  @param instances: List of instances to verify
+  @return: A frozenset of instance names violating the ipolicy
+
+  """
+  return frozenset([inst.name for inst in instances
+                    if _ComputeIPolicyInstanceViolation(ipolicy, inst)])
+
+
 def _CheckNicsBridgesExist(lu, target_nics, target_node):
   """Check that the brigdes needed by a list of nics exist.
 
@@ -1204,13 +1605,14 @@ def _GetStorageTypeArgs(cfg, storage_type):
   return []
 
 
-def _FindFaultyInstanceDisks(cfg, rpc, instance, node_name, prereq):
+def _FindFaultyInstanceDisks(cfg, rpc_runner, instance, node_name, prereq):
   faulty = []
 
   for dev in instance.disks:
     cfg.SetDiskID(dev, node_name)
 
-  result = rpc.call_blockdev_getmirrorstatus(node_name, instance.disks)
+  result = rpc_runner.call_blockdev_getmirrorstatus(node_name, (instance.disks,
+                                                                instance))
   result.Raise("Failed to get disk status from node %s" % node_name,
                prereq=prereq, ecode=errors.ECODE_ENVIRON)
 
@@ -1350,15 +1752,19 @@ class LUClusterDestroy(LogicalUnit):
     """Destroys the cluster.
 
     """
-    master = self.cfg.GetMasterNode()
+    master_params = self.cfg.GetMasterNetworkParameters()
 
     # Run post hooks on master node before it's removed
-    _RunPostHook(self, master)
+    _RunPostHook(self, master_params.name)
 
-    result = self.rpc.call_node_stop_master(master, False)
-    result.Raise("Could not disable the master role")
+    ems = self.cfg.GetUseExternalMipScript()
+    result = self.rpc.call_node_deactivate_master_ip(master_params.name,
+                                                     master_params, ems)
+    if result.fail_msg:
+      self.LogWarning("Error disabling the master IP address: %s",
+                      result.fail_msg)
 
-    return master
+    return master_params.name
 
 
 def _VerifyCertificate(filename):
@@ -1433,39 +1839,6 @@ class _VerifyErrors(object):
   self.op and self._feedback_fn to be available.)
 
   """
-  TCLUSTER = "cluster"
-  TNODE = "node"
-  TINSTANCE = "instance"
-
-  ECLUSTERCFG = (TCLUSTER, "ECLUSTERCFG")
-  ECLUSTERCERT = (TCLUSTER, "ECLUSTERCERT")
-  ECLUSTERFILECHECK = (TCLUSTER, "ECLUSTERFILECHECK")
-  ECLUSTERDANGLINGNODES = (TNODE, "ECLUSTERDANGLINGNODES")
-  ECLUSTERDANGLINGINST = (TNODE, "ECLUSTERDANGLINGINST")
-  EINSTANCEBADNODE = (TINSTANCE, "EINSTANCEBADNODE")
-  EINSTANCEDOWN = (TINSTANCE, "EINSTANCEDOWN")
-  EINSTANCELAYOUT = (TINSTANCE, "EINSTANCELAYOUT")
-  EINSTANCEMISSINGDISK = (TINSTANCE, "EINSTANCEMISSINGDISK")
-  EINSTANCEFAULTYDISK = (TINSTANCE, "EINSTANCEFAULTYDISK")
-  EINSTANCEWRONGNODE = (TINSTANCE, "EINSTANCEWRONGNODE")
-  EINSTANCESPLITGROUPS = (TINSTANCE, "EINSTANCESPLITGROUPS")
-  ENODEDRBD = (TNODE, "ENODEDRBD")
-  ENODEDRBDHELPER = (TNODE, "ENODEDRBDHELPER")
-  ENODEFILECHECK = (TNODE, "ENODEFILECHECK")
-  ENODEHOOKS = (TNODE, "ENODEHOOKS")
-  ENODEHV = (TNODE, "ENODEHV")
-  ENODELVM = (TNODE, "ENODELVM")
-  ENODEN1 = (TNODE, "ENODEN1")
-  ENODENET = (TNODE, "ENODENET")
-  ENODEOS = (TNODE, "ENODEOS")
-  ENODEORPHANINSTANCE = (TNODE, "ENODEORPHANINSTANCE")
-  ENODEORPHANLV = (TNODE, "ENODEORPHANLV")
-  ENODERPC = (TNODE, "ENODERPC")
-  ENODESSH = (TNODE, "ENODESSH")
-  ENODEVERSION = (TNODE, "ENODEVERSION")
-  ENODESETUP = (TNODE, "ENODESETUP")
-  ENODETIME = (TNODE, "ENODETIME")
-  ENODEOOBPATH = (TNODE, "ENODEOOBPATH")
 
   ETYPE_FIELD = "code"
   ETYPE_ERROR = "ERROR"
@@ -1481,7 +1854,7 @@ class _VerifyErrors(object):
 
     """
     ltype = kwargs.get(self.ETYPE_FIELD, self.ETYPE_ERROR)
-    itype, etxt = ecode
+    itype, etxt, _ = ecode
     # first complete the msg
     if args:
       msg = msg % args
@@ -1497,14 +1870,22 @@ class _VerifyErrors(object):
     # and finally report it via the feedback_fn
     self._feedback_fn("  - %s" % msg) # Mix-in. pylint: disable=E1101
 
-  def _ErrorIf(self, cond, *args, **kwargs):
+  def _ErrorIf(self, cond, ecode, *args, **kwargs):
     """Log an error message if the passed condition is True.
 
     """
     cond = (bool(cond)
             or self.op.debug_simulate_errors) # pylint: disable=E1101
+
+    # If the error code is in the list of ignored errors, demote the error to a
+    # warning
+    (_, etxt, _) = ecode
+    if etxt in self.op.ignore_errors:     # pylint: disable=E1101
+      kwargs[self.ETYPE_FIELD] = self.ETYPE_WARNING
+
     if cond:
-      self._Error(*args, **kwargs)
+      self._Error(ecode, *args, **kwargs)
+
     # do not mark the operation as failed for WARN cases only
     if kwargs.get(self.ETYPE_FIELD, self.ETYPE_ERROR) == self.ETYPE_ERROR:
       self.bad = self.bad or cond
@@ -1529,13 +1910,16 @@ class LUClusterVerify(NoHooksLU):
       groups = self.cfg.GetNodeGroupList()
 
       # Verify global configuration
-      jobs.append([opcodes.OpClusterVerifyConfig()])
+      jobs.append([
+        opcodes.OpClusterVerifyConfig(ignore_errors=self.op.ignore_errors)
+        ])
 
       # Always depend on global verification
       depends_fn = lambda: [(-len(jobs), [])]
 
     jobs.extend([opcodes.OpClusterVerifyGroup(group_name=group,
-                                              depends=depends_fn())]
+                                            ignore_errors=self.op.ignore_errors,
+                                            depends=depends_fn())]
                 for group in groups)
 
     # Fix up all parameters
@@ -1569,7 +1953,7 @@ class LUClusterVerifyConfig(NoHooksLU, _VerifyErrors):
         utils.ForceDictType(hv_params, constants.HVS_PARAMETER_TYPES)
         hv_class.CheckParameterSyntax(hv_params)
       except errors.GenericError, err:
-        self._ErrorIf(True, self.ECLUSTERCFG, None, msg % str(err))
+        self._ErrorIf(True, constants.CV_ECLUSTERCFG, None, msg % str(err))
 
   def ExpandNames(self):
     self.needed_locks = dict.fromkeys(locking.LEVELS, locking.ALL_SET)
@@ -1594,13 +1978,13 @@ class LUClusterVerifyConfig(NoHooksLU, _VerifyErrors):
     feedback_fn("* Verifying cluster config")
 
     for msg in self.cfg.VerifyConfig():
-      self._ErrorIf(True, self.ECLUSTERCFG, None, msg)
+      self._ErrorIf(True, constants.CV_ECLUSTERCFG, None, msg)
 
     feedback_fn("* Verifying cluster certificate files")
 
     for cert_filename in constants.ALL_CERT_FILES:
       (errcode, msg) = _VerifyCertificate(cert_filename)
-      self._ErrorIf(errcode, self.ECLUSTERCERT, None, msg, code=errcode)
+      self._ErrorIf(errcode, constants.CV_ECLUSTERCERT, None, msg, code=errcode)
 
     feedback_fn("* Verifying hypervisor parameters")
 
@@ -1632,11 +2016,13 @@ class LUClusterVerifyConfig(NoHooksLU, _VerifyErrors):
                                                 ["no instances"])))
         for node in dangling_nodes]
 
-    self._ErrorIf(bool(dangling_nodes), self.ECLUSTERDANGLINGNODES, None,
+    self._ErrorIf(bool(dangling_nodes), constants.CV_ECLUSTERDANGLINGNODES,
+                  None,
                   "the following nodes (and their instances) belong to a non"
                   " existing group: %s", utils.CommaJoin(pretty_dangling))
 
-    self._ErrorIf(bool(no_node_instances), self.ECLUSTERDANGLINGINST, None,
+    self._ErrorIf(bool(no_node_instances), constants.CV_ECLUSTERDANGLINGINST,
+                  None,
                   "the following instances have a non-existing primary-node:"
                   " %s", utils.CommaJoin(no_node_instances))
 
@@ -1813,7 +2199,7 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
 
     # main result, nresult should be a non-empty dict
     test = not nresult or not isinstance(nresult, dict)
-    _ErrorIf(test, self.ENODERPC, node,
+    _ErrorIf(test, constants.CV_ENODERPC, node,
                   "unable to verify node: no data returned")
     if test:
       return False
@@ -1824,13 +2210,13 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
     test = not (remote_version and
                 isinstance(remote_version, (list, tuple)) and
                 len(remote_version) == 2)
-    _ErrorIf(test, self.ENODERPC, node,
+    _ErrorIf(test, constants.CV_ENODERPC, node,
              "connection to node returned invalid data")
     if test:
       return False
 
     test = local_version != remote_version[0]
-    _ErrorIf(test, self.ENODEVERSION, node,
+    _ErrorIf(test, constants.CV_ENODEVERSION, node,
              "incompatible protocol versions: master %s,"
              " node %s", local_version, remote_version[0])
     if test:
@@ -1840,7 +2226,7 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
 
     # full package version
     self._ErrorIf(constants.RELEASE_VERSION != remote_version[1],
-                  self.ENODEVERSION, node,
+                  constants.CV_ENODEVERSION, node,
                   "software version mismatch: master %s, node %s",
                   constants.RELEASE_VERSION, remote_version[1],
                   code=self.ETYPE_WARNING)
@@ -1849,19 +2235,19 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
     if ninfo.vm_capable and isinstance(hyp_result, dict):
       for hv_name, hv_result in hyp_result.iteritems():
         test = hv_result is not None
-        _ErrorIf(test, self.ENODEHV, node,
+        _ErrorIf(test, constants.CV_ENODEHV, node,
                  "hypervisor %s verify failure: '%s'", hv_name, hv_result)
 
     hvp_result = nresult.get(constants.NV_HVPARAMS, None)
     if ninfo.vm_capable and isinstance(hvp_result, list):
       for item, hv_name, hv_result in hvp_result:
-        _ErrorIf(True, self.ENODEHV, node,
+        _ErrorIf(True, constants.CV_ENODEHV, node,
                  "hypervisor %s parameter verify failure (source %s): %s",
                  hv_name, item, hv_result)
 
     test = nresult.get(constants.NV_NODESETUP,
                        ["Missing NODESETUP results"])
-    _ErrorIf(test, self.ENODESETUP, node, "node setup error: %s",
+    _ErrorIf(test, constants.CV_ENODESETUP, node, "node setup error: %s",
              "; ".join(test))
 
     return True
@@ -1884,7 +2270,7 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
     try:
       ntime_merged = utils.MergeTime(ntime)
     except (ValueError, TypeError):
-      _ErrorIf(True, self.ENODETIME, node, "Node returned invalid time")
+      _ErrorIf(True, constants.CV_ENODETIME, node, "Node returned invalid time")
       return
 
     if ntime_merged < (nvinfo_starttime - constants.NODE_MAX_CLOCK_SKEW):
@@ -1894,7 +2280,7 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
     else:
       ntime_diff = None
 
-    _ErrorIf(ntime_diff is not None, self.ENODETIME, node,
+    _ErrorIf(ntime_diff is not None, constants.CV_ENODETIME, node,
              "Node time diverges by at least %s from master node time",
              ntime_diff)
 
@@ -1916,24 +2302,25 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
     # checks vg existence and size > 20G
     vglist = nresult.get(constants.NV_VGLIST, None)
     test = not vglist
-    _ErrorIf(test, self.ENODELVM, node, "unable to check volume groups")
+    _ErrorIf(test, constants.CV_ENODELVM, node, "unable to check volume groups")
     if not test:
       vgstatus = utils.CheckVolumeGroupSize(vglist, vg_name,
                                             constants.MIN_VG_SIZE)
-      _ErrorIf(vgstatus, self.ENODELVM, node, vgstatus)
+      _ErrorIf(vgstatus, constants.CV_ENODELVM, node, vgstatus)
 
     # check pv names
     pvlist = nresult.get(constants.NV_PVLIST, None)
     test = pvlist is None
-    _ErrorIf(test, self.ENODELVM, node, "Can't get PV list from node")
+    _ErrorIf(test, constants.CV_ENODELVM, node, "Can't get PV list from node")
     if not test:
       # check that ':' is not present in PV names, since it's a
       # special character for lvcreate (denotes the range of PEs to
       # use on the PV)
       for _, pvname, owner_vg in pvlist:
         test = ":" in pvname
-        _ErrorIf(test, self.ENODELVM, node, "Invalid character ':' in PV"
-                 " '%s' of VG '%s'", pvname, owner_vg)
+        _ErrorIf(test, constants.CV_ENODELVM, node,
+                 "Invalid character ':' in PV '%s' of VG '%s'",
+                 pvname, owner_vg)
 
   def _VerifyNodeBridges(self, ninfo, nresult, bridges):
     """Check the node bridges.
@@ -1952,11 +2339,31 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
 
     missing = nresult.get(constants.NV_BRIDGES, None)
     test = not isinstance(missing, list)
-    _ErrorIf(test, self.ENODENET, node,
+    _ErrorIf(test, constants.CV_ENODENET, node,
              "did not return valid bridge information")
     if not test:
-      _ErrorIf(bool(missing), self.ENODENET, node, "missing bridges: %s" %
-               utils.CommaJoin(sorted(missing)))
+      _ErrorIf(bool(missing), constants.CV_ENODENET, node,
+               "missing bridges: %s" % utils.CommaJoin(sorted(missing)))
+
+  def _VerifyNodeUserScripts(self, ninfo, nresult):
+    """Check the results of user scripts presence and executability on the node
+
+    @type ninfo: L{objects.Node}
+    @param ninfo: the node to check
+    @param nresult: the remote results for the node
+
+    """
+    node = ninfo.name
+
+    test = not constants.NV_USERSCRIPTS in nresult
+    self._ErrorIf(test, constants.CV_ENODEUSERSCRIPTS, node,
+                  "did not return user scripts information")
+
+    broken_scripts = nresult.get(constants.NV_USERSCRIPTS, None)
+    if not test:
+      self._ErrorIf(broken_scripts, constants.CV_ENODEUSERSCRIPTS, node,
+                    "user scripts not present or not executable: %s" %
+                    utils.CommaJoin(sorted(broken_scripts)))
 
   def _VerifyNodeNetwork(self, ninfo, nresult):
     """Check the node network connectivity results.
@@ -1970,27 +2377,27 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
     _ErrorIf = self._ErrorIf # pylint: disable=C0103
 
     test = constants.NV_NODELIST not in nresult
-    _ErrorIf(test, self.ENODESSH, node,
+    _ErrorIf(test, constants.CV_ENODESSH, node,
              "node hasn't returned node ssh connectivity data")
     if not test:
       if nresult[constants.NV_NODELIST]:
         for a_node, a_msg in nresult[constants.NV_NODELIST].items():
-          _ErrorIf(True, self.ENODESSH, node,
+          _ErrorIf(True, constants.CV_ENODESSH, node,
                    "ssh communication with node '%s': %s", a_node, a_msg)
 
     test = constants.NV_NODENETTEST not in nresult
-    _ErrorIf(test, self.ENODENET, node,
+    _ErrorIf(test, constants.CV_ENODENET, node,
              "node hasn't returned node tcp connectivity data")
     if not test:
       if nresult[constants.NV_NODENETTEST]:
         nlist = utils.NiceSort(nresult[constants.NV_NODENETTEST].keys())
         for anode in nlist:
-          _ErrorIf(True, self.ENODENET, node,
+          _ErrorIf(True, constants.CV_ENODENET, node,
                    "tcp communication with node '%s': %s",
                    anode, nresult[constants.NV_NODENETTEST][anode])
 
     test = constants.NV_MASTERIP not in nresult
-    _ErrorIf(test, self.ENODENET, node,
+    _ErrorIf(test, constants.CV_ENODENET, node,
              "node hasn't returned node master IP reachability data")
     if not test:
       if not nresult[constants.NV_MASTERIP]:
@@ -1998,7 +2405,7 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
           msg = "the master node cannot reach the master IP (not configured?)"
         else:
           msg = "cannot reach the master IP"
-        _ErrorIf(True, self.ENODENET, node, msg)
+        _ErrorIf(True, constants.CV_ENODENET, node, msg)
 
   def _VerifyInstance(self, instance, instanceconfig, node_image,
                       diskstatus):
@@ -2014,6 +2421,10 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
     node_vol_should = {}
     instanceconfig.MapLVsByNode(node_vol_should)
 
+    ipolicy = _CalculateGroupIPolicy(self.cfg.GetClusterInfo(), self.group_info)
+    err = _ComputeIPolicyInstanceViolation(ipolicy, instanceconfig)
+    _ErrorIf(err, constants.CV_EINSTANCEPOLICY, instance, utils.CommaJoin(err))
+
     for node in node_vol_should:
       n_img = node_image[node]
       if n_img.offline or n_img.rpc_fail or n_img.lvm_fail:
@@ -2021,13 +2432,13 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
         continue
       for volume in node_vol_should[node]:
         test = volume not in n_img.volumes
-        _ErrorIf(test, self.EINSTANCEMISSINGDISK, instance,
+        _ErrorIf(test, constants.CV_EINSTANCEMISSINGDISK, instance,
                  "volume %s missing on node %s", volume, node)
 
-    if instanceconfig.admin_up:
+    if instanceconfig.admin_state == constants.ADMINST_UP:
       pri_img = node_image[node_current]
       test = instance not in pri_img.instances and not pri_img.offline
-      _ErrorIf(test, self.EINSTANCEDOWN, instance,
+      _ErrorIf(test, constants.CV_EINSTANCEDOWN, instance,
                "instance not running on its primary node %s",
                node_current)
 
@@ -2040,13 +2451,14 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
       # node here
       snode = node_image[nname]
       bad_snode = snode.ghost or snode.offline
-      _ErrorIf(instanceconfig.admin_up and not success and not bad_snode,
-               self.EINSTANCEFAULTYDISK, instance,
+      _ErrorIf(instanceconfig.admin_state == constants.ADMINST_UP and
+               not success and not bad_snode,
+               constants.CV_EINSTANCEFAULTYDISK, instance,
                "couldn't retrieve status for disk/%s on %s: %s",
                idx, nname, bdev_status)
-      _ErrorIf((instanceconfig.admin_up and success and
-                bdev_status.ldisk_status == constants.LDS_FAULTY),
-               self.EINSTANCEFAULTYDISK, instance,
+      _ErrorIf((instanceconfig.admin_state == constants.ADMINST_UP and
+                success and bdev_status.ldisk_status == constants.LDS_FAULTY),
+               constants.CV_EINSTANCEFAULTYDISK, instance,
                "disk/%s on %s is faulty", idx, nname)
 
   def _VerifyOrphanVolumes(self, node_vol_should, node_image, reserved):
@@ -2068,7 +2480,7 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
         test = ((node not in node_vol_should or
                 volume not in node_vol_should[node]) and
                 not reserved.Matches(volume))
-        self._ErrorIf(test, self.ENODEORPHANLV, node,
+        self._ErrorIf(test, constants.CV_ENODEORPHANLV, node,
                       "volume %s is unknown", volume)
 
   def _VerifyNPlusOneMemory(self, node_image, instance_cfg):
@@ -2094,21 +2506,22 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
         # infromation from them; we already list instances living on such
         # nodes, and that's enough warning
         continue
+      #TODO(dynmem): also consider ballooning out other instances
       for prinode, instances in n_img.sbp.items():
         needed_mem = 0
         for instance in instances:
           bep = cluster_info.FillBE(instance_cfg[instance])
           if bep[constants.BE_AUTO_BALANCE]:
-            needed_mem += bep[constants.BE_MEMORY]
+            needed_mem += bep[constants.BE_MINMEM]
         test = n_img.mfree < needed_mem
-        self._ErrorIf(test, self.ENODEN1, node,
+        self._ErrorIf(test, constants.CV_ENODEN1, node,
                       "not enough memory to accomodate instance failovers"
                       " should node %s fail (%dMiB needed, %dMiB available)",
                       prinode, needed_mem, n_img.mfree)
 
   @classmethod
   def _VerifyFiles(cls, errorif, nodeinfo, master_node, all_nvinfo,
-                   (files_all, files_all_opt, files_mc, files_vm)):
+                   (files_all, files_opt, files_mc, files_vm)):
     """Verifies file checksums collected from all nodes.
 
     @param errorif: Callback for reporting errors
@@ -2117,14 +2530,9 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
     @param all_nvinfo: RPC results
 
     """
-    assert (len(files_all | files_all_opt | files_mc | files_vm) ==
-            sum(map(len, [files_all, files_all_opt, files_mc, files_vm]))), \
-           "Found file listed in more than one file list"
-
     # Define functions determining which nodes to consider for a file
     files2nodefn = [
       (files_all, None),
-      (files_all_opt, None),
       (files_mc, lambda node: (node.master_candidate or
                                node.name == master_node)),
       (files_vm, lambda node: node.vm_capable),
@@ -2141,7 +2549,7 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
                         frozenset(map(operator.attrgetter("name"), filenodes)))
                        for filename in files)
 
-    assert set(nodefiles) == (files_all | files_all_opt | files_mc | files_vm)
+    assert set(nodefiles) == (files_all | files_mc | files_vm)
 
     fileinfo = dict((filename, {}) for filename in nodefiles)
     ignore_nodes = set()
@@ -2159,7 +2567,7 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
         node_files = nresult.payload.get(constants.NV_FILELIST, None)
 
       test = not (node_files and isinstance(node_files, dict))
-      errorif(test, cls.ENODEFILECHECK, node.name,
+      errorif(test, constants.CV_ENODEFILECHECK, node.name,
               "Node did not return file checksum data")
       if test:
         ignore_nodes.add(node.name)
@@ -2183,23 +2591,22 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
       # Nodes missing file
       missing_file = expected_nodes - with_file
 
-      if filename in files_all_opt:
+      if filename in files_opt:
         # All or no nodes
         errorif(missing_file and missing_file != expected_nodes,
-                cls.ECLUSTERFILECHECK, None,
+                constants.CV_ECLUSTERFILECHECK, None,
                 "File %s is optional, but it must exist on all or no"
                 " nodes (not found on %s)",
                 filename, utils.CommaJoin(utils.NiceSort(missing_file)))
       else:
-        # Non-optional files
-        errorif(missing_file, cls.ECLUSTERFILECHECK, None,
+        errorif(missing_file, constants.CV_ECLUSTERFILECHECK, None,
                 "File %s is missing from node(s) %s", filename,
                 utils.CommaJoin(utils.NiceSort(missing_file)))
 
         # Warn if a node has a file it shouldn't
         unexpected = with_file - expected_nodes
         errorif(unexpected,
-                cls.ECLUSTERFILECHECK, None,
+                constants.CV_ECLUSTERFILECHECK, None,
                 "File %s should not exist on node(s) %s",
                 filename, utils.CommaJoin(utils.NiceSort(unexpected)))
 
@@ -2213,7 +2620,7 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
       else:
         variants = []
 
-      errorif(test, cls.ECLUSTERFILECHECK, None,
+      errorif(test, constants.CV_ECLUSTERFILECHECK, None,
               "File %s found with %s different checksums (%s)",
               filename, len(checksums), "; ".join(variants))
 
@@ -2236,22 +2643,22 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
     if drbd_helper:
       helper_result = nresult.get(constants.NV_DRBDHELPER, None)
       test = (helper_result == None)
-      _ErrorIf(test, self.ENODEDRBDHELPER, node,
+      _ErrorIf(test, constants.CV_ENODEDRBDHELPER, node,
                "no drbd usermode helper returned")
       if helper_result:
         status, payload = helper_result
         test = not status
-        _ErrorIf(test, self.ENODEDRBDHELPER, node,
+        _ErrorIf(test, constants.CV_ENODEDRBDHELPER, node,
                  "drbd usermode helper check unsuccessful: %s", payload)
         test = status and (payload != drbd_helper)
-        _ErrorIf(test, self.ENODEDRBDHELPER, node,
+        _ErrorIf(test, constants.CV_ENODEDRBDHELPER, node,
                  "wrong drbd usermode helper: %s", payload)
 
     # compute the DRBD minors
     node_drbd = {}
     for minor, instance in drbd_map[node].items():
       test = instance not in instanceinfo
-      _ErrorIf(test, self.ECLUSTERCFG, None,
+      _ErrorIf(test, constants.CV_ECLUSTERCFG, None,
                "ghost instance '%s' in temporary DRBD map", instance)
         # ghost instance should not be running, but otherwise we
         # don't give double warnings (both ghost instance and
@@ -2260,12 +2667,13 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
         node_drbd[minor] = (instance, False)
       else:
         instance = instanceinfo[instance]
-        node_drbd[minor] = (instance.name, instance.admin_up)
+        node_drbd[minor] = (instance.name,
+                            instance.admin_state == constants.ADMINST_UP)
 
     # and now check them
     used_minors = nresult.get(constants.NV_DRBDLIST, [])
     test = not isinstance(used_minors, (tuple, list))
-    _ErrorIf(test, self.ENODEDRBD, node,
+    _ErrorIf(test, constants.CV_ENODEDRBD, node,
              "cannot parse drbd status file: %s", str(used_minors))
     if test:
       # we cannot check drbd status
@@ -2273,11 +2681,11 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
 
     for minor, (iname, must_exist) in node_drbd.items():
       test = minor not in used_minors and must_exist
-      _ErrorIf(test, self.ENODEDRBD, node,
+      _ErrorIf(test, constants.CV_ENODEDRBD, node,
                "drbd minor %d of instance %s is not active", minor, iname)
     for minor in used_minors:
       test = minor not in node_drbd
-      _ErrorIf(test, self.ENODEDRBD, node,
+      _ErrorIf(test, constants.CV_ENODEDRBD, node,
                "unallocated drbd minor %d is in use", minor)
 
   def _UpdateNodeOS(self, ninfo, nresult, nimg):
@@ -2297,7 +2705,7 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
             not compat.all(isinstance(v, list) and len(v) == 7
                            for v in remote_os))
 
-    _ErrorIf(test, self.ENODEOS, node,
+    _ErrorIf(test, constants.CV_ENODEOS, node,
              "node hasn't returned valid OS data")
 
     nimg.os_fail = test
@@ -2339,14 +2747,14 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
     for os_name, os_data in nimg.oslist.items():
       assert os_data, "Empty OS status for OS %s?!" % os_name
       f_path, f_status, f_diag, f_var, f_param, f_api = os_data[0]
-      _ErrorIf(not f_status, self.ENODEOS, node,
+      _ErrorIf(not f_status, constants.CV_ENODEOS, node,
                "Invalid OS %s (located at %s): %s", os_name, f_path, f_diag)
-      _ErrorIf(len(os_data) > 1, self.ENODEOS, node,
+      _ErrorIf(len(os_data) > 1, constants.CV_ENODEOS, node,
                "OS '%s' has multiple entries (first one shadows the rest): %s",
                os_name, utils.CommaJoin([v[0] for v in os_data]))
       # comparisons with the 'base' image
       test = os_name not in base.oslist
-      _ErrorIf(test, self.ENODEOS, node,
+      _ErrorIf(test, constants.CV_ENODEOS, node,
                "Extra OS %s not present on reference node (%s)",
                os_name, base.name)
       if test:
@@ -2360,14 +2768,14 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
                          ("variants list", f_var, b_var),
                          ("parameters", beautify_params(f_param),
                           beautify_params(b_param))]:
-        _ErrorIf(a != b, self.ENODEOS, node,
+        _ErrorIf(a != b, constants.CV_ENODEOS, node,
                  "OS %s for %s differs from reference node %s: [%s] vs. [%s]",
                  kind, os_name, base.name,
                  utils.CommaJoin(sorted(a)), utils.CommaJoin(sorted(b)))
 
     # check any missing OSes
     missing = set(base.oslist.keys()).difference(nimg.oslist.keys())
-    _ErrorIf(missing, self.ENODEOS, node,
+    _ErrorIf(missing, constants.CV_ENODEOS, node,
              "OSes present on reference node %s but missing on this node: %s",
              base.name, utils.CommaJoin(missing))
 
@@ -2385,7 +2793,7 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
     if ((ninfo.master_candidate or ninfo.master_capable) and
         constants.NV_OOB_PATHS in nresult):
       for path_result in nresult[constants.NV_OOB_PATHS]:
-        self._ErrorIf(path_result, self.ENODEOOBPATH, node, path_result)
+        self._ErrorIf(path_result, constants.CV_ENODEOOBPATH, node, path_result)
 
   def _UpdateNodeVolumes(self, ninfo, nresult, nimg, vg_name):
     """Verifies and updates the node volume data.
@@ -2408,10 +2816,11 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
     if vg_name is None:
       pass
     elif isinstance(lvdata, basestring):
-      _ErrorIf(True, self.ENODELVM, node, "LVM problem on node: %s",
+      _ErrorIf(True, constants.CV_ENODELVM, node, "LVM problem on node: %s",
                utils.SafeEncode(lvdata))
     elif not isinstance(lvdata, dict):
-      _ErrorIf(True, self.ENODELVM, node, "rpc call to node failed (lvlist)")
+      _ErrorIf(True, constants.CV_ENODELVM, node,
+               "rpc call to node failed (lvlist)")
     else:
       nimg.volumes = lvdata
       nimg.lvm_fail = False
@@ -2431,8 +2840,9 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
     """
     idata = nresult.get(constants.NV_INSTANCELIST, None)
     test = not isinstance(idata, list)
-    self._ErrorIf(test, self.ENODEHV, ninfo.name, "rpc call to node failed"
-                  " (instancelist): %s", utils.SafeEncode(str(idata)))
+    self._ErrorIf(test, constants.CV_ENODEHV, ninfo.name,
+                  "rpc call to node failed (instancelist): %s",
+                  utils.SafeEncode(str(idata)))
     if test:
       nimg.hyp_fail = True
     else:
@@ -2454,26 +2864,27 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
     # try to read free memory (from the hypervisor)
     hv_info = nresult.get(constants.NV_HVINFO, None)
     test = not isinstance(hv_info, dict) or "memory_free" not in hv_info
-    _ErrorIf(test, self.ENODEHV, node, "rpc call to node failed (hvinfo)")
+    _ErrorIf(test, constants.CV_ENODEHV, node,
+             "rpc call to node failed (hvinfo)")
     if not test:
       try:
         nimg.mfree = int(hv_info["memory_free"])
       except (ValueError, TypeError):
-        _ErrorIf(True, self.ENODERPC, node,
+        _ErrorIf(True, constants.CV_ENODERPC, node,
                  "node returned invalid nodeinfo, check hypervisor")
 
     # FIXME: devise a free space model for file based instances as well
     if vg_name is not None:
       test = (constants.NV_VGLIST not in nresult or
               vg_name not in nresult[constants.NV_VGLIST])
-      _ErrorIf(test, self.ENODELVM, node,
+      _ErrorIf(test, constants.CV_ENODELVM, node,
                "node didn't return data for the volume group '%s'"
                " - it is either missing or broken", vg_name)
       if not test:
         try:
           nimg.dfree = int(nresult[constants.NV_VGLIST][vg_name])
         except (ValueError, TypeError):
-          _ErrorIf(True, self.ENODERPC, node,
+          _ErrorIf(True, constants.CV_ENODERPC, node,
                    "node returned invalid LVM info, check LVM status")
 
   def _CollectDiskInfo(self, nodelist, node_image, instanceinfo):
@@ -2513,12 +2924,12 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
 
       node_disks[nname] = disks
 
-      # Creating copies as SetDiskID below will modify the objects and that can
-      # lead to incorrect data returned from nodes
-      devonly = [dev.Copy() for (_, dev) in disks]
-
-      for dev in devonly:
-        self.cfg.SetDiskID(dev, nname)
+      # _AnnotateDiskParams makes already copies of the disks
+      devonly = []
+      for (inst, dev) in disks:
+        (anno_disk,) = _AnnotateDiskParams(instanceinfo[inst], [dev], self.cfg)
+        self.cfg.SetDiskID(anno_disk, nname)
+        devonly.append(anno_disk)
 
       node_disks_devonly[nname] = devonly
 
@@ -2540,7 +2951,7 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
         data = len(disks) * [(False, "node offline")]
       else:
         msg = nres.fail_msg
-        _ErrorIf(msg, self.ENODERPC, nname,
+        _ErrorIf(msg, constants.CV_ENODERPC, nname,
                  "while getting disk information: %s", msg)
         if msg:
           # No data from this node
@@ -2655,6 +3066,7 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
 
     i_non_redundant = [] # Non redundant instances
     i_non_a_balanced = [] # Non auto-balanced instances
+    i_offline = 0 # Count of offline instances
     n_offline = 0 # Count of offline nodes
     n_drained = 0 # Count of nodes being drained
     node_vol_should = {}
@@ -2670,6 +3082,10 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
 
     feedback_fn("* Gathering data (%d nodes)" % len(self.my_node_names))
 
+    user_scripts = []
+    if self.cfg.GetUseExternalMipScript():
+      user_scripts.append(constants.EXTERNAL_MASTER_SETUP_SCRIPT)
+
     node_verify_param = {
       constants.NV_FILELIST:
         utils.UniqueSequence(filename
@@ -2692,6 +3108,7 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
       constants.NV_MASTERIP: (master_node, master_ip),
       constants.NV_OSLIST: None,
       constants.NV_VMNODES: self.cfg.GetNonVmCapableNodeList(),
+      constants.NV_USERSCRIPTS: user_scripts,
       }
 
     if vg_name is not None:
@@ -2736,6 +3153,8 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
 
     for instance in self.my_inst_names:
       inst_config = self.my_inst_info[instance]
+      if inst_config.admin_state == constants.ADMINST_OFFLINE:
+        i_offline += 1
 
       for nname in inst_config.all_nodes:
         if nname not in node_image:
@@ -2839,7 +3258,8 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
         feedback_fn("* Verifying node %s (%s)" % (node, ntype))
 
       msg = all_nvinfo[node].fail_msg
-      _ErrorIf(msg, self.ENODERPC, node, "while contacting node: %s", msg)
+      _ErrorIf(msg, constants.CV_ENODERPC, node, "while contacting node: %s",
+               msg)
       if msg:
         nimg.rpc_fail = True
         continue
@@ -2849,6 +3269,7 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
       nimg.call_ok = self._VerifyNode(node_i, nresult)
       self._VerifyNodeTime(node_i, nresult, nvinfo_starttime, nvinfo_endtime)
       self._VerifyNodeNetwork(node_i, nresult)
+      self._VerifyNodeUserScripts(node_i, nresult)
       self._VerifyOob(node_i, nresult)
 
       if nimg.vm_capable:
@@ -2874,9 +3295,9 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
 
         for inst in non_primary_inst:
           test = inst in self.all_inst_info
-          _ErrorIf(test, self.EINSTANCEWRONGNODE, inst,
+          _ErrorIf(test, constants.CV_EINSTANCEWRONGNODE, inst,
                    "instance should not run on node %s", node_i.name)
-          _ErrorIf(not test, self.ENODEORPHANINSTANCE, node_i.name,
+          _ErrorIf(not test, constants.CV_ENODEORPHANINSTANCE, node_i.name,
                    "node is running unknown instance %s", inst)
 
     for node, result in extra_lv_nvinfo.items():
@@ -2895,11 +3316,12 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
       pnode = inst_config.primary_node
       pnode_img = node_image[pnode]
       _ErrorIf(pnode_img.rpc_fail and not pnode_img.offline,
-               self.ENODERPC, pnode, "instance %s, connection to"
+               constants.CV_ENODERPC, pnode, "instance %s, connection to"
                " primary node failed", instance)
 
-      _ErrorIf(inst_config.admin_up and pnode_img.offline,
-               self.EINSTANCEBADNODE, instance,
+      _ErrorIf(inst_config.admin_state == constants.ADMINST_UP and
+               pnode_img.offline,
+               constants.CV_EINSTANCEBADNODE, instance,
                "instance is marked as running and lives on offline node %s",
                inst_config.primary_node)
 
@@ -2911,7 +3333,8 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
       if not inst_config.secondary_nodes:
         i_non_redundant.append(instance)
 
-      _ErrorIf(len(inst_config.secondary_nodes) > 1, self.EINSTANCELAYOUT,
+      _ErrorIf(len(inst_config.secondary_nodes) > 1,
+               constants.CV_EINSTANCELAYOUT,
                instance, "instance has multiple secondary nodes: %s",
                utils.CommaJoin(inst_config.secondary_nodes),
                code=self.ETYPE_WARNING)
@@ -2932,7 +3355,8 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
                                      key=lambda (_, nodes): pnode in nodes,
                                      reverse=True)]
 
-        self._ErrorIf(len(instance_groups) > 1, self.EINSTANCESPLITGROUPS,
+        self._ErrorIf(len(instance_groups) > 1,
+                      constants.CV_EINSTANCESPLITGROUPS,
                       instance, "instance has primary and secondary nodes in"
                       " different groups: %s", utils.CommaJoin(pretty_list),
                       code=self.ETYPE_WARNING)
@@ -2942,21 +3366,22 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
 
       for snode in inst_config.secondary_nodes:
         s_img = node_image[snode]
-        _ErrorIf(s_img.rpc_fail and not s_img.offline, self.ENODERPC, snode,
-                 "instance %s, connection to secondary node failed", instance)
+        _ErrorIf(s_img.rpc_fail and not s_img.offline, constants.CV_ENODERPC,
+                 snode, "instance %s, connection to secondary node failed",
+                 instance)
 
         if s_img.offline:
           inst_nodes_offline.append(snode)
 
       # warn that the instance lives on offline nodes
-      _ErrorIf(inst_nodes_offline, self.EINSTANCEBADNODE, instance,
+      _ErrorIf(inst_nodes_offline, constants.CV_EINSTANCEBADNODE, instance,
                "instance has offline secondary node(s) %s",
                utils.CommaJoin(inst_nodes_offline))
       # ... or ghost/non-vm_capable nodes
       for node in inst_config.all_nodes:
-        _ErrorIf(node_image[node].ghost, self.EINSTANCEBADNODE, instance,
-                 "instance lives on ghost node %s", node)
-        _ErrorIf(not node_image[node].vm_capable, self.EINSTANCEBADNODE,
+        _ErrorIf(node_image[node].ghost, constants.CV_EINSTANCEBADNODE,
+                 instance, "instance lives on ghost node %s", node)
+        _ErrorIf(not node_image[node].vm_capable, constants.CV_EINSTANCEBADNODE,
                  instance, "instance lives on non-vm_capable node %s", node)
 
     feedback_fn("* Verifying orphan volumes")
@@ -2987,6 +3412,9 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
       feedback_fn("  - NOTICE: %d non-auto-balanced instance(s) found."
                   % len(i_non_a_balanced))
 
+    if i_offline:
+      feedback_fn("  - NOTICE: %d offline instance(s) found." % i_offline)
+
     if n_offline:
       feedback_fn("  - NOTICE: %d offline node(s) found." % n_offline)
 
@@ -3024,7 +3452,7 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
         res = hooks_results[node_name]
         msg = res.fail_msg
         test = msg and not res.offline
-        self._ErrorIf(test, self.ENODEHOOKS, node_name,
+        self._ErrorIf(test, constants.CV_ENODEHOOKS, node_name,
                       "Communication failure in hooks execution: %s", msg)
         if res.offline or msg:
           # No need to investigate payload if node is offline or gave
@@ -3032,7 +3460,7 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
           continue
         for script, hkr, output in res.payload:
           test = hkr == constants.HKR_FAIL
-          self._ErrorIf(test, self.ENODEHOOKS, node_name,
+          self._ErrorIf(test, constants.CV_ENODEHOOKS, node_name,
                         "Script %s failed, output:", script)
           if test:
             output = self._HOOKS_INDENT_RE.sub("      ", output)
@@ -3125,15 +3553,8 @@ class LUGroupVerifyDisks(NoHooksLU):
     self.instances = dict(self.cfg.GetMultiInstanceInfo(owned_instances))
 
     # Check if node groups for locked instances are still correct
-    for (instance_name, inst) in self.instances.items():
-      assert owned_nodes.issuperset(inst.all_nodes), \
-        "Instance %s's nodes changed while we kept the lock" % instance_name
-
-      inst_groups = _CheckInstanceNodeGroups(self.cfg, instance_name,
-                                             owned_groups)
-
-      assert self.group_uuid in inst_groups, \
-        "Instance %s has no node in group %s" % (instance_name, self.group_uuid)
+    _CheckInstancesNodeGroups(self.cfg, self.instances,
+                              owned_groups, owned_nodes, self.group_uuid)
 
   def Exec(self, feedback_fn):
     """Verify integrity of cluster disks.
@@ -3149,8 +3570,8 @@ class LUGroupVerifyDisks(NoHooksLU):
     res_missing = {}
 
     nv_dict = _MapInstanceDisksToNodes([inst
-                                        for inst in self.instances.values()
-                                        if inst.admin_up])
+            for inst in self.instances.values()
+            if inst.admin_state == constants.ADMINST_UP])
 
     if nv_dict:
       nodes = utils.NiceSort(set(self.owned_locks(locking.LEVEL_NODE)) &
@@ -3191,24 +3612,24 @@ class LUClusterRepairDiskSizes(NoHooksLU):
     if self.op.instances:
       self.wanted_names = _GetWantedInstances(self, self.op.instances)
       self.needed_locks = {
-        locking.LEVEL_NODE: [],
+        locking.LEVEL_NODE_RES: [],
         locking.LEVEL_INSTANCE: self.wanted_names,
         }
-      self.recalculate_locks[locking.LEVEL_NODE] = constants.LOCKS_REPLACE
+      self.recalculate_locks[locking.LEVEL_NODE_RES] = constants.LOCKS_REPLACE
     else:
       self.wanted_names = None
       self.needed_locks = {
-        locking.LEVEL_NODE: locking.ALL_SET,
+        locking.LEVEL_NODE_RES: locking.ALL_SET,
         locking.LEVEL_INSTANCE: locking.ALL_SET,
         }
     self.share_locks = {
-      locking.LEVEL_NODE: 1,
+      locking.LEVEL_NODE_RES: 1,
       locking.LEVEL_INSTANCE: 0,
       }
 
   def DeclareLocks(self, level):
-    if level == locking.LEVEL_NODE and self.wanted_names is not None:
-      self._LockInstancesNodes(primary_only=True)
+    if level == locking.LEVEL_NODE_RES and self.wanted_names is not None:
+      self._LockInstancesNodes(primary_only=True, level=level)
 
   def CheckPrereq(self):
     """Check prerequisites.
@@ -3259,6 +3680,11 @@ class LUClusterRepairDiskSizes(NoHooksLU):
       for idx, disk in enumerate(instance.disks):
         per_node_disks[pnode].append((instance, idx, disk))
 
+    assert not (frozenset(per_node_disks.keys()) -
+                self.owned_locks(locking.LEVEL_NODE_RES)), \
+      "Not owning correct locks"
+    assert not self.owned_locks(locking.LEVEL_NODE)
+
     changed = []
     for node, dskl in per_node_disks.items():
       newl = [v[2].Copy() for v in dskl]
@@ -3348,29 +3774,33 @@ class LUClusterRename(LogicalUnit):
 
     """
     clustername = self.op.name
-    ip = self.ip
+    new_ip = self.ip
 
     # shutdown the master IP
-    master = self.cfg.GetMasterNode()
-    result = self.rpc.call_node_stop_master(master, False)
+    master_params = self.cfg.GetMasterNetworkParameters()
+    ems = self.cfg.GetUseExternalMipScript()
+    result = self.rpc.call_node_deactivate_master_ip(master_params.name,
+                                                     master_params, ems)
     result.Raise("Could not disable the master role")
 
     try:
       cluster = self.cfg.GetClusterInfo()
       cluster.cluster_name = clustername
-      cluster.master_ip = ip
+      cluster.master_ip = new_ip
       self.cfg.Update(cluster, feedback_fn)
 
       # update the known hosts file
       ssh.WriteKnownHostsFile(self.cfg, constants.SSH_KNOWN_HOSTS_FILE)
       node_list = self.cfg.GetOnlineNodeList()
       try:
-        node_list.remove(master)
+        node_list.remove(master_params.name)
       except ValueError:
         pass
       _UploadHelper(self, node_list, constants.SSH_KNOWN_HOSTS_FILE)
     finally:
-      result = self.rpc.call_node_start_master(master, False, False)
+      master_params.ip = new_ip
+      result = self.rpc.call_node_activate_master_ip(master_params.name,
+                                                     master_params, ems)
       msg = result.fail_msg
       if msg:
         self.LogWarning("Could not re-enable the master role on"
@@ -3379,6 +3809,27 @@ class LUClusterRename(LogicalUnit):
     return clustername
 
 
+def _ValidateNetmask(cfg, netmask):
+  """Checks if a netmask is valid.
+
+  @type cfg: L{config.ConfigWriter}
+  @param cfg: The cluster configuration
+  @type netmask: int
+  @param netmask: the netmask to be verified
+  @raise errors.OpPrereqError: if the validation fails
+
+  """
+  ip_family = cfg.GetPrimaryIPFamily()
+  try:
+    ipcls = netutils.IPAddress.GetClassFromIpFamily(ip_family)
+  except errors.ProgrammerError:
+    raise errors.OpPrereqError("Invalid primary ip family: %s." %
+                               ip_family)
+  if not ipcls.ValidateNetmask(netmask):
+    raise errors.OpPrereqError("CIDR netmask (%s) not valid" %
+                                (netmask))
+
+
 class LUClusterSetParams(LogicalUnit):
   """Change the parameters of the cluster.
 
@@ -3400,13 +3851,31 @@ class LUClusterSetParams(LogicalUnit):
     if self.op.remove_uids:
       uidpool.CheckUidPool(self.op.remove_uids)
 
+    if self.op.master_netmask is not None:
+      _ValidateNetmask(self.cfg, self.op.master_netmask)
+
+    if self.op.diskparams:
+      for dt_params in self.op.diskparams.values():
+        utils.ForceDictType(dt_params, constants.DISK_DT_TYPES)
+      try:
+        utils.VerifyDictOptions(self.op.diskparams, constants.DISK_DT_DEFAULTS)
+      except errors.OpPrereqError, err:
+        raise errors.OpPrereqError("While verify diskparams options: %s" % err,
+                                   errors.ECODE_INVAL)
+
   def ExpandNames(self):
     # FIXME: in the future maybe other cluster params won't require checking on
     # all nodes to be modified.
     self.needed_locks = {
       locking.LEVEL_NODE: locking.ALL_SET,
+      locking.LEVEL_INSTANCE: locking.ALL_SET,
+      locking.LEVEL_NODEGROUP: locking.ALL_SET,
+    }
+    self.share_locks = {
+        locking.LEVEL_NODE: 1,
+        locking.LEVEL_INSTANCE: 1,
+        locking.LEVEL_NODEGROUP: 1,
     }
-    self.share_locks[locking.LEVEL_NODE] = 1
 
   def BuildHooksEnv(self):
     """Build hooks env.
@@ -3481,6 +3950,7 @@ class LUClusterSetParams(LogicalUnit):
     self.cluster = cluster = self.cfg.GetClusterInfo()
     # validate params changes
     if self.op.beparams:
+      objects.UpgradeBeParams(self.op.beparams)
       utils.ForceDictType(self.op.beparams, constants.BES_PARAMETER_TYPES)
       self.new_beparams = cluster.SimpleFillBE(self.op.beparams)
 
@@ -3494,6 +3964,42 @@ class LUClusterSetParams(LogicalUnit):
         self.new_ndparams["oob_program"] = \
             constants.NDC_DEFAULTS[constants.ND_OOB_PROGRAM]
 
+    if self.op.hv_state:
+      new_hv_state = _MergeAndVerifyHvState(self.op.hv_state,
+                                            self.cluster.hv_state_static)
+      self.new_hv_state = dict((hv, cluster.SimpleFillHvState(values))
+                               for hv, values in new_hv_state.items())
+
+    if self.op.disk_state:
+      new_disk_state = _MergeAndVerifyDiskState(self.op.disk_state,
+                                                self.cluster.disk_state_static)
+      self.new_disk_state = \
+        dict((storage, dict((name, cluster.SimpleFillDiskState(values))
+                            for name, values in svalues.items()))
+             for storage, svalues in new_disk_state.items())
+
+    if self.op.ipolicy:
+      self.new_ipolicy = _GetUpdatedIPolicy(cluster.ipolicy, self.op.ipolicy,
+                                            group_policy=False)
+
+      all_instances = self.cfg.GetAllInstancesInfo().values()
+      violations = set()
+      for group in self.cfg.GetAllNodeGroupsInfo().values():
+        instances = frozenset([inst for inst in all_instances
+                               if compat.any(node in group.members
+                                             for node in inst.all_nodes)])
+        new_ipolicy = objects.FillIPolicy(self.new_ipolicy, group.ipolicy)
+        new = _ComputeNewInstanceViolations(_CalculateGroupIPolicy(cluster,
+                                                                   group),
+                                            new_ipolicy, instances)
+        if new:
+          violations.update(new)
+
+      if violations:
+        self.LogWarning("After the ipolicy change the following instances"
+                        " violate them: %s",
+                        utils.CommaJoin(utils.NiceSort(violations)))
+
     if self.op.nicparams:
       utils.ForceDictType(self.op.nicparams, constants.NICS_PARAMETER_TYPES)
       self.new_nicparams = cluster.SimpleFillNIC(self.op.nicparams)
@@ -3531,6 +4037,15 @@ class LUClusterSetParams(LogicalUnit):
         else:
           self.new_hvparams[hv_name].update(hv_dict)
 
+    # disk template parameters
+    self.new_diskparams = objects.FillDict(cluster.diskparams, {})
+    if self.op.diskparams:
+      for dt_name, dt_params in self.op.diskparams.items():
+        if dt_name not in self.op.diskparams:
+          self.new_diskparams[dt_name] = dt_params
+        else:
+          self.new_diskparams[dt_name].update(dt_params)
+
     # os hypervisor parameters
     self.new_os_hvp = objects.FillDict(cluster.os_hvp, {})
     if self.op.os_hvp:
@@ -3645,10 +4160,18 @@ class LUClusterSetParams(LogicalUnit):
       self.cluster.beparams[constants.PP_DEFAULT] = self.new_beparams
     if self.op.nicparams:
       self.cluster.nicparams[constants.PP_DEFAULT] = self.new_nicparams
+    if self.op.ipolicy:
+      self.cluster.ipolicy = self.new_ipolicy
     if self.op.osparams:
       self.cluster.osparams = self.new_osp
     if self.op.ndparams:
       self.cluster.ndparams = self.new_ndparams
+    if self.op.diskparams:
+      self.cluster.diskparams = self.new_diskparams
+    if self.op.hv_state:
+      self.cluster.hv_state_static = self.new_hv_state
+    if self.op.disk_state:
+      self.cluster.disk_state_static = self.new_disk_state
 
     if self.op.candidate_pool_size is not None:
       self.cluster.candidate_pool_size = self.op.candidate_pool_size
@@ -3656,6 +4179,9 @@ class LUClusterSetParams(LogicalUnit):
       _AdjustCandidatePool(self, [])
 
     if self.op.maintain_node_health is not None:
+      if self.op.maintain_node_health and not constants.ENABLE_CONFD:
+        feedback_fn("Note: CONFD was disabled at build time, node health"
+                    " maintenance is not useful (still enabling it)")
       self.cluster.maintain_node_health = self.op.maintain_node_health
 
     if self.op.prealloc_wipe_disks is not None:
@@ -3676,6 +4202,9 @@ class LUClusterSetParams(LogicalUnit):
     if self.op.reserved_lvs is not None:
       self.cluster.reserved_lvs = self.op.reserved_lvs
 
+    if self.op.use_external_mip_script is not None:
+      self.cluster.use_external_mip_script = self.op.use_external_mip_script
+
     def helper_os(aname, mods, desc):
       desc += " OS list"
       lst = getattr(self.cluster, aname)
@@ -3700,21 +4229,40 @@ class LUClusterSetParams(LogicalUnit):
       helper_os("blacklisted_os", self.op.blacklisted_os, "blacklisted")
 
     if self.op.master_netdev:
-      master = self.cfg.GetMasterNode()
+      master_params = self.cfg.GetMasterNetworkParameters()
+      ems = self.cfg.GetUseExternalMipScript()
       feedback_fn("Shutting down master ip on the current netdev (%s)" %
                   self.cluster.master_netdev)
-      result = self.rpc.call_node_stop_master(master, False)
+      result = self.rpc.call_node_deactivate_master_ip(master_params.name,
+                                                       master_params, ems)
       result.Raise("Could not disable the master ip")
       feedback_fn("Changing master_netdev from %s to %s" %
-                  (self.cluster.master_netdev, self.op.master_netdev))
+                  (master_params.netdev, self.op.master_netdev))
       self.cluster.master_netdev = self.op.master_netdev
 
+    if self.op.master_netmask:
+      master_params = self.cfg.GetMasterNetworkParameters()
+      feedback_fn("Changing master IP netmask to %s" % self.op.master_netmask)
+      result = self.rpc.call_node_change_master_netmask(master_params.name,
+                                                        master_params.netmask,
+                                                        self.op.master_netmask,
+                                                        master_params.ip,
+                                                        master_params.netdev)
+      if result.fail_msg:
+        msg = "Could not change the master IP netmask: %s" % result.fail_msg
+        feedback_fn(msg)
+
+      self.cluster.master_netmask = self.op.master_netmask
+
     self.cfg.Update(self.cluster, feedback_fn)
 
     if self.op.master_netdev:
+      master_params = self.cfg.GetMasterNetworkParameters()
       feedback_fn("Starting the master ip on the new master netdev (%s)" %
                   self.op.master_netdev)
-      result = self.rpc.call_node_start_master(master, False, False)
+      ems = self.cfg.GetUseExternalMipScript()
+      result = self.rpc.call_node_activate_master_ip(master_params.name,
+                                                     master_params, ems)
       if result.fail_msg:
         self.LogWarning("Could not re-enable the master ip on"
                         " the master, please restart manually: %s",
@@ -3747,6 +4295,9 @@ def _ComputeAncillaryFiles(cluster, redist):
     constants.SSH_KNOWN_HOSTS_FILE,
     constants.CONFD_HMAC_KEY,
     constants.CLUSTER_DOMAIN_SECRET_FILE,
+    constants.SPICE_CERT_FILE,
+    constants.SPICE_CACERT_FILE,
+    constants.RAPI_USERS_FILE,
     ])
 
   if not redist:
@@ -3759,27 +4310,42 @@ def _ComputeAncillaryFiles(cluster, redist):
   if cluster.modify_etc_hosts:
     files_all.add(constants.ETC_HOSTS)
 
-  # Files which must either exist on all nodes or on none
-  files_all_opt = set([
+  if cluster.use_external_mip_script:
+    files_all.add(constants.EXTERNAL_MASTER_SETUP_SCRIPT)
+
+  # Files which are optional, these must:
+  # - be present in one other category as well
+  # - either exist or not exist on all nodes of that category (mc, vm all)
+  files_opt = set([
     constants.RAPI_USERS_FILE,
     ])
 
   # Files which should only be on master candidates
   files_mc = set()
+
   if not redist:
     files_mc.add(constants.CLUSTER_CONF_FILE)
 
   # Files which should only be on VM-capable nodes
   files_vm = set(filename
     for hv_name in cluster.enabled_hypervisors
-    for filename in hypervisor.GetHypervisor(hv_name).GetAncillaryFiles())
+    for filename in hypervisor.GetHypervisor(hv_name).GetAncillaryFiles()[0])
+
+  files_opt |= set(filename
+    for hv_name in cluster.enabled_hypervisors
+    for filename in hypervisor.GetHypervisor(hv_name).GetAncillaryFiles()[1])
 
-  # Filenames must be unique
-  assert (len(files_all | files_all_opt | files_mc | files_vm) ==
-          sum(map(len, [files_all, files_all_opt, files_mc, files_vm]))), \
+  # Filenames in each category must be unique
+  all_files_set = files_all | files_mc | files_vm
+  assert (len(all_files_set) ==
+          sum(map(len, [files_all, files_mc, files_vm]))), \
          "Found file listed in more than one file list"
 
-  return (files_all, files_all_opt, files_mc, files_vm)
+  # Optional files must be present in one other category
+  assert all_files_set.issuperset(files_opt), \
+         "Optional file not in a different required list"
+
+  return (files_all, files_opt, files_mc, files_vm)
 
 
 def _RedistributeAncillaryFiles(lu, additional_nodes=None, additional_vm=True):
@@ -3800,7 +4366,8 @@ def _RedistributeAncillaryFiles(lu, additional_nodes=None, additional_vm=True):
   master_info = lu.cfg.GetNodeInfo(lu.cfg.GetMasterNode())
 
   online_nodes = lu.cfg.GetOnlineNodeList()
-  vm_nodes = lu.cfg.GetVmCapableNodeList()
+  online_set = frozenset(online_nodes)
+  vm_nodes = list(online_set.intersection(lu.cfg.GetVmCapableNodeList()))
 
   if additional_nodes is not None:
     online_nodes.extend(additional_nodes)
@@ -3813,7 +4380,7 @@ def _RedistributeAncillaryFiles(lu, additional_nodes=None, additional_vm=True):
       nodelist.remove(master_info.name)
 
   # Gather file lists
-  (files_all, files_all_opt, files_mc, files_vm) = \
+  (files_all, _, files_mc, files_vm) = \
     _ComputeAncillaryFiles(cluster, True)
 
   # Never re-distribute configuration file from here
@@ -3823,7 +4390,6 @@ def _RedistributeAncillaryFiles(lu, additional_nodes=None, additional_vm=True):
 
   filemap = [
     (online_nodes, files_all),
-    (online_nodes, files_all_opt),
     (vm_nodes, files_vm),
     ]
 
@@ -3855,6 +4421,36 @@ class LUClusterRedistConf(NoHooksLU):
     _RedistributeAncillaryFiles(self)
 
 
+class LUClusterActivateMasterIp(NoHooksLU):
+  """Activate the master IP on the master node.
+
+  """
+  def Exec(self, feedback_fn):
+    """Activate the master IP.
+
+    """
+    master_params = self.cfg.GetMasterNetworkParameters()
+    ems = self.cfg.GetUseExternalMipScript()
+    result = self.rpc.call_node_activate_master_ip(master_params.name,
+                                                   master_params, ems)
+    result.Raise("Could not activate the master IP")
+
+
+class LUClusterDeactivateMasterIp(NoHooksLU):
+  """Deactivate the master IP on the master node.
+
+  """
+  def Exec(self, feedback_fn):
+    """Deactivate the master IP.
+
+    """
+    master_params = self.cfg.GetMasterNetworkParameters()
+    ems = self.cfg.GetUseExternalMipScript()
+    result = self.rpc.call_node_deactivate_master_ip(master_params.name,
+                                                     master_params, ems)
+    result.Raise("Could not deactivate the master IP")
+
+
 def _WaitForSync(lu, instance, disks=None, oneshot=False):
   """Sleep and poll for an instance's disk to sync.
 
@@ -3880,7 +4476,7 @@ def _WaitForSync(lu, instance, disks=None, oneshot=False):
     max_time = 0
     done = True
     cumul_degraded = False
-    rstats = lu.rpc.call_blockdev_getmirrorstatus(node, disks)
+    rstats = lu.rpc.call_blockdev_getmirrorstatus(node, (disks, instance))
     msg = rstats.fail_msg
     if msg:
       lu.LogWarning("Can't get any data from node %s: %s", node, msg)
@@ -3930,9 +4526,35 @@ def _WaitForSync(lu, instance, disks=None, oneshot=False):
   return not cumul_degraded
 
 
-def _CheckDiskConsistency(lu, dev, node, on_primary, ldisk=False):
+def _BlockdevFind(lu, node, dev, instance):
+  """Wrapper around call_blockdev_find to annotate diskparams.
+
+  @param lu: A reference to the lu object
+  @param node: The node to call out
+  @param dev: The device to find
+  @param instance: The instance object the device belongs to
+  @returns The result of the rpc call
+
+  """
+  (disk,) = _AnnotateDiskParams(instance, [dev], lu.cfg)
+  return lu.rpc.call_blockdev_find(node, disk)
+
+
+def _CheckDiskConsistency(lu, instance, dev, node, on_primary, ldisk=False):
+  """Wrapper around L{_CheckDiskConsistencyInner}.
+
+  """
+  (disk,) = _AnnotateDiskParams(instance, [dev], lu.cfg)
+  return _CheckDiskConsistencyInner(lu, instance, disk, node, on_primary,
+                                    ldisk=ldisk)
+
+
+def _CheckDiskConsistencyInner(lu, instance, dev, node, on_primary,
+                               ldisk=False):
   """Check that mirrors are not degraded.
 
+  @attention: The device has to be annotated already.
+
   The ldisk parameter, if True, will change the test from the
   is_degraded attribute (which represents overall non-ok status for
   the device(s)) to the ldisk (representing the local storage status).
@@ -3959,7 +4581,8 @@ def _CheckDiskConsistency(lu, dev, node, on_primary, ldisk=False):
 
   if dev.children:
     for child in dev.children:
-      result = result and _CheckDiskConsistency(lu, child, node, on_primary)
+      result = result and _CheckDiskConsistencyInner(lu, instance, child, node,
+                                                     on_primary)
 
   return result
 
@@ -4315,9 +4938,6 @@ class LUNodeRemove(LogicalUnit):
   def BuildHooksEnv(self):
     """Build hooks env.
 
-    This doesn't run on the target node in the pre phase as a failed
-    node would then be impossible to remove.
-
     """
     return {
       "OP_TARGET": self.op.node_name,
@@ -4327,13 +4947,15 @@ class LUNodeRemove(LogicalUnit):
   def BuildHooksNodes(self):
     """Build hooks nodes.
 
+    This doesn't run on the target node in the pre phase as a failed
+    node would then be impossible to remove.
+
     """
     all_nodes = self.cfg.GetNodeList()
     try:
       all_nodes.remove(self.op.node_name)
     except ValueError:
-      logging.warning("Node '%s', which is about to be removed, was not found"
-                      " in the list of all nodes", self.op.node_name)
+      pass
     return (all_nodes, all_nodes)
 
   def CheckPrereq(self):
@@ -4374,6 +4996,9 @@ class LUNodeRemove(LogicalUnit):
 
     modify_ssh_setup = self.cfg.GetClusterInfo().modify_ssh_setup
 
+    assert locking.BGL in self.owned_locks(locking.LEVEL_CLUSTER), \
+      "Not owning BGL"
+
     # Promote nodes to master candidate as needed
     _AdjustCandidatePool(self, exceptions=[node.name])
     self.context.RemoveNode(node.name)
@@ -4432,9 +5057,9 @@ class _NodeQuery(_QueryBase):
       # filter out non-vm_capable nodes
       toquery_nodes = [name for name in nodenames if all_info[name].vm_capable]
 
-      node_data = lu.rpc.call_node_info(toquery_nodes, lu.cfg.GetVGName(),
-                                        lu.cfg.GetHypervisorType())
-      live_data = dict((name, nresult.payload)
+      node_data = lu.rpc.call_node_info(toquery_nodes, [lu.cfg.GetVGName()],
+                                        [lu.cfg.GetHypervisorType()])
+      live_data = dict((name, _MakeLegacyNodeInfo(nresult.payload))
                        for (name, nresult) in node_data.items()
                        if not nresult.fail_msg and nresult.payload)
     else:
@@ -4487,6 +5112,9 @@ class LUNodeQuery(NoHooksLU):
   def ExpandNames(self):
     self.nq.ExpandNames(self)
 
+  def DeclareLocks(self, level):
+    self.nq.DeclareLocks(self, level)
+
   def Exec(self, feedback_fn):
     return self.nq.OldStyleQuery(self)
 
@@ -4505,8 +5133,9 @@ class LUNodeQueryvols(NoHooksLU):
                        selected=self.op.output_fields)
 
   def ExpandNames(self):
+    self.share_locks = _ShareAll()
     self.needed_locks = {}
-    self.share_locks[locking.LEVEL_NODE] = 1
+
     if not self.op.nodes:
       self.needed_locks[locking.LEVEL_NODE] = locking.ALL_SET
     else:
@@ -4573,8 +5202,8 @@ class LUNodeQueryStorage(NoHooksLU):
                        selected=self.op.output_fields)
 
   def ExpandNames(self):
+    self.share_locks = _ShareAll()
     self.needed_locks = {}
-    self.share_locks[locking.LEVEL_NODE] = 1
 
     if self.op.nodes:
       self.needed_locks[locking.LEVEL_NODE] = \
@@ -4785,7 +5414,7 @@ class LUQuery(NoHooksLU):
   def CheckArguments(self):
     qcls = _GetQueryImplementation(self.op.what)
 
-    self.impl = qcls(self.op.filter, self.op.fields, self.op.use_locking)
+    self.impl = qcls(self.op.qfilter, self.op.fields, self.op.use_locking)
 
   def ExpandNames(self):
     self.impl.ExpandNames(self)
@@ -5033,8 +5662,15 @@ class LUNodeAdd(LogicalUnit):
     if self.op.ndparams:
       utils.ForceDictType(self.op.ndparams, constants.NDS_PARAMETER_TYPES)
 
-    # check connectivity
-    result = self.rpc.call_version([self.new_node.name])[self.new_node.name]
+    if self.op.hv_state:
+      self.new_hv_state = _MergeAndVerifyHvState(self.op.hv_state, None)
+
+    if self.op.disk_state:
+      self.new_disk_state = _MergeAndVerifyDiskState(self.op.disk_state, None)
+
+    # TODO: If we need to have multiple DnsOnlyRunner we probably should make
+    #       it a property on the base class.
+    result = rpc.DnsOnlyRunner().call_version([node])[node]
     result.Raise("Can't get version information from node %s" % node)
     if constants.PROTOCOL_VERSION == result.payload:
       logging.info("Communication to node %s fine, sw version %s match",
@@ -5052,6 +5688,9 @@ class LUNodeAdd(LogicalUnit):
     new_node = self.new_node
     node = new_node.name
 
+    assert locking.BGL in self.owned_locks(locking.LEVEL_CLUSTER), \
+      "Not owning BGL"
+
     # We adding a new node so we assume it's powered
     new_node.powered = True
 
@@ -5080,6 +5719,12 @@ class LUNodeAdd(LogicalUnit):
     else:
       new_node.ndparams = {}
 
+    if self.op.hv_state:
+      new_node.hv_state_static = self.new_hv_state
+
+    if self.op.disk_state:
+      new_node.disk_state_static = self.new_disk_state
+
     # Add node to our /etc/hosts, and add key to known_hosts
     if self.cfg.GetClusterInfo().modify_etc_hosts:
       master_node = self.cfg.GetMasterNode()
@@ -5155,7 +5800,8 @@ class LUNodeSetParams(LogicalUnit):
     self.op.node_name = _ExpandNodeName(self.cfg, self.op.node_name)
     all_mods = [self.op.offline, self.op.master_candidate, self.op.drained,
                 self.op.master_capable, self.op.vm_capable,
-                self.op.secondary_ip, self.op.ndparams]
+                self.op.secondary_ip, self.op.ndparams, self.op.hv_state,
+                self.op.disk_state]
     if all_mods.count(None) == len(all_mods):
       raise errors.OpPrereqError("Please pass at least one modification",
                                  errors.ECODE_INVAL)
@@ -5179,35 +5825,32 @@ class LUNodeSetParams(LogicalUnit):
     self.lock_all = self.op.auto_promote and self.might_demote
     self.lock_instances = self.op.secondary_ip is not None
 
+  def _InstanceFilter(self, instance):
+    """Filter for getting affected instances.
+
+    """
+    return (instance.disk_template in constants.DTS_INT_MIRROR and
+            self.op.node_name in instance.all_nodes)
+
   def ExpandNames(self):
     if self.lock_all:
       self.needed_locks = {locking.LEVEL_NODE: locking.ALL_SET}
     else:
       self.needed_locks = {locking.LEVEL_NODE: self.op.node_name}
 
-    if self.lock_instances:
-      self.needed_locks[locking.LEVEL_INSTANCE] = locking.ALL_SET
+    # Since modifying a node can have severe effects on currently running
+    # operations the resource lock is at least acquired in shared mode
+    self.needed_locks[locking.LEVEL_NODE_RES] = \
+      self.needed_locks[locking.LEVEL_NODE]
 
-  def DeclareLocks(self, level):
-    # If we have locked all instances, before waiting to lock nodes, release
-    # all the ones living on nodes unrelated to the current operation.
-    if level == locking.LEVEL_NODE and self.lock_instances:
-      self.affected_instances = []
-      if self.needed_locks[locking.LEVEL_NODE] is not locking.ALL_SET:
-        instances_keep = []
-
-        # Build list of instances to release
-        locked_i = self.owned_locks(locking.LEVEL_INSTANCE)
-        for instance_name, instance in self.cfg.GetMultiInstanceInfo(locked_i):
-          if (instance.disk_template in constants.DTS_INT_MIRROR and
-              self.op.node_name in instance.all_nodes):
-            instances_keep.append(instance_name)
-            self.affected_instances.append(instance)
-
-        _ReleaseLocks(self, locking.LEVEL_INSTANCE, keep=instances_keep)
-
-        assert (set(self.owned_locks(locking.LEVEL_INSTANCE)) ==
-                set(instances_keep))
+    # Get node resource and instance locks in shared mode; they are not used
+    # for anything but read-only access
+    self.share_locks[locking.LEVEL_NODE_RES] = 1
+    self.share_locks[locking.LEVEL_INSTANCE] = 1
+
+    if self.lock_instances:
+      self.needed_locks[locking.LEVEL_INSTANCE] = \
+        frozenset(self.cfg.GetInstancesInfoByFilter(self._InstanceFilter))
 
   def BuildHooksEnv(self):
     """Build hooks env.
@@ -5239,6 +5882,25 @@ class LUNodeSetParams(LogicalUnit):
     """
     node = self.node = self.cfg.GetNodeInfo(self.op.node_name)
 
+    if self.lock_instances:
+      affected_instances = \
+        self.cfg.GetInstancesInfoByFilter(self._InstanceFilter)
+
+      # Verify instance locks
+      owned_instances = self.owned_locks(locking.LEVEL_INSTANCE)
+      wanted_instances = frozenset(affected_instances.keys())
+      if wanted_instances - owned_instances:
+        raise errors.OpPrereqError("Instances affected by changing node %s's"
+                                   " secondary IP address have changed since"
+                                   " locks were acquired, wanted '%s', have"
+                                   " '%s'; retry the operation" %
+                                   (self.op.node_name,
+                                    utils.CommaJoin(wanted_instances),
+                                    utils.CommaJoin(owned_instances)),
+                                   errors.ECODE_STATE)
+    else:
+      affected_instances = None
+
     if (self.op.master_candidate is not None or
         self.op.drained is not None or
         self.op.offline is not None):
@@ -5269,7 +5931,8 @@ class LUNodeSetParams(LogicalUnit):
       if mc_remaining < mc_should:
         raise errors.OpPrereqError("Not enough master candidates, please"
                                    " pass auto promote option to allow"
-                                   " promotion", errors.ECODE_STATE)
+                                   " promotion (--auto-promote or RAPI"
+                                   " auto_promote=True)", errors.ECODE_STATE)
 
     self.old_flags = old_flags = (node.master_candidate,
                                   node.drained, node.offline)
@@ -5347,16 +6010,21 @@ class LUNodeSetParams(LogicalUnit):
         raise errors.OpPrereqError("Cannot change the secondary ip on a single"
                                    " homed cluster", errors.ECODE_INVAL)
 
+      assert not (frozenset(affected_instances) -
+                  self.owned_locks(locking.LEVEL_INSTANCE))
+
       if node.offline:
-        if self.affected_instances:
-          raise errors.OpPrereqError("Cannot change secondary ip: offline"
-                                     " node has instances (%s) configured"
-                                     " to use it" % self.affected_instances)
+        if affected_instances:
+          raise errors.OpPrereqError("Cannot change secondary IP address:"
+                                     " offline node has instances (%s)"
+                                     " configured to use it" %
+                                     utils.CommaJoin(affected_instances.keys()))
       else:
         # On online nodes, check that no instances are running, and that
         # the node has the new ip and we can reach it.
-        for instance in self.affected_instances:
-          _CheckInstanceDown(self, instance, "cannot change secondary ip")
+        for instance in affected_instances.values():
+          _CheckInstanceState(self, instance, INSTANCE_DOWN,
+                              msg="cannot change secondary ip")
 
         _CheckNodeHasSecondaryIP(self, node.name, self.op.secondary_ip, True)
         if master.name != node.name:
@@ -5373,6 +6041,15 @@ class LUNodeSetParams(LogicalUnit):
       utils.ForceDictType(new_ndparams, constants.NDS_PARAMETER_TYPES)
       self.new_ndparams = new_ndparams
 
+    if self.op.hv_state:
+      self.new_hv_state = _MergeAndVerifyHvState(self.op.hv_state,
+                                                 self.node.hv_state_static)
+
+    if self.op.disk_state:
+      self.new_disk_state = \
+        _MergeAndVerifyDiskState(self.op.disk_state,
+                                 self.node.disk_state_static)
+
   def Exec(self, feedback_fn):
     """Modifies a node.
 
@@ -5389,6 +6066,12 @@ class LUNodeSetParams(LogicalUnit):
     if self.op.powered is not None:
       node.powered = self.op.powered
 
+    if self.op.hv_state:
+      node.hv_state_static = self.new_hv_state
+
+    if self.op.disk_state:
+      node.disk_state_static = self.new_disk_state
+
     for attr in ["master_capable", "vm_capable"]:
       val = getattr(self.op, attr)
       if val is not None:
@@ -5493,20 +6176,24 @@ class LUClusterQuery(NoHooksLU):
       "config_version": constants.CONFIG_VERSION,
       "os_api_version": max(constants.OS_API_VERSIONS),
       "export_version": constants.EXPORT_VERSION,
-      "architecture": (platform.architecture()[0], platform.machine()),
+      "architecture": runtime.GetArchInfo(),
       "name": cluster.cluster_name,
       "master": cluster.master_node,
-      "default_hypervisor": cluster.enabled_hypervisors[0],
+      "default_hypervisor": cluster.primary_hypervisor,
       "enabled_hypervisors": cluster.enabled_hypervisors,
       "hvparams": dict([(hypervisor_name, cluster.hvparams[hypervisor_name])
                         for hypervisor_name in cluster.enabled_hypervisors]),
       "os_hvp": os_hvp,
       "beparams": cluster.beparams,
       "osparams": cluster.osparams,
+      "ipolicy": cluster.ipolicy,
       "nicparams": cluster.nicparams,
       "ndparams": cluster.ndparams,
+      "diskparams": cluster.diskparams,
       "candidate_pool_size": cluster.candidate_pool_size,
       "master_netdev": cluster.master_netdev,
+      "master_netmask": cluster.master_netmask,
+      "use_external_mip_script": cluster.use_external_mip_script,
       "volume_group_name": cluster.volume_group_name,
       "drbd_usermode_helper": cluster.drbd_usermode_helper,
       "file_storage_dir": cluster.file_storage_dir,
@@ -5533,57 +6220,89 @@ class LUClusterConfigQuery(NoHooksLU):
 
   """
   REQ_BGL = False
-  _FIELDS_DYNAMIC = utils.FieldSet()
-  _FIELDS_STATIC = utils.FieldSet("cluster_name", "master_node", "drain_flag",
-                                  "watcher_pause", "volume_group_name")
 
   def CheckArguments(self):
-    _CheckOutputFields(static=self._FIELDS_STATIC,
-                       dynamic=self._FIELDS_DYNAMIC,
-                       selected=self.op.output_fields)
+    self.cq = _ClusterQuery(None, self.op.output_fields, False)
 
   def ExpandNames(self):
-    self.needed_locks = {}
+    self.cq.ExpandNames(self)
+
+  def DeclareLocks(self, level):
+    self.cq.DeclareLocks(self, level)
 
   def Exec(self, feedback_fn):
-    """Dump a representation of the cluster config to the standard output.
-
-    """
-    values = []
-    for field in self.op.output_fields:
-      if field == "cluster_name":
-        entry = self.cfg.GetClusterName()
-      elif field == "master_node":
-        entry = self.cfg.GetMasterNode()
-      elif field == "drain_flag":
-        entry = os.path.exists(constants.JOB_QUEUE_DRAIN_FILE)
-      elif field == "watcher_pause":
-        entry = utils.ReadWatcherPauseFile(constants.WATCHER_PAUSEFILE)
-      elif field == "volume_group_name":
-        entry = self.cfg.GetVGName()
-      else:
-        raise errors.ParameterError(field)
-      values.append(entry)
-    return values
+    result = self.cq.OldStyleQuery(self)
 
+    assert len(result) == 1
 
-class LUInstanceActivateDisks(NoHooksLU):
-  """Bring up an instance's disks.
+    return result[0]
 
-  """
-  REQ_BGL = False
 
-  def ExpandNames(self):
-    self._ExpandAndLockInstance()
-    self.needed_locks[locking.LEVEL_NODE] = []
-    self.recalculate_locks[locking.LEVEL_NODE] = constants.LOCKS_REPLACE
+class _ClusterQuery(_QueryBase):
+  FIELDS = query.CLUSTER_FIELDS
 
-  def DeclareLocks(self, level):
-    if level == locking.LEVEL_NODE:
-      self._LockInstancesNodes()
+  #: Do not sort (there is only one item)
+  SORT_FIELD = None
 
-  def CheckPrereq(self):
-    """Check prerequisites.
+  def ExpandNames(self, lu):
+    lu.needed_locks = {}
+
+    # The following variables interact with _QueryBase._GetNames
+    self.wanted = locking.ALL_SET
+    self.do_locking = self.use_locking
+
+    if self.do_locking:
+      raise errors.OpPrereqError("Can not use locking for cluster queries",
+                                 errors.ECODE_INVAL)
+
+  def DeclareLocks(self, lu, level):
+    pass
+
+  def _GetQueryData(self, lu):
+    """Computes the list of nodes and their attributes.
+
+    """
+    # Locking is not used
+    assert not (compat.any(lu.glm.is_owned(level)
+                           for level in locking.LEVELS
+                           if level != locking.LEVEL_CLUSTER) or
+                self.do_locking or self.use_locking)
+
+    if query.CQ_CONFIG in self.requested_data:
+      cluster = lu.cfg.GetClusterInfo()
+    else:
+      cluster = NotImplemented
+
+    if query.CQ_QUEUE_DRAINED in self.requested_data:
+      drain_flag = os.path.exists(constants.JOB_QUEUE_DRAIN_FILE)
+    else:
+      drain_flag = NotImplemented
+
+    if query.CQ_WATCHER_PAUSE in self.requested_data:
+      watcher_pause = utils.ReadWatcherPauseFile(constants.WATCHER_PAUSEFILE)
+    else:
+      watcher_pause = NotImplemented
+
+    return query.ClusterQueryData(cluster, drain_flag, watcher_pause)
+
+
+class LUInstanceActivateDisks(NoHooksLU):
+  """Bring up an instance's disks.
+
+  """
+  REQ_BGL = False
+
+  def ExpandNames(self):
+    self._ExpandAndLockInstance()
+    self.needed_locks[locking.LEVEL_NODE] = []
+    self.recalculate_locks[locking.LEVEL_NODE] = constants.LOCKS_REPLACE
+
+  def DeclareLocks(self, level):
+    if level == locking.LEVEL_NODE:
+      self._LockInstancesNodes()
+
+  def CheckPrereq(self):
+    """Check prerequisites.
 
     This checks that the instance is in the cluster.
 
@@ -5651,13 +6370,16 @@ def _AssembleInstanceDisks(lu, instance, disks=None, ignore_secondaries=False,
         node_disk = node_disk.Copy()
         node_disk.UnsetSize()
       lu.cfg.SetDiskID(node_disk, node)
-      result = lu.rpc.call_blockdev_assemble(node, node_disk, iname, False, idx)
+      result = lu.rpc.call_blockdev_assemble(node, (node_disk, instance), iname,
+                                             False, idx)
       msg = result.fail_msg
       if msg:
+        is_offline_secondary = (node in instance.secondary_nodes and
+                                result.offline)
         lu.proc.LogWarning("Could not prepare block device %s on node %s"
                            " (is_primary=False, pass=1): %s",
                            inst_disk.iv_name, node, msg)
-        if not ignore_secondaries:
+        if not (ignore_secondaries or is_offline_secondary):
           disks_ok = False
 
   # FIXME: race condition on drbd migration to primary
@@ -5673,7 +6395,8 @@ def _AssembleInstanceDisks(lu, instance, disks=None, ignore_secondaries=False,
         node_disk = node_disk.Copy()
         node_disk.UnsetSize()
       lu.cfg.SetDiskID(node_disk, node)
-      result = lu.rpc.call_blockdev_assemble(node, node_disk, iname, True, idx)
+      result = lu.rpc.call_blockdev_assemble(node, (node_disk, instance), iname,
+                                             True, idx)
       msg = result.fail_msg
       if msg:
         lu.proc.LogWarning("Could not prepare block device %s on node %s"
@@ -5752,7 +6475,7 @@ def _SafeShutdownInstanceDisks(lu, instance, disks=None):
   _ShutdownInstanceDisks.
 
   """
-  _CheckInstanceDown(lu, instance, "cannot shutdown disks")
+  _CheckInstanceState(lu, instance, INSTANCE_DOWN, msg="cannot shutdown disks")
   _ShutdownInstanceDisks(lu, instance, disks=disks)
 
 
@@ -5789,7 +6512,7 @@ def _ShutdownInstanceDisks(lu, instance, disks=None, ignore_primary=False):
   for disk in disks:
     for node, top_disk in disk.ComputeNodeTree(instance.primary_node):
       lu.cfg.SetDiskID(top_disk, node)
-      result = lu.rpc.call_blockdev_shutdown(node, top_disk)
+      result = lu.rpc.call_blockdev_shutdown(node, (top_disk, instance))
       msg = result.fail_msg
       if msg:
         lu.LogWarning("Could not shutdown block device %s on node %s: %s",
@@ -5818,14 +6541,18 @@ def _CheckNodeFreeMemory(lu, node, reason, requested, hypervisor_name):
   @param requested: the amount of memory in MiB to check for
   @type hypervisor_name: C{str}
   @param hypervisor_name: the hypervisor to ask for memory stats
+  @rtype: integer
+  @return: node current free memory
   @raise errors.OpPrereqError: if the node doesn't have enough memory, or
       we cannot check the node
 
   """
-  nodeinfo = lu.rpc.call_node_info([node], None, hypervisor_name)
+  nodeinfo = lu.rpc.call_node_info([node], None, [hypervisor_name])
   nodeinfo[node].Raise("Can't get data from node %s" % node,
                        prereq=True, ecode=errors.ECODE_ENVIRON)
-  free_mem = nodeinfo[node].payload.get("memory_free", None)
+  (_, _, (hv_info, )) = nodeinfo[node].payload
+
+  free_mem = hv_info.get("memory_free", None)
   if not isinstance(free_mem, int):
     raise errors.OpPrereqError("Can't compute free memory on node %s, result"
                                " was '%s'" % (node, free_mem),
@@ -5835,6 +6562,7 @@ def _CheckNodeFreeMemory(lu, node, reason, requested, hypervisor_name):
                                " needed %s MiB, available %s MiB" %
                                (node, reason, requested, free_mem),
                                errors.ECODE_NORES)
+  return free_mem
 
 
 def _CheckNodesFreeDiskPerVG(lu, nodenames, req_sizes):
@@ -5880,12 +6608,13 @@ def _CheckNodesFreeDiskOnVG(lu, nodenames, vg, requested):
       or we cannot check the node
 
   """
-  nodeinfo = lu.rpc.call_node_info(nodenames, vg, None)
+  nodeinfo = lu.rpc.call_node_info(nodenames, [vg], None)
   for node in nodenames:
     info = nodeinfo[node]
     info.Raise("Cannot get current information from node %s" % node,
                prereq=True, ecode=errors.ECODE_ENVIRON)
-    vg_free = info.payload.get("vg_free", None)
+    (_, (vg_info, ), _) = info.payload
+    vg_free = vg_info.get("vg_free", None)
     if not isinstance(vg_free, int):
       raise errors.OpPrereqError("Can't compute free disk space on node"
                                  " %s for vg %s, result was '%s'" %
@@ -5897,6 +6626,41 @@ def _CheckNodesFreeDiskOnVG(lu, nodenames, vg, requested):
                                  errors.ECODE_NORES)
 
 
+def _CheckNodesPhysicalCPUs(lu, nodenames, requested, hypervisor_name):
+  """Checks if nodes have enough physical CPUs
+
+  This function checks if all given nodes have the needed number of
+  physical CPUs. In case any node has less CPUs or we cannot get the
+  information from the node, this function raises an OpPrereqError
+  exception.
+
+  @type lu: C{LogicalUnit}
+  @param lu: a logical unit from which we get configuration data
+  @type nodenames: C{list}
+  @param nodenames: the list of node names to check
+  @type requested: C{int}
+  @param requested: the minimum acceptable number of physical CPUs
+  @raise errors.OpPrereqError: if the node doesn't have enough CPUs,
+      or we cannot check the node
+
+  """
+  nodeinfo = lu.rpc.call_node_info(nodenames, None, [hypervisor_name])
+  for node in nodenames:
+    info = nodeinfo[node]
+    info.Raise("Cannot get current information from node %s" % node,
+               prereq=True, ecode=errors.ECODE_ENVIRON)
+    (_, _, (hv_info, )) = info.payload
+    num_cpus = hv_info.get("cpu_total", None)
+    if not isinstance(num_cpus, int):
+      raise errors.OpPrereqError("Can't compute the number of physical CPUs"
+                                 " on node %s, result was '%s'" %
+                                 (node, num_cpus), errors.ECODE_ENVIRON)
+    if requested > num_cpus:
+      raise errors.OpPrereqError("Node %s has %s physical CPUs, but %s are "
+                                 "required" % (node, num_cpus, requested),
+                                 errors.ECODE_NORES)
+
+
 class LUInstanceStartup(LogicalUnit):
   """Starts an instance.
 
@@ -5909,10 +6673,16 @@ class LUInstanceStartup(LogicalUnit):
     # extra beparams
     if self.op.beparams:
       # fill the beparams dict
+      objects.UpgradeBeParams(self.op.beparams)
       utils.ForceDictType(self.op.beparams, constants.BES_PARAMETER_TYPES)
 
   def ExpandNames(self):
     self._ExpandAndLockInstance()
+    self.recalculate_locks[locking.LEVEL_NODE_RES] = constants.LOCKS_REPLACE
+
+  def DeclareLocks(self, level):
+    if level == locking.LEVEL_NODE_RES:
+      self._LockInstancesNodes(primary_only=True, level=locking.LEVEL_NODE_RES)
 
   def BuildHooksEnv(self):
     """Build hooks env.
@@ -5956,6 +6726,8 @@ class LUInstanceStartup(LogicalUnit):
       hv_type.CheckParameterSyntax(filled_hvp)
       _CheckHVParams(self, instance.all_nodes, instance.hypervisor, filled_hvp)
 
+    _CheckInstanceState(self, instance, INSTANCE_ONLINE)
+
     self.primary_offline = self.cfg.GetNodeInfo(instance.primary_node).offline
 
     if self.primary_offline and self.op.ignore_offline_nodes:
@@ -5967,6 +6739,7 @@ class LUInstanceStartup(LogicalUnit):
       _CheckNodeOnline(self, instance.primary_node)
 
       bep = self.cfg.GetClusterInfo().FillBE(instance)
+      bep.update(self.op.beparams)
 
       # check bridges existence
       _CheckInstanceBridgesExist(self, instance)
@@ -5979,7 +6752,7 @@ class LUInstanceStartup(LogicalUnit):
       if not remote_info.payload: # not running already
         _CheckNodeFreeMemory(self, instance.primary_node,
                              "starting instance %s" % instance.name,
-                             bep[constants.BE_MEMORY], instance.hypervisor)
+                             bep[constants.BE_MINMEM], instance.hypervisor)
 
   def Exec(self, feedback_fn):
     """Start the instance.
@@ -5999,9 +6772,11 @@ class LUInstanceStartup(LogicalUnit):
 
       _StartInstanceDisks(self, instance, force)
 
-      result = self.rpc.call_instance_start(node_current, instance,
-                                            self.op.hvparams, self.op.beparams,
-                                            self.op.startup_paused)
+      result = \
+        self.rpc.call_instance_start(node_current,
+                                     (instance, self.op.hvparams,
+                                      self.op.beparams),
+                                     self.op.startup_paused)
       msg = result.fail_msg
       if msg:
         _ShutdownInstanceDisks(self, instance)
@@ -6051,7 +6826,7 @@ class LUInstanceReboot(LogicalUnit):
     self.instance = instance = self.cfg.GetInstanceInfo(self.op.instance_name)
     assert self.instance is not None, \
       "Cannot retrieve locked instance %s" % self.op.instance_name
-
+    _CheckInstanceState(self, instance, INSTANCE_ONLINE)
     _CheckNodeOnline(self, instance.primary_node)
 
     # check bridges existence
@@ -6091,8 +6866,8 @@ class LUInstanceReboot(LogicalUnit):
         self.LogInfo("Instance %s was already stopped, starting now",
                      instance.name)
       _StartInstanceDisks(self, instance, ignore_secondaries)
-      result = self.rpc.call_instance_start(node_current, instance,
-                                            None, None, False)
+      result = self.rpc.call_instance_start(node_current,
+                                            (instance, None, None), False)
       msg = result.fail_msg
       if msg:
         _ShutdownInstanceDisks(self, instance)
@@ -6140,6 +6915,8 @@ class LUInstanceShutdown(LogicalUnit):
     assert self.instance is not None, \
       "Cannot retrieve locked instance %s" % self.op.instance_name
 
+    _CheckInstanceState(self, self.instance, INSTANCE_ONLINE)
+
     self.primary_offline = \
       self.cfg.GetNodeInfo(self.instance.primary_node).offline
 
@@ -6208,15 +6985,12 @@ class LUInstanceReinstall(LogicalUnit):
       "Cannot retrieve locked instance %s" % self.op.instance_name
     _CheckNodeOnline(self, instance.primary_node, "Instance primary node"
                      " offline, cannot reinstall")
-    for node in instance.secondary_nodes:
-      _CheckNodeOnline(self, node, "Instance secondary node offline,"
-                       " cannot reinstall")
 
     if instance.disk_template == constants.DT_DISKLESS:
       raise errors.OpPrereqError("Instance '%s' has no disks" %
                                  self.op.instance_name,
                                  errors.ECODE_INVAL)
-    _CheckInstanceDown(self, instance, "cannot reinstall")
+    _CheckInstanceState(self, instance, INSTANCE_DOWN, msg="cannot reinstall")
 
     if self.op.os_type is not None:
       # OS verification
@@ -6253,9 +7027,9 @@ class LUInstanceReinstall(LogicalUnit):
     try:
       feedback_fn("Running the instance OS create scripts...")
       # FIXME: pass debug option from opcode to backend
-      result = self.rpc.call_instance_os_add(inst.primary_node, inst, True,
-                                             self.op.debug_level,
-                                             osparams=self.os_inst)
+      result = self.rpc.call_instance_os_add(inst.primary_node,
+                                             (inst, self.os_inst), True,
+                                             self.op.debug_level)
       result.Raise("Could not install OS for instance %s on node %s" %
                    (inst.name, inst.primary_node))
     finally:
@@ -6270,9 +7044,39 @@ class LUInstanceRecreateDisks(LogicalUnit):
   HTYPE = constants.HTYPE_INSTANCE
   REQ_BGL = False
 
+  _MODIFYABLE = frozenset([
+    constants.IDISK_SIZE,
+    constants.IDISK_MODE,
+    ])
+
+  # New or changed disk parameters may have different semantics
+  assert constants.IDISK_PARAMS == (_MODIFYABLE | frozenset([
+    constants.IDISK_ADOPT,
+
+    # TODO: Implement support changing VG while recreating
+    constants.IDISK_VG,
+    constants.IDISK_METAVG,
+    ]))
+
   def CheckArguments(self):
-    # normalise the disk list
-    self.op.disks = sorted(frozenset(self.op.disks))
+    if self.op.disks and ht.TPositiveInt(self.op.disks[0]):
+      # Normalize and convert deprecated list of disk indices
+      self.op.disks = [(idx, {}) for idx in sorted(frozenset(self.op.disks))]
+
+    duplicates = utils.FindDuplicates(map(compat.fst, self.op.disks))
+    if duplicates:
+      raise errors.OpPrereqError("Some disks have been specified more than"
+                                 " once: %s" % utils.CommaJoin(duplicates),
+                                 errors.ECODE_INVAL)
+
+    for (idx, params) in self.op.disks:
+      utils.ForceDictType(params, constants.IDISK_PARAMS_TYPES)
+      unsupported = frozenset(params.keys()) - self._MODIFYABLE
+      if unsupported:
+        raise errors.OpPrereqError("Parameters for disk %s try to change"
+                                   " unmodifyable parameter(s): %s" %
+                                   (idx, utils.CommaJoin(unsupported)),
+                                   errors.ECODE_INVAL)
 
   def ExpandNames(self):
     self._ExpandAndLockInstance()
@@ -6282,6 +7086,7 @@ class LUInstanceRecreateDisks(LogicalUnit):
       self.needed_locks[locking.LEVEL_NODE] = list(self.op.nodes)
     else:
       self.needed_locks[locking.LEVEL_NODE] = []
+    self.needed_locks[locking.LEVEL_NODE_RES] = []
 
   def DeclareLocks(self, level):
     if level == locking.LEVEL_NODE:
@@ -6289,6 +7094,10 @@ class LUInstanceRecreateDisks(LogicalUnit):
       # otherwise we need to lock all nodes for disk re-creation
       primary_only = bool(self.op.nodes)
       self._LockInstancesNodes(primary_only=primary_only)
+    elif level == locking.LEVEL_NODE_RES:
+      # Copy node locks
+      self.needed_locks[locking.LEVEL_NODE_RES] = \
+        self.needed_locks[locking.LEVEL_NODE][:]
 
   def BuildHooksEnv(self):
     """Build hooks env.
@@ -6333,24 +7142,32 @@ class LUInstanceRecreateDisks(LogicalUnit):
     if instance.disk_template == constants.DT_DISKLESS:
       raise errors.OpPrereqError("Instance '%s' has no disks" %
                                  self.op.instance_name, errors.ECODE_INVAL)
+
     # if we replace nodes *and* the old primary is offline, we don't
     # check
-    assert instance.primary_node in self.needed_locks[locking.LEVEL_NODE]
+    assert instance.primary_node in self.owned_locks(locking.LEVEL_NODE)
+    assert instance.primary_node in self.owned_locks(locking.LEVEL_NODE_RES)
     old_pnode = self.cfg.GetNodeInfo(instance.primary_node)
     if not (self.op.nodes and old_pnode.offline):
-      _CheckInstanceDown(self, instance, "cannot recreate disks")
+      _CheckInstanceState(self, instance, INSTANCE_NOT_RUNNING,
+                          msg="cannot recreate disks")
 
-    if not self.op.disks:
-      self.op.disks = range(len(instance.disks))
+    if self.op.disks:
+      self.disks = dict(self.op.disks)
     else:
-      for idx in self.op.disks:
-        if idx >= len(instance.disks):
-          raise errors.OpPrereqError("Invalid disk index '%s'" % idx,
-                                     errors.ECODE_INVAL)
-    if self.op.disks != range(len(instance.disks)) and self.op.nodes:
+      self.disks = dict((idx, {}) for idx in range(len(instance.disks)))
+
+    maxidx = max(self.disks.keys())
+    if maxidx >= len(instance.disks):
+      raise errors.OpPrereqError("Invalid disk index '%s'" % maxidx,
+                                 errors.ECODE_INVAL)
+
+    if (self.op.nodes and
+        sorted(self.disks.keys()) != range(len(instance.disks))):
       raise errors.OpPrereqError("Can't recreate disks partially and"
                                  " change the nodes at the same time",
                                  errors.ECODE_INVAL)
+
     self.instance = instance
 
   def Exec(self, feedback_fn):
@@ -6359,31 +7176,46 @@ class LUInstanceRecreateDisks(LogicalUnit):
     """
     instance = self.instance
 
+    assert (self.owned_locks(locking.LEVEL_NODE) ==
+            self.owned_locks(locking.LEVEL_NODE_RES))
+
     to_skip = []
-    mods = [] # keeps track of needed logical_id changes
+    mods = [] # keeps track of needed changes
 
     for idx, disk in enumerate(instance.disks):
-      if idx not in self.op.disks: # disk idx has not been passed in
+      try:
+        changes = self.disks[idx]
+      except KeyError:
+        # Disk should not be recreated
         to_skip.append(idx)
         continue
+
       # update secondaries for disks, if needed
-      if self.op.nodes:
-        if disk.dev_type == constants.LD_DRBD8:
-          # need to update the nodes and minors
-          assert len(self.op.nodes) == 2
-          assert len(disk.logical_id) == 6 # otherwise disk internals
-                                           # have changed
-          (_, _, old_port, _, _, old_secret) = disk.logical_id
-          new_minors = self.cfg.AllocateDRBDMinor(self.op.nodes, instance.name)
-          new_id = (self.op.nodes[0], self.op.nodes[1], old_port,
-                    new_minors[0], new_minors[1], old_secret)
-          assert len(disk.logical_id) == len(new_id)
-          mods.append((idx, new_id))
+      if self.op.nodes and disk.dev_type == constants.LD_DRBD8:
+        # need to update the nodes and minors
+        assert len(self.op.nodes) == 2
+        assert len(disk.logical_id) == 6 # otherwise disk internals
+                                         # have changed
+        (_, _, old_port, _, _, old_secret) = disk.logical_id
+        new_minors = self.cfg.AllocateDRBDMinor(self.op.nodes, instance.name)
+        new_id = (self.op.nodes[0], self.op.nodes[1], old_port,
+                  new_minors[0], new_minors[1], old_secret)
+        assert len(disk.logical_id) == len(new_id)
+      else:
+        new_id = None
+
+      mods.append((idx, new_id, changes))
 
     # now that we have passed all asserts above, we can apply the mods
     # in a single run (to avoid partial changes)
-    for idx, new_id in mods:
-      instance.disks[idx].logical_id = new_id
+    for idx, new_id, changes in mods:
+      disk = instance.disks[idx]
+      if new_id is not None:
+        assert disk.dev_type == constants.LD_DRBD8
+        disk.logical_id = new_id
+      if changes:
+        disk.Update(size=changes.get(constants.IDISK_SIZE, None),
+                    mode=changes.get(constants.IDISK_MODE, None))
 
     # change primary node, if needed
     if self.op.nodes:
@@ -6441,7 +7273,8 @@ class LUInstanceRename(LogicalUnit):
     instance = self.cfg.GetInstanceInfo(self.op.instance_name)
     assert instance is not None
     _CheckNodeOnline(self, instance.primary_node)
-    _CheckInstanceDown(self, instance, "cannot rename")
+    _CheckInstanceState(self, instance, INSTANCE_NOT_RUNNING,
+                        msg="cannot rename")
     self.instance = instance
 
     new_name = self.op.new_name
@@ -6527,11 +7360,16 @@ class LUInstanceRemove(LogicalUnit):
   def ExpandNames(self):
     self._ExpandAndLockInstance()
     self.needed_locks[locking.LEVEL_NODE] = []
+    self.needed_locks[locking.LEVEL_NODE_RES] = []
     self.recalculate_locks[locking.LEVEL_NODE] = constants.LOCKS_REPLACE
 
   def DeclareLocks(self, level):
     if level == locking.LEVEL_NODE:
       self._LockInstancesNodes()
+    elif level == locking.LEVEL_NODE_RES:
+      # Copy node locks
+      self.needed_locks[locking.LEVEL_NODE_RES] = \
+        self.needed_locks[locking.LEVEL_NODE][:]
 
   def BuildHooksEnv(self):
     """Build hooks env.
@@ -6580,6 +7418,12 @@ class LUInstanceRemove(LogicalUnit):
                                  " node %s: %s" %
                                  (instance.name, instance.primary_node, msg))
 
+    assert (self.owned_locks(locking.LEVEL_NODE) ==
+            self.owned_locks(locking.LEVEL_NODE_RES))
+    assert not (set(instance.all_nodes) -
+                self.owned_locks(locking.LEVEL_NODE)), \
+      "Not owning correct locks"
+
     _RemoveInstance(self, feedback_fn, instance, self.op.ignore_failures)
 
 
@@ -6650,13 +7494,17 @@ class LUInstanceFailover(LogicalUnit):
     self.needed_locks[locking.LEVEL_NODE] = []
     self.recalculate_locks[locking.LEVEL_NODE] = constants.LOCKS_REPLACE
 
+    self.needed_locks[locking.LEVEL_NODE_RES] = []
+    self.recalculate_locks[locking.LEVEL_NODE_RES] = constants.LOCKS_REPLACE
+
     ignore_consistency = self.op.ignore_consistency
     shutdown_timeout = self.op.shutdown_timeout
     self._migrater = TLMigrateInstance(self, self.op.instance_name,
                                        cleanup=False,
                                        failover=True,
                                        ignore_consistency=ignore_consistency,
-                                       shutdown_timeout=shutdown_timeout)
+                                       shutdown_timeout=shutdown_timeout,
+                                       ignore_ipolicy=self.op.ignore_ipolicy)
     self.tasklets = [self._migrater]
 
   def DeclareLocks(self, level):
@@ -6671,6 +7519,10 @@ class LUInstanceFailover(LogicalUnit):
         del self.recalculate_locks[locking.LEVEL_NODE]
       else:
         self._LockInstancesNodes()
+    elif level == locking.LEVEL_NODE_RES:
+      # Copy node locks
+      self.needed_locks[locking.LEVEL_NODE_RES] = \
+        self.needed_locks[locking.LEVEL_NODE][:]
 
   def BuildHooksEnv(self):
     """Build hooks env.
@@ -6727,10 +7579,16 @@ class LUInstanceMigrate(LogicalUnit):
     self.needed_locks[locking.LEVEL_NODE] = []
     self.recalculate_locks[locking.LEVEL_NODE] = constants.LOCKS_REPLACE
 
-    self._migrater = TLMigrateInstance(self, self.op.instance_name,
-                                       cleanup=self.op.cleanup,
-                                       failover=False,
-                                       fallback=self.op.allow_failover)
+    self.needed_locks[locking.LEVEL_NODE] = []
+    self.recalculate_locks[locking.LEVEL_NODE] = constants.LOCKS_REPLACE
+
+    self._migrater = \
+      TLMigrateInstance(self, self.op.instance_name,
+                        cleanup=self.op.cleanup,
+                        failover=False,
+                        fallback=self.op.allow_failover,
+                        allow_runtime_changes=self.op.allow_runtime_changes,
+                        ignore_ipolicy=self.op.ignore_ipolicy)
     self.tasklets = [self._migrater]
 
   def DeclareLocks(self, level):
@@ -6745,6 +7603,10 @@ class LUInstanceMigrate(LogicalUnit):
         del self.recalculate_locks[locking.LEVEL_NODE]
       else:
         self._LockInstancesNodes()
+    elif level == locking.LEVEL_NODE_RES:
+      # Copy node locks
+      self.needed_locks[locking.LEVEL_NODE_RES] = \
+        self.needed_locks[locking.LEVEL_NODE][:]
 
   def BuildHooksEnv(self):
     """Build hooks env.
@@ -6761,6 +7623,7 @@ class LUInstanceMigrate(LogicalUnit):
       "MIGRATE_CLEANUP": self.op.cleanup,
       "OLD_PRIMARY": source_node,
       "NEW_PRIMARY": target_node,
+      "ALLOW_RUNTIME_CHANGES": self.op.allow_runtime_changes,
       })
 
     if instance.disk_template in constants.DTS_INT_MIRROR:
@@ -6793,11 +7656,16 @@ class LUInstanceMove(LogicalUnit):
     target_node = _ExpandNodeName(self.cfg, self.op.target_node)
     self.op.target_node = target_node
     self.needed_locks[locking.LEVEL_NODE] = [target_node]
+    self.needed_locks[locking.LEVEL_NODE_RES] = []
     self.recalculate_locks[locking.LEVEL_NODE] = constants.LOCKS_APPEND
 
   def DeclareLocks(self, level):
     if level == locking.LEVEL_NODE:
       self._LockInstancesNodes(primary_only=True)
+    elif level == locking.LEVEL_NODE_RES:
+      # Copy node locks
+      self.needed_locks[locking.LEVEL_NODE_RES] = \
+        self.needed_locks[locking.LEVEL_NODE][:]
 
   def BuildHooksEnv(self):
     """Build hooks env.
@@ -6854,11 +7722,15 @@ class LUInstanceMove(LogicalUnit):
     _CheckNodeOnline(self, target_node)
     _CheckNodeNotDrained(self, target_node)
     _CheckNodeVmCapable(self, target_node)
+    ipolicy = _CalculateGroupIPolicy(self.cfg.GetClusterInfo(),
+                                     self.cfg.GetNodeGroup(node.group))
+    _CheckTargetNodeIPolicy(self, ipolicy, instance, node,
+                            ignore=self.op.ignore_ipolicy)
 
-    if instance.admin_up:
+    if instance.admin_state == constants.ADMINST_UP:
       # check memory requirements on the secondary node
       _CheckNodeFreeMemory(self, target_node, "failing over instance %s" %
-                           instance.name, bep[constants.BE_MEMORY],
+                           instance.name, bep[constants.BE_MAXMEM],
                            instance.hypervisor)
     else:
       self.LogInfo("Not checking memory on the secondary node as"
@@ -6882,6 +7754,9 @@ class LUInstanceMove(LogicalUnit):
     self.LogInfo("Shutting down instance %s on source node %s",
                  instance.name, source_node)
 
+    assert (self.owned_locks(locking.LEVEL_NODE) ==
+            self.owned_locks(locking.LEVEL_NODE_RES))
+
     result = self.rpc.call_instance_shutdown(source_node, instance,
                                              self.op.shutdown_timeout)
     msg = result.fail_msg
@@ -6913,7 +7788,7 @@ class LUInstanceMove(LogicalUnit):
     # activate, get path, copy the data over
     for idx, disk in enumerate(instance.disks):
       self.LogInfo("Copying data for disk %d", idx)
-      result = self.rpc.call_blockdev_assemble(target_node, disk,
+      result = self.rpc.call_blockdev_assemble(target_node, (disk, instance),
                                                instance.name, True, idx)
       if result.fail_msg:
         self.LogWarning("Can't assemble newly created disk %d: %s",
@@ -6921,7 +7796,7 @@ class LUInstanceMove(LogicalUnit):
         errs.append(result.fail_msg)
         break
       dev_path = result.payload
-      result = self.rpc.call_blockdev_export(source_node, disk,
+      result = self.rpc.call_blockdev_export(source_node, (disk, instance),
                                              target_node, dev_path,
                                              cluster_name)
       if result.fail_msg:
@@ -6946,7 +7821,7 @@ class LUInstanceMove(LogicalUnit):
     _RemoveDisks(self, instance, target_node=source_node)
 
     # Only start the instance if it's marked as up
-    if instance.admin_up:
+    if instance.admin_state == constants.ADMINST_UP:
       self.LogInfo("Starting instance %s on node %s",
                    instance.name, target_node)
 
@@ -6956,8 +7831,8 @@ class LUInstanceMove(LogicalUnit):
         _ShutdownInstanceDisks(self, instance)
         raise errors.OpExecError("Can't activate the instance's disks")
 
-      result = self.rpc.call_instance_start(target_node, instance,
-                                            None, None, False)
+      result = self.rpc.call_instance_start(target_node,
+                                            (instance, None, None), False)
       msg = result.fail_msg
       if msg:
         _ShutdownInstanceDisks(self, instance)
@@ -6992,6 +7867,7 @@ class LUNodeMigrate(LogicalUnit):
     """
     return {
       "NODE_NAME": self.op.node_name,
+      "ALLOW_RUNTIME_CHANGES": self.op.allow_runtime_changes,
       }
 
   def BuildHooksNodes(self):
@@ -7006,12 +7882,15 @@ class LUNodeMigrate(LogicalUnit):
 
   def Exec(self, feedback_fn):
     # Prepare jobs for migration instances
+    allow_runtime_changes = self.op.allow_runtime_changes
     jobs = [
       [opcodes.OpInstanceMigrate(instance_name=inst.name,
                                  mode=self.op.mode,
                                  live=self.op.live,
                                  iallocator=self.op.iallocator,
-                                 target_node=self.op.target_node)]
+                                 target_node=self.op.target_node,
+                                 allow_runtime_changes=allow_runtime_changes,
+                                 ignore_ipolicy=self.op.ignore_ipolicy)]
       for inst in _GetNodePrimaryInstances(self.cfg, self.op.node_name)
       ]
 
@@ -7048,12 +7927,21 @@ class TLMigrateInstance(Tasklet):
                             and target node
   @type shutdown_timeout: int
   @ivar shutdown_timeout: In case of failover timeout of the shutdown
+  @type ignore_ipolicy: bool
+  @ivar ignore_ipolicy: If true, we can ignore instance policy when migrating
 
   """
+
+  # Constants
+  _MIGRATION_POLL_INTERVAL = 1      # seconds
+  _MIGRATION_FEEDBACK_INTERVAL = 10 # seconds
+
   def __init__(self, lu, instance_name, cleanup=False,
                failover=False, fallback=False,
                ignore_consistency=False,
-               shutdown_timeout=constants.DEFAULT_SHUTDOWN_TIMEOUT):
+               allow_runtime_changes=True,
+               shutdown_timeout=constants.DEFAULT_SHUTDOWN_TIMEOUT,
+               ignore_ipolicy=False):
     """Initializes this class.
 
     """
@@ -7067,6 +7955,8 @@ class TLMigrateInstance(Tasklet):
     self.fallback = fallback
     self.ignore_consistency = ignore_consistency
     self.shutdown_timeout = shutdown_timeout
+    self.ignore_ipolicy = ignore_ipolicy
+    self.allow_runtime_changes = allow_runtime_changes
 
   def CheckPrereq(self):
     """Check prerequisites.
@@ -7078,11 +7968,13 @@ class TLMigrateInstance(Tasklet):
     instance = self.cfg.GetInstanceInfo(instance_name)
     assert instance is not None
     self.instance = instance
+    cluster = self.cfg.GetClusterInfo()
 
-    if (not self.cleanup and not instance.admin_up and not self.failover and
-        self.fallback):
-      self.lu.LogInfo("Instance is marked down, fallback allowed, switching"
-                      " to failover")
+    if (not self.cleanup and
+        not instance.admin_state == constants.ADMINST_UP and
+        not self.failover and self.fallback):
+      self.lu.LogInfo("Instance is marked down or offline, fallback allowed,"
+                      " switching to failover")
       self.failover = True
 
     if instance.disk_template not in constants.DTS_MIRRORED:
@@ -7104,6 +7996,13 @@ class TLMigrateInstance(Tasklet):
         # BuildHooksEnv
         self.target_node = self.lu.op.target_node
 
+      # Check that the target node is correct in terms of instance policy
+      nodeinfo = self.cfg.GetNodeInfo(self.target_node)
+      group_info = self.cfg.GetNodeGroup(nodeinfo.group)
+      ipolicy = _CalculateGroupIPolicy(cluster, group_info)
+      _CheckTargetNodeIPolicy(self.lu, ipolicy, instance, nodeinfo,
+                              ignore=self.ignore_ipolicy)
+
       # self.target_node is already populated, either directly or by the
       # iallocator run
       target_node = self.target_node
@@ -7137,18 +8036,38 @@ class TLMigrateInstance(Tasklet):
                                    " node can be passed)" %
                                    (instance.disk_template, text),
                                    errors.ECODE_INVAL)
+      nodeinfo = self.cfg.GetNodeInfo(target_node)
+      group_info = self.cfg.GetNodeGroup(nodeinfo.group)
+      ipolicy = _CalculateGroupIPolicy(cluster, group_info)
+      _CheckTargetNodeIPolicy(self.lu, ipolicy, instance, nodeinfo,
+                              ignore=self.ignore_ipolicy)
 
-    i_be = self.cfg.GetClusterInfo().FillBE(instance)
+    i_be = cluster.FillBE(instance)
 
     # check memory requirements on the secondary node
-    if not self.cleanup and (not self.failover or instance.admin_up):
-      _CheckNodeFreeMemory(self.lu, target_node, "migrating instance %s" %
-                           instance.name, i_be[constants.BE_MEMORY],
-                           instance.hypervisor)
+    if (not self.cleanup and
+         (not self.failover or instance.admin_state == constants.ADMINST_UP)):
+      self.tgt_free_mem = _CheckNodeFreeMemory(self.lu, target_node,
+                                               "migrating instance %s" %
+                                               instance.name,
+                                               i_be[constants.BE_MINMEM],
+                                               instance.hypervisor)
     else:
       self.lu.LogInfo("Not checking memory on the secondary node as"
                       " instance will not be started")
 
+    # check if failover must be forced instead of migration
+    if (not self.cleanup and not self.failover and
+        i_be[constants.BE_ALWAYS_FAILOVER]):
+      if self.fallback:
+        self.lu.LogInfo("Instance configured to always failover; fallback"
+                        " to failover")
+        self.failover = True
+      else:
+        raise errors.OpPrereqError("This instance has been configured to"
+                                   " always failover, please allow failover",
+                                   errors.ECODE_STATE)
+
     # check bridge existance
     _CheckInstanceBridgesExist(self.lu, instance, node=target_node)
 
@@ -7182,8 +8101,7 @@ class TLMigrateInstance(Tasklet):
         self.lu.op.live = None
       elif self.lu.op.mode is None:
         # read the default value from the hypervisor
-        i_hv = self.cfg.GetClusterInfo().FillHV(self.instance,
-                                                skip_globals=False)
+        i_hv = cluster.FillHV(self.instance, skip_globals=False)
         self.lu.op.mode = i_hv[constants.HV_MIGRATION_MODE]
 
       self.live = self.lu.op.mode == constants.HT_MIGRATION_LIVE
@@ -7191,16 +8109,25 @@ class TLMigrateInstance(Tasklet):
       # Failover is never live
       self.live = False
 
+    if not (self.failover or self.cleanup):
+      remote_info = self.rpc.call_instance_info(instance.primary_node,
+                                                instance.name,
+                                                instance.hypervisor)
+      remote_info.Raise("Error checking instance on node %s" %
+                        instance.primary_node)
+      instance_running = bool(remote_info.payload)
+      if instance_running:
+        self.current_mem = int(remote_info.payload["memory"])
+
   def _RunAllocator(self):
     """Run the allocator based on input opcode.
 
     """
+    # FIXME: add a self.ignore_ipolicy option
     ial = IAllocator(self.cfg, self.rpc,
                      mode=constants.IALLOCATOR_MODE_RELOC,
                      name=self.instance_name,
-                     # TODO See why hail breaks with a single node below
-                     relocate_from=[self.instance.primary_node,
-                                    self.instance.primary_node],
+                     relocate_from=[self.instance.primary_node],
                      )
 
     ial.Run(self.lu.op.iallocator)
@@ -7232,7 +8159,8 @@ class TLMigrateInstance(Tasklet):
       all_done = True
       result = self.rpc.call_drbd_wait_sync(self.all_nodes,
                                             self.nodes_ip,
-                                            self.instance.disks)
+                                            (self.instance.disks,
+                                             self.instance))
       min_percent = 100
       for node, nres in result.items():
         nres.Raise("Cannot resync disks on node %s" % node)
@@ -7278,7 +8206,7 @@ class TLMigrateInstance(Tasklet):
       msg = "single-master"
     self.feedback_fn("* changing disks into %s mode" % msg)
     result = self.rpc.call_drbd_attach_net(self.all_nodes, self.nodes_ip,
-                                           self.instance.disks,
+                                           (self.instance.disks, self.instance),
                                            self.instance.name, multimaster)
     for node, nres in result.items():
       nres.Raise("Cannot change disks config on node %s" % node)
@@ -7373,12 +8301,13 @@ class TLMigrateInstance(Tasklet):
     """
     instance = self.instance
     target_node = self.target_node
+    source_node = self.source_node
     migration_info = self.migration_info
 
-    abort_result = self.rpc.call_finalize_migration(target_node,
-                                                    instance,
-                                                    migration_info,
-                                                    False)
+    abort_result = self.rpc.call_instance_finalize_migration_dst(target_node,
+                                                                 instance,
+                                                                 migration_info,
+                                                                 False)
     abort_msg = abort_result.fail_msg
     if abort_msg:
       logging.error("Aborting migration failed on target node %s: %s",
@@ -7386,6 +8315,13 @@ class TLMigrateInstance(Tasklet):
       # Don't raise an exception here, as we stil have to try to revert the
       # disk status, even if this step failed.
 
+    abort_result = self.rpc.call_instance_finalize_migration_src(source_node,
+        instance, False, self.live)
+    abort_msg = abort_result.fail_msg
+    if abort_msg:
+      logging.error("Aborting migration failed on source node %s: %s",
+                    source_node, abort_msg)
+
   def _ExecMigration(self):
     """Migrate an instance.
 
@@ -7402,12 +8338,43 @@ class TLMigrateInstance(Tasklet):
     target_node = self.target_node
     source_node = self.source_node
 
+    # Check for hypervisor version mismatch and warn the user.
+    nodeinfo = self.rpc.call_node_info([source_node, target_node],
+                                       None, [self.instance.hypervisor])
+    for ninfo in nodeinfo.values():
+      ninfo.Raise("Unable to retrieve node information from node '%s'" %
+                  ninfo.node)
+    (_, _, (src_info, )) = nodeinfo[source_node].payload
+    (_, _, (dst_info, )) = nodeinfo[target_node].payload
+
+    if ((constants.HV_NODEINFO_KEY_VERSION in src_info) and
+        (constants.HV_NODEINFO_KEY_VERSION in dst_info)):
+      src_version = src_info[constants.HV_NODEINFO_KEY_VERSION]
+      dst_version = dst_info[constants.HV_NODEINFO_KEY_VERSION]
+      if src_version != dst_version:
+        self.feedback_fn("* warning: hypervisor version mismatch between"
+                         " source (%s) and target (%s) node" %
+                         (src_version, dst_version))
+
     self.feedback_fn("* checking disk consistency between source and target")
-    for dev in instance.disks:
-      if not _CheckDiskConsistency(self.lu, dev, target_node, False):
+    for (idx, dev) in enumerate(instance.disks):
+      if not _CheckDiskConsistency(self.lu, instance, dev, target_node, False):
         raise errors.OpExecError("Disk %s is degraded or not fully"
                                  " synchronized on target node,"
-                                 " aborting migration" % dev.iv_name)
+                                 " aborting migration" % idx)
+
+    if self.current_mem > self.tgt_free_mem:
+      if not self.allow_runtime_changes:
+        raise errors.OpExecError("Memory ballooning not allowed and not enough"
+                                 " free memory to fit instance %s on target"
+                                 " node %s (have %dMB, need %dMB)" %
+                                 (instance.name, target_node,
+                                  self.tgt_free_mem, self.current_mem))
+      self.feedback_fn("* setting instance memory to %s" % self.tgt_free_mem)
+      rpcres = self.rpc.call_instance_balloon_memory(instance.primary_node,
+                                                     instance,
+                                                     self.tgt_free_mem)
+      rpcres.Raise("Cannot modify instance runtime memory")
 
     # First get the migration information from the remote node
     result = self.rpc.call_migration_info(source_node, instance)
@@ -7457,18 +8424,59 @@ class TLMigrateInstance(Tasklet):
       raise errors.OpExecError("Could not migrate instance %s: %s" %
                                (instance.name, msg))
 
+    self.feedback_fn("* starting memory transfer")
+    last_feedback = time.time()
+    while True:
+      result = self.rpc.call_instance_get_migration_status(source_node,
+                                                           instance)
+      msg = result.fail_msg
+      ms = result.payload   # MigrationStatus instance
+      if msg or (ms.status in constants.HV_MIGRATION_FAILED_STATUSES):
+        logging.error("Instance migration failed, trying to revert"
+                      " disk status: %s", msg)
+        self.feedback_fn("Migration failed, aborting")
+        self._AbortMigration()
+        self._RevertDiskStatus()
+        raise errors.OpExecError("Could not migrate instance %s: %s" %
+                                 (instance.name, msg))
+
+      if result.payload.status != constants.HV_MIGRATION_ACTIVE:
+        self.feedback_fn("* memory transfer complete")
+        break
+
+      if (utils.TimeoutExpired(last_feedback,
+                               self._MIGRATION_FEEDBACK_INTERVAL) and
+          ms.transferred_ram is not None):
+        mem_progress = 100 * float(ms.transferred_ram) / float(ms.total_ram)
+        self.feedback_fn("* memory transfer progress: %.2f %%" % mem_progress)
+        last_feedback = time.time()
+
+      time.sleep(self._MIGRATION_POLL_INTERVAL)
+
+    result = self.rpc.call_instance_finalize_migration_src(source_node,
+                                                           instance,
+                                                           True,
+                                                           self.live)
+    msg = result.fail_msg
+    if msg:
+      logging.error("Instance migration succeeded, but finalization failed"
+                    " on the source node: %s", msg)
+      raise errors.OpExecError("Could not finalize instance migration: %s" %
+                               msg)
+
     instance.primary_node = target_node
+
     # distribute new instance config to the other nodes
     self.cfg.Update(instance, self.feedback_fn)
 
-    result = self.rpc.call_finalize_migration(target_node,
-                                              instance,
-                                              migration_info,
-                                              True)
+    result = self.rpc.call_instance_finalize_migration_dst(target_node,
+                                                           instance,
+                                                           migration_info,
+                                                           True)
     msg = result.fail_msg
     if msg:
-      logging.error("Instance migration succeeded, but finalization failed:"
-                    " %s", msg)
+      logging.error("Instance migration succeeded, but finalization failed"
+                    " on the target node: %s", msg)
       raise errors.OpExecError("Could not finalize instance migration: %s" %
                                msg)
 
@@ -7479,6 +8487,21 @@ class TLMigrateInstance(Tasklet):
       self._GoReconnect(False)
       self._WaitUntilSync()
 
+    # If the instance's disk template is `rbd' and there was a successful
+    # migration, unmap the device from the source node.
+    if self.instance.disk_template == constants.DT_RBD:
+      disks = _ExpandCheckDisks(instance, instance.disks)
+      self.feedback_fn("* unmapping instance's disks from %s" % source_node)
+      for disk in disks:
+        result = self.rpc.call_blockdev_shutdown(source_node, (disk, instance))
+        msg = result.fail_msg
+        if msg:
+          logging.error("Migration was successful, but couldn't unmap the"
+                        " block device %s on source node %s: %s",
+                        disk.iv_name, source_node, msg)
+          logging.error("You need to unmap the device %s manually on %s",
+                        disk.iv_name, source_node)
+
     self.feedback_fn("* done")
 
   def _ExecFailover(self):
@@ -7494,18 +8517,19 @@ class TLMigrateInstance(Tasklet):
     source_node = instance.primary_node
     target_node = self.target_node
 
-    if instance.admin_up:
+    if instance.admin_state == constants.ADMINST_UP:
       self.feedback_fn("* checking disk consistency between source and target")
-      for dev in instance.disks:
+      for (idx, dev) in enumerate(instance.disks):
         # for drbd, these are drbd over lvm
-        if not _CheckDiskConsistency(self.lu, dev, target_node, False):
+        if not _CheckDiskConsistency(self.lu, instance, dev, target_node,
+                                     False):
           if primary_node.offline:
             self.feedback_fn("Node %s is offline, ignoring degraded disk %s on"
                              " target node %s" %
-                             (primary_node.name, dev.iv_name, target_node))
+                             (primary_node.name, idx, target_node))
           elif not self.ignore_consistency:
             raise errors.OpExecError("Disk %s is degraded on target node,"
-                                     " aborting failover" % dev.iv_name)
+                                     " aborting failover" % idx)
     else:
       self.feedback_fn("* not checking disk consistency as instance is not"
                        " running")
@@ -7537,7 +8561,7 @@ class TLMigrateInstance(Tasklet):
     self.cfg.Update(instance, self.feedback_fn)
 
     # Only start the instance if it's marked as up
-    if instance.admin_up:
+    if instance.admin_state == constants.ADMINST_UP:
       self.feedback_fn("* activating the instance's disks on target node %s" %
                        target_node)
       logging.info("Starting instance %s on node %s",
@@ -7551,7 +8575,7 @@ class TLMigrateInstance(Tasklet):
 
       self.feedback_fn("* starting the instance on the target node %s" %
                        target_node)
-      result = self.rpc.call_instance_start(target_node, instance, None, None,
+      result = self.rpc.call_instance_start(target_node, (instance, None, None),
                                             False)
       msg = result.fail_msg
       if msg:
@@ -7588,8 +8612,20 @@ class TLMigrateInstance(Tasklet):
         return self._ExecMigration()
 
 
-def _CreateBlockDev(lu, node, instance, device, force_create,
-                    info, force_open):
+def _CreateBlockDev(lu, node, instance, device, force_create, info,
+                    force_open):
+  """Wrapper around L{_CreateBlockDevInner}.
+
+  This method annotates the root device first.
+
+  """
+  (disk,) = _AnnotateDiskParams(instance, [device], lu.cfg)
+  return _CreateBlockDevInner(lu, node, instance, disk, force_create, info,
+                              force_open)
+
+
+def _CreateBlockDevInner(lu, node, instance, device, force_create,
+                         info, force_open):
   """Create a tree of block devices on a given node.
 
   If this device type has to be created on secondaries, create it and
@@ -7597,6 +8633,8 @@ def _CreateBlockDev(lu, node, instance, device, force_create,
 
   If not, just recurse to children keeping the same 'force' value.
 
+  @attention: The device has to be annotated already.
+
   @param lu: the lu on whose behalf we execute
   @param node: the node on which to create the device
   @type instance: L{objects.Instance}
@@ -7621,8 +8659,8 @@ def _CreateBlockDev(lu, node, instance, device, force_create,
 
   if device.children:
     for child in device.children:
-      _CreateBlockDev(lu, node, instance, child, force_create,
-                      info, force_open)
+      _CreateBlockDevInner(lu, node, instance, child, force_create,
+                           info, force_open)
 
   if not force_create:
     return
@@ -7681,24 +8719,41 @@ def _GenerateDRBD8Branch(lu, primary, secondary, size, vgnames, names,
   assert len(vgnames) == len(names) == 2
   port = lu.cfg.AllocatePort()
   shared_secret = lu.cfg.GenerateDRBDSecret(lu.proc.GetECId())
+
   dev_data = objects.Disk(dev_type=constants.LD_LV, size=size,
-                          logical_id=(vgnames[0], names[0]))
-  dev_meta = objects.Disk(dev_type=constants.LD_LV, size=128,
-                          logical_id=(vgnames[1], names[1]))
+                          logical_id=(vgnames[0], names[0]),
+                          params={})
+  dev_meta = objects.Disk(dev_type=constants.LD_LV, size=DRBD_META_SIZE,
+                          logical_id=(vgnames[1], names[1]),
+                          params={})
   drbd_dev = objects.Disk(dev_type=constants.LD_DRBD8, size=size,
                           logical_id=(primary, secondary, port,
                                       p_minor, s_minor,
                                       shared_secret),
                           children=[dev_data, dev_meta],
-                          iv_name=iv_name)
+                          iv_name=iv_name, params={})
   return drbd_dev
 
 
-def _GenerateDiskTemplate(lu, template_name,
-                          instance_name, primary_node,
-                          secondary_nodes, disk_info,
-                          file_storage_dir, file_driver,
-                          base_index, feedback_fn):
+_DISK_TEMPLATE_NAME_PREFIX = {
+  constants.DT_PLAIN: "",
+  constants.DT_RBD: ".rbd",
+  }
+
+
+_DISK_TEMPLATE_DEVICE_TYPE = {
+  constants.DT_PLAIN: constants.LD_LV,
+  constants.DT_FILE: constants.LD_FILE,
+  constants.DT_SHARED_FILE: constants.LD_FILE,
+  constants.DT_BLOCK: constants.LD_BLOCKDEV,
+  constants.DT_RBD: constants.LD_RBD,
+  }
+
+
+def _GenerateDiskTemplate(lu, template_name, instance_name, primary_node,
+    secondary_nodes, disk_info, file_storage_dir, file_driver, base_index,
+    feedback_fn, full_disk_params, _req_file_storage=opcodes.RequireFileStorage,
+    _req_shr_file_storage=opcodes.RequireSharedFileStorage):
   """Generate the entire disk layout for a given template type.
 
   """
@@ -7707,24 +8762,9 @@ def _GenerateDiskTemplate(lu, template_name,
   vgname = lu.cfg.GetVGName()
   disk_count = len(disk_info)
   disks = []
+
   if template_name == constants.DT_DISKLESS:
     pass
-  elif template_name == constants.DT_PLAIN:
-    if len(secondary_nodes) != 0:
-      raise errors.ProgrammerError("Wrong template configuration")
-
-    names = _GenerateUniqueNames(lu, [".disk%d" % (base_index + i)
-                                      for i in range(disk_count)])
-    for idx, disk in enumerate(disk_info):
-      disk_index = idx + base_index
-      vg = disk.get(constants.IDISK_VG, vgname)
-      feedback_fn("* disk %i, vg %s, name %s" % (idx, vg, names[idx]))
-      disk_dev = objects.Disk(dev_type=constants.LD_LV,
-                              size=disk[constants.IDISK_SIZE],
-                              logical_id=(vg, names[idx]),
-                              iv_name="disk/%d" % disk_index,
-                              mode=disk[constants.IDISK_MODE])
-      disks.append(disk_dev)
   elif template_name == constants.DT_DRBD8:
     if len(secondary_nodes) != 1:
       raise errors.ProgrammerError("Wrong template configuration")
@@ -7732,6 +8772,10 @@ def _GenerateDiskTemplate(lu, template_name,
     minors = lu.cfg.AllocateDRBDMinor(
       [primary_node, remote_node] * len(disk_info), instance_name)
 
+    (drbd_params, _, _) = objects.Disk.ComputeLDParams(template_name,
+                                                       full_disk_params)
+    drbd_default_metavg = drbd_params[constants.LDP_DEFAULT_METAVG]
+
     names = []
     for lv_prefix in _GenerateUniqueNames(lu, [".disk%d" % (base_index + i)
                                                for i in range(disk_count)]):
@@ -7740,7 +8784,7 @@ def _GenerateDiskTemplate(lu, template_name,
     for idx, disk in enumerate(disk_info):
       disk_index = idx + base_index
       data_vg = disk.get(constants.IDISK_VG, vgname)
-      meta_vg = disk.get(constants.IDISK_METAVG, data_vg)
+      meta_vg = disk.get(constants.IDISK_METAVG, drbd_default_metavg)
       disk_dev = _GenerateDRBD8Branch(lu, primary_node, remote_node,
                                       disk[constants.IDISK_SIZE],
                                       [data_vg, meta_vg],
@@ -7749,54 +8793,54 @@ def _GenerateDiskTemplate(lu, template_name,
                                       minors[idx * 2], minors[idx * 2 + 1])
       disk_dev.mode = disk[constants.IDISK_MODE]
       disks.append(disk_dev)
-  elif template_name == constants.DT_FILE:
-    if len(secondary_nodes) != 0:
+  else:
+    if secondary_nodes:
       raise errors.ProgrammerError("Wrong template configuration")
 
-    opcodes.RequireFileStorage()
-
-    for idx, disk in enumerate(disk_info):
-      disk_index = idx + base_index
-      disk_dev = objects.Disk(dev_type=constants.LD_FILE,
-                              size=disk[constants.IDISK_SIZE],
-                              iv_name="disk/%d" % disk_index,
-                              logical_id=(file_driver,
-                                          "%s/disk%d" % (file_storage_dir,
-                                                         disk_index)),
-                              mode=disk[constants.IDISK_MODE])
-      disks.append(disk_dev)
-  elif template_name == constants.DT_SHARED_FILE:
-    if len(secondary_nodes) != 0:
-      raise errors.ProgrammerError("Wrong template configuration")
+    if template_name == constants.DT_FILE:
+      _req_file_storage()
+    elif template_name == constants.DT_SHARED_FILE:
+      _req_shr_file_storage()
 
-    opcodes.RequireSharedFileStorage()
+    name_prefix = _DISK_TEMPLATE_NAME_PREFIX.get(template_name, None)
+    if name_prefix is None:
+      names = None
+    else:
+      names = _GenerateUniqueNames(lu, ["%s.disk%s" %
+                                        (name_prefix, base_index + i)
+                                        for i in range(disk_count)])
+
+    if template_name == constants.DT_PLAIN:
+      def logical_id_fn(idx, _, disk):
+        vg = disk.get(constants.IDISK_VG, vgname)
+        return (vg, names[idx])
+    elif template_name in (constants.DT_FILE, constants.DT_SHARED_FILE):
+      logical_id_fn = \
+        lambda _, disk_index, disk: (file_driver,
+                                     "%s/disk%d" % (file_storage_dir,
+                                                    disk_index))
+    elif template_name == constants.DT_BLOCK:
+      logical_id_fn = \
+        lambda idx, disk_index, disk: (constants.BLOCKDEV_DRIVER_MANUAL,
+                                       disk[constants.IDISK_ADOPT])
+    elif template_name == constants.DT_RBD:
+      logical_id_fn = lambda idx, _, disk: ("rbd", names[idx])
+    else:
+      raise errors.ProgrammerError("Unknown disk template '%s'" % template_name)
 
-    for idx, disk in enumerate(disk_info):
-      disk_index = idx + base_index
-      disk_dev = objects.Disk(dev_type=constants.LD_FILE,
-                              size=disk[constants.IDISK_SIZE],
-                              iv_name="disk/%d" % disk_index,
-                              logical_id=(file_driver,
-                                          "%s/disk%d" % (file_storage_dir,
-                                                         disk_index)),
-                              mode=disk[constants.IDISK_MODE])
-      disks.append(disk_dev)
-  elif template_name == constants.DT_BLOCK:
-    if len(secondary_nodes) != 0:
-      raise errors.ProgrammerError("Wrong template configuration")
+    dev_type = _DISK_TEMPLATE_DEVICE_TYPE[template_name]
 
     for idx, disk in enumerate(disk_info):
       disk_index = idx + base_index
-      disk_dev = objects.Disk(dev_type=constants.LD_BLOCKDEV,
-                              size=disk[constants.IDISK_SIZE],
-                              logical_id=(constants.BLOCKDEV_DRIVER_MANUAL,
-                                          disk[constants.IDISK_ADOPT]),
-                              iv_name="disk/%d" % disk_index,
-                              mode=disk[constants.IDISK_MODE])
-      disks.append(disk_dev)
+      size = disk[constants.IDISK_SIZE]
+      feedback_fn("* disk %s, size %s" %
+                  (disk_index, utils.FormatUnit(size, "h")))
+      disks.append(objects.Disk(dev_type=dev_type, size=size,
+                                logical_id=logical_id_fn(idx, disk_index, disk),
+                                iv_name="disk/%d" % disk_index,
+                                mode=disk[constants.IDISK_MODE],
+                                params={}))
 
-  else:
-    raise errors.ProgrammerError("Invalid disk template '%s'" % template_name)
   return disks
 
 
@@ -7836,7 +8880,9 @@ def _WipeDisks(lu, instance):
     lu.cfg.SetDiskID(device, node)
 
   logging.info("Pause sync of instance %s disks", instance.name)
-  result = lu.rpc.call_blockdev_pause_resume_sync(node, instance.disks, True)
+  result = lu.rpc.call_blockdev_pause_resume_sync(node,
+                                                  (instance.disks, instance),
+                                                  True)
 
   for idx, success in enumerate(result.payload):
     if not success:
@@ -7866,7 +8912,8 @@ def _WipeDisks(lu, instance):
         wipe_size = min(wipe_chunk_size, size - offset)
         logging.debug("Wiping disk %d, offset %s, chunk %s",
                       idx, offset, wipe_size)
-        result = lu.rpc.call_blockdev_wipe(node, device, offset, wipe_size)
+        result = lu.rpc.call_blockdev_wipe(node, (device, instance), offset,
+                                           wipe_size)
         result.Raise("Could not wipe disk %d at offset %d for size %d" %
                      (idx, offset, wipe_size))
         now = time.time()
@@ -7879,7 +8926,9 @@ def _WipeDisks(lu, instance):
   finally:
     logging.info("Resume sync of instance %s disks", instance.name)
 
-    result = lu.rpc.call_blockdev_pause_resume_sync(node, instance.disks, False)
+    result = lu.rpc.call_blockdev_pause_resume_sync(node,
+                                                    (instance.disks, instance),
+                                                    False)
 
     for idx, success in enumerate(result.payload):
       if not success:
@@ -7926,8 +8975,7 @@ def _CreateDisks(lu, instance, to_skip=None, target_node=None):
   for idx, device in enumerate(instance.disks):
     if to_skip and idx in to_skip:
       continue
-    logging.info("Creating volume %s for instance %s",
-                 device.iv_name, instance.name)
+    logging.info("Creating disk %s for instance '%s'", idx, instance.name)
     #HARDCODE
     for node in all_nodes:
       f_create = node == pnode
@@ -7956,18 +9004,20 @@ def _RemoveDisks(lu, instance, target_node=None, ignore_failures=False):
 
   all_result = True
   ports_to_release = set()
-  for device in instance.disks:
+  anno_disks = _AnnotateDiskParams(instance, instance.disks, lu.cfg)
+  for (idx, device) in enumerate(anno_disks):
     if target_node:
       edata = [(target_node, device)]
     else:
       edata = device.ComputeNodeTree(instance.primary_node)
     for node, disk in edata:
       lu.cfg.SetDiskID(disk, node)
-      msg = lu.rpc.call_blockdev_remove(node, disk).fail_msg
-      if msg:
-        lu.LogWarning("Could not remove block device %s on node %s,"
-                      " continuing anyway: %s", device.iv_name, node, msg)
-        all_result = False
+      result = lu.rpc.call_blockdev_remove(node, disk)
+      if result.fail_msg:
+        lu.LogWarning("Could not remove disk %s on node %s,"
+                      " continuing anyway: %s", idx, node, result.fail_msg)
+        if not (result.offline and node != instance.primary_node):
+          all_result = False
 
     # if this is a DRBD disk, return its port to the pool
     if device.dev_type in constants.LDS_DRBD:
@@ -8012,7 +9062,7 @@ def _ComputeDiskSizePerVG(disk_template, disks):
     constants.DT_DISKLESS: {},
     constants.DT_PLAIN: _compute(disks, 0),
     # 128 MB are added for drbd metadata for each disk
-    constants.DT_DRBD8: _compute(disks, 128),
+    constants.DT_DRBD8: _compute(disks, DRBD_META_SIZE),
     constants.DT_FILE: {},
     constants.DT_SHARED_FILE: {},
   }
@@ -8033,10 +9083,12 @@ def _ComputeDiskSize(disk_template, disks):
     constants.DT_DISKLESS: None,
     constants.DT_PLAIN: sum(d[constants.IDISK_SIZE] for d in disks),
     # 128 MB are added for drbd metadata for each disk
-    constants.DT_DRBD8: sum(d[constants.IDISK_SIZE] + 128 for d in disks),
+    constants.DT_DRBD8:
+      sum(d[constants.IDISK_SIZE] + DRBD_META_SIZE for d in disks),
     constants.DT_FILE: None,
     constants.DT_SHARED_FILE: 0,
     constants.DT_BLOCK: 0,
+    constants.DT_RBD: 0,
   }
 
   if disk_template not in req_size_dict:
@@ -8079,9 +9131,11 @@ def _CheckHVParams(lu, nodenames, hvname, hvparams):
 
   """
   nodenames = _FilterVmNodes(lu, nodenames)
-  hvinfo = lu.rpc.call_hypervisor_validate_params(nodenames,
-                                                  hvname,
-                                                  hvparams)
+
+  cluster = lu.cfg.GetClusterInfo()
+  hvfull = objects.FillDict(cluster.hvparams.get(hvname, {}), hvparams)
+
+  hvinfo = lu.rpc.call_hypervisor_validate_params(nodenames, hvname, hvfull)
   for node in nodenames:
     info = hvinfo[node]
     if info.offline:
@@ -8107,7 +9161,7 @@ def _CheckOSParams(lu, required, nodenames, osname, osparams):
 
   """
   nodenames = _FilterVmNodes(lu, nodenames)
-  result = lu.rpc.call_os_validate(required, nodenames, osname,
+  result = lu.rpc.call_os_validate(nodenames, required, osname,
                                    [constants.OS_VALIDATE_PARAMETERS],
                                    osparams)
   for node, nres in result.items():
@@ -8300,7 +9354,11 @@ class LUInstanceCreate(LogicalUnit):
     self.add_locks[locking.LEVEL_INSTANCE] = instance_name
 
     if self.op.iallocator:
+      # TODO: Find a solution to not lock all nodes in the cluster, e.g. by
+      # specifying a group on instance creation and then selecting nodes from
+      # that group
       self.needed_locks[locking.LEVEL_NODE] = locking.ALL_SET
+      self.needed_locks[locking.LEVEL_NODE_RES] = locking.ALL_SET
     else:
       self.op.pnode = _ExpandNodeName(self.cfg, self.op.pnode)
       nodelist = [self.op.pnode]
@@ -8308,6 +9366,9 @@ class LUInstanceCreate(LogicalUnit):
         self.op.snode = _ExpandNodeName(self.cfg, self.op.snode)
         nodelist.append(self.op.snode)
       self.needed_locks[locking.LEVEL_NODE] = nodelist
+      # Lock resources of instance's primary and secondary nodes (copy to
+      # prevent accidential modification)
+      self.needed_locks[locking.LEVEL_NODE_RES] = list(nodelist)
 
     # in case of import lock the source node too
     if self.op.mode == constants.INSTANCE_IMPORT:
@@ -8344,7 +9405,8 @@ class LUInstanceCreate(LogicalUnit):
                      tags=self.op.tags,
                      os=self.op.os_type,
                      vcpus=self.be_full[constants.BE_VCPUS],
-                     memory=self.be_full[constants.BE_MEMORY],
+                     memory=self.be_full[constants.BE_MAXMEM],
+                     spindle_use=self.be_full[constants.BE_SPINDLE_USE],
                      disks=self.disks,
                      nics=nics,
                      hypervisor=self.op.hypervisor,
@@ -8389,7 +9451,8 @@ class LUInstanceCreate(LogicalUnit):
       secondary_nodes=self.secondaries,
       status=self.op.start,
       os_type=self.op.os_type,
-      memory=self.be_full[constants.BE_MEMORY],
+      minmem=self.be_full[constants.BE_MINMEM],
+      maxmem=self.be_full[constants.BE_MAXMEM],
       vcpus=self.be_full[constants.BE_VCPUS],
       nics=_NICListToTuple(self, self.nics),
       disk_template=self.op.disk_template,
@@ -8471,33 +9534,39 @@ class LUInstanceCreate(LogicalUnit):
       if einfo.has_option(constants.INISECT_INS, "disk_template"):
         self.op.disk_template = einfo.get(constants.INISECT_INS,
                                           "disk_template")
+        if self.op.disk_template not in constants.DISK_TEMPLATES:
+          raise errors.OpPrereqError("Disk template specified in configuration"
+                                     " file is not one of the allowed values:"
+                                     " %s" % " ".join(constants.DISK_TEMPLATES))
       else:
         raise errors.OpPrereqError("No disk template specified and the export"
                                    " is missing the disk_template information",
                                    errors.ECODE_INVAL)
 
     if not self.op.disks:
-      if einfo.has_option(constants.INISECT_INS, "disk_count"):
-        disks = []
-        # TODO: import the disk iv_name too
-        for idx in range(einfo.getint(constants.INISECT_INS, "disk_count")):
+      disks = []
+      # TODO: import the disk iv_name too
+      for idx in range(constants.MAX_DISKS):
+        if einfo.has_option(constants.INISECT_INS, "disk%d_size" % idx):
           disk_sz = einfo.getint(constants.INISECT_INS, "disk%d_size" % idx)
           disks.append({constants.IDISK_SIZE: disk_sz})
-        self.op.disks = disks
-      else:
+      self.op.disks = disks
+      if not disks and self.op.disk_template != constants.DT_DISKLESS:
         raise errors.OpPrereqError("No disk info specified and the export"
                                    " is missing the disk information",
                                    errors.ECODE_INVAL)
 
-    if (not self.op.nics and
-        einfo.has_option(constants.INISECT_INS, "nic_count")):
+    if not self.op.nics:
       nics = []
-      for idx in range(einfo.getint(constants.INISECT_INS, "nic_count")):
-        ndict = {}
-        for name in list(constants.NICS_PARAMETERS) + ["ip", "mac"]:
-          v = einfo.get(constants.INISECT_INS, "nic%d_%s" % (idx, name))
-          ndict[name] = v
-        nics.append(ndict)
+      for idx in range(constants.MAX_NICS):
+        if einfo.has_option(constants.INISECT_INS, "nic%d_mac" % idx):
+          ndict = {}
+          for name in list(constants.NICS_PARAMETERS) + ["ip", "mac"]:
+            v = einfo.get(constants.INISECT_INS, "nic%d_%s" % (idx, name))
+            ndict[name] = v
+          nics.append(ndict)
+        else:
+          break
       self.op.nics = nics
 
     if not self.op.tags and einfo.has_option(constants.INISECT_INS, "tags"):
@@ -8519,6 +9588,12 @@ class LUInstanceCreate(LogicalUnit):
       for name, value in einfo.items(constants.INISECT_BEP):
         if name not in self.op.beparams:
           self.op.beparams[name] = value
+        # Compatibility for the old "memory" be param
+        if name == constants.BE_MEMORY:
+          if constants.BE_MAXMEM not in self.op.beparams:
+            self.op.beparams[constants.BE_MAXMEM] = value
+          if constants.BE_MINMEM not in self.op.beparams:
+            self.op.beparams[constants.BE_MINMEM] = value
     else:
       # try to read the parameters old style, from the main section
       for name in constants.BES_PARAMETERS:
@@ -8586,7 +9661,7 @@ class LUInstanceCreate(LogicalUnit):
       # pylint: disable=W0142
       self.instance_file_storage_dir = utils.PathJoin(*joinargs)
 
-  def CheckPrereq(self):
+  def CheckPrereq(self): # pylint: disable=R0914
     """Check prerequisites.
 
     """
@@ -8595,13 +9670,17 @@ class LUInstanceCreate(LogicalUnit):
     if self.op.mode == constants.INSTANCE_IMPORT:
       export_info = self._ReadExportInfo()
       self._ReadExportParams(export_info)
+      self._old_instance_name = export_info.get(constants.INISECT_INS, "name")
+    else:
+      self._old_instance_name = None
 
     if (not self.cfg.GetVGName() and
         self.op.disk_template not in constants.DTS_NOT_LVM):
       raise errors.OpPrereqError("Cluster does not support lvm-based"
                                  " instances", errors.ECODE_STATE)
 
-    if self.op.hypervisor is None:
+    if (self.op.hypervisor is None or
+        self.op.hypervisor == constants.VALUE_AUTO):
       self.op.hypervisor = self.cfg.GetHypervisorType()
 
     cluster = self.cfg.GetClusterInfo()
@@ -8627,6 +9706,11 @@ class LUInstanceCreate(LogicalUnit):
     _CheckGlobalHvParams(self.op.hvparams)
 
     # fill and remember the beparams dict
+    default_beparams = cluster.beparams[constants.PP_DEFAULT]
+    for param, value in self.op.beparams.iteritems():
+      if value == constants.VALUE_AUTO:
+        self.op.beparams[param] = default_beparams[param]
+    objects.UpgradeBeParams(self.op.beparams)
     utils.ForceDictType(self.op.beparams, constants.BES_PARAMETER_TYPES)
     self.be_full = cluster.SimpleFillBE(self.op.beparams)
 
@@ -8643,7 +9727,7 @@ class LUInstanceCreate(LogicalUnit):
     for idx, nic in enumerate(self.op.nics):
       nic_mode_req = nic.get(constants.INIC_MODE, None)
       nic_mode = nic_mode_req
-      if nic_mode is None:
+      if nic_mode is None or nic_mode == constants.VALUE_AUTO:
         nic_mode = cluster.nicparams[constants.PP_DEFAULT][constants.NIC_MODE]
 
       # in routed mode, for the first nic, the default ip is 'auto'
@@ -8687,9 +9771,11 @@ class LUInstanceCreate(LogicalUnit):
 
       #  Build nic parameters
       link = nic.get(constants.INIC_LINK, None)
+      if link == constants.VALUE_AUTO:
+        link = cluster.nicparams[constants.PP_DEFAULT][constants.NIC_LINK]
       nicparams = {}
       if nic_mode_req:
-        nicparams[constants.NIC_MODE] = nic_mode_req
+        nicparams[constants.NIC_MODE] = nic_mode
       if link:
         nicparams[constants.NIC_LINK] = link
 
@@ -8719,25 +9805,16 @@ class LUInstanceCreate(LogicalUnit):
         constants.IDISK_SIZE: size,
         constants.IDISK_MODE: mode,
         constants.IDISK_VG: data_vg,
-        constants.IDISK_METAVG: disk.get(constants.IDISK_METAVG, data_vg),
         }
+      if constants.IDISK_METAVG in disk:
+        new_disk[constants.IDISK_METAVG] = disk[constants.IDISK_METAVG]
       if constants.IDISK_ADOPT in disk:
         new_disk[constants.IDISK_ADOPT] = disk[constants.IDISK_ADOPT]
       self.disks.append(new_disk)
 
     if self.op.mode == constants.INSTANCE_IMPORT:
-
-      # Check that the new instance doesn't have less disks than the export
-      instance_disks = len(self.disks)
-      export_disks = export_info.getint(constants.INISECT_INS, 'disk_count')
-      if instance_disks < export_disks:
-        raise errors.OpPrereqError("Not enough disks to import."
-                                   " (instance: %d, export: %d)" %
-                                   (instance_disks, export_disks),
-                                   errors.ECODE_INVAL)
-
       disk_images = []
-      for idx in range(export_disks):
+      for idx in range(len(self.disks)):
         option = "disk%d_dump" % idx
         if export_info.has_option(constants.INISECT_INS, option):
           # FIXME: are the old os-es, disk sizes, etc. useful?
@@ -8749,16 +9826,9 @@ class LUInstanceCreate(LogicalUnit):
 
       self.src_images = disk_images
 
-      old_name = export_info.get(constants.INISECT_INS, "name")
-      try:
-        exp_nic_count = export_info.getint(constants.INISECT_INS, "nic_count")
-      except (TypeError, ValueError), err:
-        raise errors.OpPrereqError("Invalid export file, nic_count is not"
-                                   " an integer: %s" % str(err),
-                                   errors.ECODE_STATE)
-      if self.op.instance_name == old_name:
+      if self.op.instance_name == self._old_instance_name:
         for idx, nic in enumerate(self.nics):
-          if nic.mac == constants.VALUE_AUTO and exp_nic_count >= idx:
+          if nic.mac == constants.VALUE_AUTO:
             nic_mac_ini = "nic%d_mac" % idx
             nic.mac = export_info.get(constants.INISECT_INS, nic_mac_ini)
 
@@ -8788,6 +9858,14 @@ class LUInstanceCreate(LogicalUnit):
     if self.op.iallocator is not None:
       self._RunAllocator()
 
+    # Release all unneeded node locks
+    _ReleaseLocks(self, locking.LEVEL_NODE,
+                  keep=filter(None, [self.op.pnode, self.op.snode,
+                                     self.op.src_node]))
+    _ReleaseLocks(self, locking.LEVEL_NODE_RES,
+                  keep=filter(None, [self.op.pnode, self.op.snode,
+                                     self.op.src_node]))
+
     #### node related checks
 
     # check primary node
@@ -8816,12 +9894,45 @@ class LUInstanceCreate(LogicalUnit):
       _CheckNodeVmCapable(self, self.op.snode)
       self.secondaries.append(self.op.snode)
 
+      snode = self.cfg.GetNodeInfo(self.op.snode)
+      if pnode.group != snode.group:
+        self.LogWarning("The primary and secondary nodes are in two"
+                        " different node groups; the disk parameters"
+                        " from the first disk's node group will be"
+                        " used")
+
     nodenames = [pnode.name] + self.secondaries
 
+    # Verify instance specs
+    spindle_use = self.be_full.get(constants.BE_SPINDLE_USE, None)
+    ispec = {
+      constants.ISPEC_MEM_SIZE: self.be_full.get(constants.BE_MAXMEM, None),
+      constants.ISPEC_CPU_COUNT: self.be_full.get(constants.BE_VCPUS, None),
+      constants.ISPEC_DISK_COUNT: len(self.disks),
+      constants.ISPEC_DISK_SIZE: [disk["size"] for disk in self.disks],
+      constants.ISPEC_NIC_COUNT: len(self.nics),
+      constants.ISPEC_SPINDLE_USE: spindle_use,
+      }
+
+    group_info = self.cfg.GetNodeGroup(pnode.group)
+    ipolicy = _CalculateGroupIPolicy(cluster, group_info)
+    res = _ComputeIPolicyInstanceSpecViolation(ipolicy, ispec)
+    if not self.op.ignore_ipolicy and res:
+      raise errors.OpPrereqError(("Instance allocation to group %s violates"
+                                  " policy: %s") % (pnode.group,
+                                                    utils.CommaJoin(res)),
+                                  errors.ECODE_INVAL)
+
     if not self.adopt_disks:
-      # Check lv size requirements, if not adopting
-      req_sizes = _ComputeDiskSizePerVG(self.op.disk_template, self.disks)
-      _CheckNodesFreeDiskPerVG(self, nodenames, req_sizes)
+      if self.op.disk_template == constants.DT_RBD:
+        # _CheckRADOSFreeSpace() is just a placeholder.
+        # Any function that checks prerequisites can be placed here.
+        # Check if there is enough space on the RADOS cluster.
+        _CheckRADOSFreeSpace()
+      else:
+        # Check lv size requirements, if not adopting
+        req_sizes = _ComputeDiskSizePerVG(self.op.disk_template, self.disks)
+        _CheckNodesFreeDiskPerVG(self, nodenames, req_sizes)
 
     elif self.op.disk_template == constants.DT_PLAIN: # Check the adoption data
       all_lvs = set(["%s/%s" % (disk[constants.IDISK_VG],
@@ -8902,10 +10013,11 @@ class LUInstanceCreate(LogicalUnit):
     _CheckNicsBridgesExist(self, self.nics, self.pnode.name)
 
     # memory check on primary node
+    #TODO(dynmem): use MINMEM for checking
     if self.op.start:
       _CheckNodeFreeMemory(self, self.pnode.name,
                            "creating instance %s" % self.op.instance_name,
-                           self.be_full[constants.BE_MEMORY],
+                           self.be_full[constants.BE_MAXMEM],
                            self.op.hypervisor)
 
     self.dry_run_result = list(nodenames)
@@ -8917,12 +10029,21 @@ class LUInstanceCreate(LogicalUnit):
     instance = self.op.instance_name
     pnode_name = self.pnode.name
 
+    assert not (self.owned_locks(locking.LEVEL_NODE_RES) -
+                self.owned_locks(locking.LEVEL_NODE)), \
+      "Node locks differ from node resource locks"
+
     ht_kind = self.op.hypervisor
     if ht_kind in constants.HTS_REQ_PORT:
       network_port = self.cfg.AllocatePort()
     else:
       network_port = None
 
+    # This is ugly but we got a chicken-egg problem here
+    # We can only take the group disk parameters, as the instance
+    # has no disks yet (we are generating them right here).
+    node = self.cfg.GetNodeInfo(pnode_name)
+    nodegroup = self.cfg.GetNodeGroup(node.group)
     disks = _GenerateDiskTemplate(self,
                                   self.op.disk_template,
                                   instance, pnode_name,
@@ -8931,13 +10052,14 @@ class LUInstanceCreate(LogicalUnit):
                                   self.instance_file_storage_dir,
                                   self.op.file_driver,
                                   0,
-                                  feedback_fn)
+                                  feedback_fn,
+                                  self.cfg.GetGroupDiskParams(nodegroup))
 
     iobj = objects.Instance(name=instance, os=self.op.os_type,
                             primary_node=pnode_name,
                             nics=self.nics, disks=disks,
                             disk_template=self.op.disk_template,
-                            admin_up=False,
+                            admin_state=constants.ADMINST_DOWN,
                             network_port=network_port,
                             beparams=self.op.beparams,
                             hvparams=self.op.hvparams,
@@ -9019,7 +10141,15 @@ class LUInstanceCreate(LogicalUnit):
       raise errors.OpExecError("There are some degraded disks for"
                                " this instance")
 
+    # Release all node resource locks
+    _ReleaseLocks(self, locking.LEVEL_NODE_RES)
+
     if iobj.disk_template != constants.DT_DISKLESS and not self.adopt_disks:
+      # we need to set the disks ID to the primary node, since the
+      # preceding code might or might have not done it, depending on
+      # disk template and other options
+      for disk in iobj.disks:
+        self.cfg.SetDiskID(disk, pnode_name)
       if self.op.mode == constants.INSTANCE_CREATE:
         if not self.op.no_install:
           pause_sync = (iobj.disk_template in constants.DTS_INT_MIRROR and
@@ -9027,7 +10157,8 @@ class LUInstanceCreate(LogicalUnit):
           if pause_sync:
             feedback_fn("* pausing disk sync to install instance OS")
             result = self.rpc.call_blockdev_pause_resume_sync(pnode_name,
-                                                              iobj.disks, True)
+                                                              (iobj.disks,
+                                                               iobj), True)
             for idx, success in enumerate(result.payload):
               if not success:
                 logging.warn("pause-sync of instance %s for disk %d failed",
@@ -9035,93 +10166,111 @@ class LUInstanceCreate(LogicalUnit):
 
           feedback_fn("* running the instance OS create scripts...")
           # FIXME: pass debug option from opcode to backend
-          result = self.rpc.call_instance_os_add(pnode_name, iobj, False,
-                                                 self.op.debug_level)
+          os_add_result = \
+            self.rpc.call_instance_os_add(pnode_name, (iobj, None), False,
+                                          self.op.debug_level)
           if pause_sync:
             feedback_fn("* resuming disk sync")
             result = self.rpc.call_blockdev_pause_resume_sync(pnode_name,
-                                                              iobj.disks, False)
+                                                              (iobj.disks,
+                                                               iobj), False)
             for idx, success in enumerate(result.payload):
               if not success:
                 logging.warn("resume-sync of instance %s for disk %d failed",
                              instance, idx)
 
-          result.Raise("Could not add os for instance %s"
-                       " on node %s" % (instance, pnode_name))
+          os_add_result.Raise("Could not add os for instance %s"
+                              " on node %s" % (instance, pnode_name))
 
-      elif self.op.mode == constants.INSTANCE_IMPORT:
-        feedback_fn("* running the instance OS import scripts...")
+      else:
+        if self.op.mode == constants.INSTANCE_IMPORT:
+          feedback_fn("* running the instance OS import scripts...")
+
+          transfers = []
+
+          for idx, image in enumerate(self.src_images):
+            if not image:
+              continue
+
+            # FIXME: pass debug option from opcode to backend
+            dt = masterd.instance.DiskTransfer("disk/%s" % idx,
+                                               constants.IEIO_FILE, (image, ),
+                                               constants.IEIO_SCRIPT,
+                                               (iobj.disks[idx], idx),
+                                               None)
+            transfers.append(dt)
+
+          import_result = \
+            masterd.instance.TransferInstanceData(self, feedback_fn,
+                                                  self.op.src_node, pnode_name,
+                                                  self.pnode.secondary_ip,
+                                                  iobj, transfers)
+          if not compat.all(import_result):
+            self.LogWarning("Some disks for instance %s on node %s were not"
+                            " imported successfully" % (instance, pnode_name))
+
+          rename_from = self._old_instance_name
+
+        elif self.op.mode == constants.INSTANCE_REMOTE_IMPORT:
+          feedback_fn("* preparing remote import...")
+          # The source cluster will stop the instance before attempting to make
+          # a connection. In some cases stopping an instance can take a long
+          # time, hence the shutdown timeout is added to the connection
+          # timeout.
+          connect_timeout = (constants.RIE_CONNECT_TIMEOUT +
+                             self.op.source_shutdown_timeout)
+          timeouts = masterd.instance.ImportExportTimeouts(connect_timeout)
 
-        transfers = []
+          assert iobj.primary_node == self.pnode.name
+          disk_results = \
+            masterd.instance.RemoteImport(self, feedback_fn, iobj, self.pnode,
+                                          self.source_x509_ca,
+                                          self._cds, timeouts)
+          if not compat.all(disk_results):
+            # TODO: Should the instance still be started, even if some disks
+            # failed to import (valid for local imports, too)?
+            self.LogWarning("Some disks for instance %s on node %s were not"
+                            " imported successfully" % (instance, pnode_name))
 
-        for idx, image in enumerate(self.src_images):
-          if not image:
-            continue
+          rename_from = self.source_instance_name
 
-          # FIXME: pass debug option from opcode to backend
-          dt = masterd.instance.DiskTransfer("disk/%s" % idx,
-                                             constants.IEIO_FILE, (image, ),
-                                             constants.IEIO_SCRIPT,
-                                             (iobj.disks[idx], idx),
-                                             None)
-          transfers.append(dt)
-
-        import_result = \
-          masterd.instance.TransferInstanceData(self, feedback_fn,
-                                                self.op.src_node, pnode_name,
-                                                self.pnode.secondary_ip,
-                                                iobj, transfers)
-        if not compat.all(import_result):
-          self.LogWarning("Some disks for instance %s on node %s were not"
-                          " imported successfully" % (instance, pnode_name))
-
-      elif self.op.mode == constants.INSTANCE_REMOTE_IMPORT:
-        feedback_fn("* preparing remote import...")
-        # The source cluster will stop the instance before attempting to make a
-        # connection. In some cases stopping an instance can take a long time,
-        # hence the shutdown timeout is added to the connection timeout.
-        connect_timeout = (constants.RIE_CONNECT_TIMEOUT +
-                           self.op.source_shutdown_timeout)
-        timeouts = masterd.instance.ImportExportTimeouts(connect_timeout)
-
-        assert iobj.primary_node == self.pnode.name
-        disk_results = \
-          masterd.instance.RemoteImport(self, feedback_fn, iobj, self.pnode,
-                                        self.source_x509_ca,
-                                        self._cds, timeouts)
-        if not compat.all(disk_results):
-          # TODO: Should the instance still be started, even if some disks
-          # failed to import (valid for local imports, too)?
-          self.LogWarning("Some disks for instance %s on node %s were not"
-                          " imported successfully" % (instance, pnode_name))
+        else:
+          # also checked in the prereq part
+          raise errors.ProgrammerError("Unknown OS initialization mode '%s'"
+                                       % self.op.mode)
 
         # Run rename script on newly imported instance
         assert iobj.name == instance
         feedback_fn("Running rename script for %s" % instance)
         result = self.rpc.call_instance_run_rename(pnode_name, iobj,
-                                                   self.source_instance_name,
+                                                   rename_from,
                                                    self.op.debug_level)
         if result.fail_msg:
           self.LogWarning("Failed to run rename script for %s on node"
                           " %s: %s" % (instance, pnode_name, result.fail_msg))
 
-      else:
-        # also checked in the prereq part
-        raise errors.ProgrammerError("Unknown OS initialization mode '%s'"
-                                     % self.op.mode)
+    assert not self.owned_locks(locking.LEVEL_NODE_RES)
 
     if self.op.start:
-      iobj.admin_up = True
+      iobj.admin_state = constants.ADMINST_UP
       self.cfg.Update(iobj, feedback_fn)
       logging.info("Starting instance %s on node %s", instance, pnode_name)
       feedback_fn("* starting instance...")
-      result = self.rpc.call_instance_start(pnode_name, iobj,
-                                            None, None, False)
+      result = self.rpc.call_instance_start(pnode_name, (iobj, None, None),
+                                            False)
       result.Raise("Could not start instance")
 
     return list(iobj.all_nodes)
 
 
+def _CheckRADOSFreeSpace():
+  """Compute disk size requirements inside the RADOS cluster.
+
+  """
+  # For the RADOS cluster we assume there is always enough space.
+  pass
+
+
 class LUInstanceConsole(NoHooksLU):
   """Connect to an instance's console.
 
@@ -9133,6 +10282,7 @@ class LUInstanceConsole(NoHooksLU):
   REQ_BGL = False
 
   def ExpandNames(self):
+    self.share_locks = _ShareAll()
     self._ExpandAndLockInstance()
 
   def CheckPrereq(self):
@@ -9158,10 +10308,12 @@ class LUInstanceConsole(NoHooksLU):
     node_insts.Raise("Can't get node information from %s" % node)
 
     if instance.name not in node_insts.payload:
-      if instance.admin_up:
+      if instance.admin_state == constants.ADMINST_UP:
         state = constants.INSTST_ERRORDOWN
-      else:
+      elif instance.admin_state == constants.ADMINST_DOWN:
         state = constants.INSTST_ADMINDOWN
+      else:
+        state = constants.INSTST_ADMINOFFLINE
       raise errors.OpExecError("Instance %s is not running (state %s)" %
                                (instance.name, state))
 
@@ -9207,6 +10359,7 @@ class LUInstanceReplaceDisks(LogicalUnit):
     self._ExpandAndLockInstance()
 
     assert locking.LEVEL_NODE not in self.needed_locks
+    assert locking.LEVEL_NODE_RES not in self.needed_locks
     assert locking.LEVEL_NODEGROUP not in self.needed_locks
 
     assert self.op.iallocator is None or self.op.remote_node is None, \
@@ -9229,9 +10382,12 @@ class LUInstanceReplaceDisks(LogicalUnit):
         # iallocator will select a new node in the same group
         self.needed_locks[locking.LEVEL_NODEGROUP] = []
 
+    self.needed_locks[locking.LEVEL_NODE_RES] = []
+
     self.replacer = TLReplaceDisks(self, self.op.instance_name, self.op.mode,
                                    self.op.iallocator, self.op.remote_node,
-                                   self.op.disks, False, self.op.early_release)
+                                   self.op.disks, False, self.op.early_release,
+                                   self.op.ignore_ipolicy)
 
     self.tasklets = [self.replacer]
 
@@ -9242,6 +10398,8 @@ class LUInstanceReplaceDisks(LogicalUnit):
       assert not self.needed_locks[locking.LEVEL_NODEGROUP]
 
       self.share_locks[locking.LEVEL_NODEGROUP] = 1
+      # Lock all groups used by instance optimistically; this requires going
+      # via the node before it's locked, requiring verification later on
       self.needed_locks[locking.LEVEL_NODEGROUP] = \
         self.cfg.GetInstanceNodeGroups(self.op.instance_name)
 
@@ -9256,6 +10414,10 @@ class LUInstanceReplaceDisks(LogicalUnit):
           for node_name in self.cfg.GetNodeGroup(group_uuid).members]
       else:
         self._LockInstancesNodes()
+    elif level == locking.LEVEL_NODE_RES:
+      # Reuse node locks
+      self.needed_locks[locking.LEVEL_NODE_RES] = \
+        self.needed_locks[locking.LEVEL_NODE]
 
   def BuildHooksEnv(self):
     """Build hooks env.
@@ -9292,6 +10454,7 @@ class LUInstanceReplaceDisks(LogicalUnit):
     assert (self.glm.is_owned(locking.LEVEL_NODEGROUP) or
             self.op.iallocator is None)
 
+    # Verify if node group locks are still correct
     owned_groups = self.owned_locks(locking.LEVEL_NODEGROUP)
     if owned_groups:
       _CheckInstanceNodeGroups(self.cfg, self.op.instance_name, owned_groups)
@@ -9306,7 +10469,7 @@ class TLReplaceDisks(Tasklet):
 
   """
   def __init__(self, lu, instance_name, mode, iallocator_name, remote_node,
-               disks, delay_iallocator, early_release):
+               disks, delay_iallocator, early_release, ignore_ipolicy):
     """Initializes this class.
 
     """
@@ -9320,6 +10483,7 @@ class TLReplaceDisks(Tasklet):
     self.disks = disks
     self.delay_iallocator = delay_iallocator
     self.early_release = early_release
+    self.ignore_ipolicy = ignore_ipolicy
 
     # Runtime data
     self.instance = None
@@ -9403,7 +10567,7 @@ class TLReplaceDisks(Tasklet):
         self.lu.LogInfo("Checking disk/%d on %s", idx, node)
         self.cfg.SetDiskID(dev, node)
 
-        result = self.rpc.call_blockdev_find(node, dev)
+        result = _BlockdevFind(self, node, dev, instance)
 
         if result.offline:
           continue
@@ -9542,6 +10706,16 @@ class TLReplaceDisks(Tasklet):
       if not self.disks:
         self.disks = range(len(self.instance.disks))
 
+    # TODO: This is ugly, but right now we can't distinguish between internal
+    # submitted opcode and external one. We should fix that.
+    if self.remote_node_info:
+      # We change the node, lets verify it still meets instance policy
+      new_group_info = self.cfg.GetNodeGroup(self.remote_node_info.group)
+      ipolicy = _CalculateGroupIPolicy(self.cfg.GetClusterInfo(),
+                                       new_group_info)
+      _CheckTargetNodeIPolicy(self, ipolicy, instance, self.remote_node_info,
+                              ignore=self.ignore_ipolicy)
+
     for node in check_nodes:
       _CheckNodeOnline(self.lu, node)
 
@@ -9550,8 +10724,9 @@ class TLReplaceDisks(Tasklet):
                                                           self.target_node]
                               if node_name is not None)
 
-    # Release unneeded node locks
+    # Release unneeded node and node resource locks
     _ReleaseLocks(self.lu, locking.LEVEL_NODE, keep=touched_nodes)
+    _ReleaseLocks(self.lu, locking.LEVEL_NODE_RES, keep=touched_nodes)
 
     # Release any owned node group
     if self.lu.glm.is_owned(locking.LEVEL_NODEGROUP):
@@ -9580,6 +10755,8 @@ class TLReplaceDisks(Tasklet):
       assert set(owned_nodes) == set(self.node_secondary_ip), \
           ("Incorrect node locks, owning %s, expected %s" %
            (owned_nodes, self.node_secondary_ip.keys()))
+      assert (self.lu.owned_locks(locking.LEVEL_NODE) ==
+              self.lu.owned_locks(locking.LEVEL_NODE_RES))
 
       owned_instances = self.lu.owned_locks(locking.LEVEL_INSTANCE)
       assert list(owned_instances) == [self.instance_name], \
@@ -9595,7 +10772,7 @@ class TLReplaceDisks(Tasklet):
     feedback_fn("Replacing disk(s) %s for %s" %
                 (utils.CommaJoin(self.disks), self.instance.name))
 
-    activate_disks = (not self.instance.admin_up)
+    activate_disks = (self.instance.admin_state != constants.ADMINST_UP)
 
     # Activate the instance disks if we're replacing them on a down instance
     if activate_disks:
@@ -9615,9 +10792,11 @@ class TLReplaceDisks(Tasklet):
       if activate_disks:
         _SafeShutdownInstanceDisks(self.lu, self.instance)
 
+    assert not self.lu.owned_locks(locking.LEVEL_NODE)
+
     if __debug__:
       # Verify owned locks
-      owned_nodes = self.lu.owned_locks(locking.LEVEL_NODE)
+      owned_nodes = self.lu.owned_locks(locking.LEVEL_NODE_RES)
       nodes = frozenset(self.node_secondary_ip)
       assert ((self.early_release and not owned_nodes) or
               (not self.early_release and not (set(owned_nodes) - nodes))), \
@@ -9653,7 +10832,7 @@ class TLReplaceDisks(Tasklet):
         self.lu.LogInfo("Checking disk/%d on %s" % (idx, node))
         self.cfg.SetDiskID(dev, node)
 
-        result = self.rpc.call_blockdev_find(node, dev)
+        result = _BlockdevFind(self, node, dev, self.instance)
 
         msg = result.fail_msg
         if msg or not result.payload:
@@ -9670,8 +10849,8 @@ class TLReplaceDisks(Tasklet):
       self.lu.LogInfo("Checking disk/%d consistency on node %s" %
                       (idx, node_name))
 
-      if not _CheckDiskConsistency(self.lu, dev, node_name, on_primary,
-                                   ldisk=ldisk):
+      if not _CheckDiskConsistency(self.lu, self.instance, dev, node_name,
+                                   on_primary, ldisk=ldisk):
         raise errors.OpExecError("Node %s has degraded storage, unsafe to"
                                  " replace disks for instance %s" %
                                  (node_name, self.instance.name))
@@ -9685,7 +10864,8 @@ class TLReplaceDisks(Tasklet):
     """
     iv_names = {}
 
-    for idx, dev in enumerate(self.instance.disks):
+    disks = _AnnotateDiskParams(self.instance, self.instance.disks, self.cfg)
+    for idx, dev in enumerate(disks):
       if idx not in self.disks:
         continue
 
@@ -9696,12 +10876,15 @@ class TLReplaceDisks(Tasklet):
       lv_names = [".disk%d_%s" % (idx, suffix) for suffix in ["data", "meta"]]
       names = _GenerateUniqueNames(self.lu, lv_names)
 
-      vg_data = dev.children[0].logical_id[0]
+      (data_disk, meta_disk) = dev.children
+      vg_data = data_disk.logical_id[0]
       lv_data = objects.Disk(dev_type=constants.LD_LV, size=dev.size,
-                             logical_id=(vg_data, names[0]))
-      vg_meta = dev.children[1].logical_id[0]
-      lv_meta = objects.Disk(dev_type=constants.LD_LV, size=128,
-                             logical_id=(vg_meta, names[1]))
+                             logical_id=(vg_data, names[0]),
+                             params=data_disk.params)
+      vg_meta = meta_disk.logical_id[0]
+      lv_meta = objects.Disk(dev_type=constants.LD_LV, size=DRBD_META_SIZE,
+                             logical_id=(vg_meta, names[1]),
+                             params=meta_disk.params)
 
       new_lvs = [lv_data, lv_meta]
       old_lvs = [child.Copy() for child in dev.children]
@@ -9709,8 +10892,8 @@ class TLReplaceDisks(Tasklet):
 
       # we pass force_create=True to force the LVM creation
       for new_lv in new_lvs:
-        _CreateBlockDev(self.lu, node_name, self.instance, new_lv, True,
-                        _GetInstanceInfoText(self.instance), False)
+        _CreateBlockDevInner(self.lu, node_name, self.instance, new_lv, True,
+                             _GetInstanceInfoText(self.instance), False)
 
     return iv_names
 
@@ -9718,7 +10901,7 @@ class TLReplaceDisks(Tasklet):
     for name, (dev, _, _) in iv_names.iteritems():
       self.cfg.SetDiskID(dev, node_name)
 
-      result = self.rpc.call_blockdev_find(node_name, dev)
+      result = _BlockdevFind(self, node_name, dev, self.instance)
 
       msg = result.fail_msg
       if msg or not result.payload:
@@ -9839,8 +11022,8 @@ class TLReplaceDisks(Tasklet):
 
       # Now that the new lvs have the old name, we can add them to the device
       self.lu.LogInfo("Adding new mirror component on %s" % self.target_node)
-      result = self.rpc.call_blockdev_addchildren(self.target_node, dev,
-                                                  new_lvs)
+      result = self.rpc.call_blockdev_addchildren(self.target_node,
+                                                  (dev, self.instance), new_lvs)
       msg = result.fail_msg
       if msg:
         for new_lv in new_lvs:
@@ -9852,21 +11035,28 @@ class TLReplaceDisks(Tasklet):
                                      "volumes"))
         raise errors.OpExecError("Can't add local storage to drbd: %s" % msg)
 
-    cstep = 5
+    cstep = itertools.count(5)
+
     if self.early_release:
-      self.lu.LogStep(cstep, steps_total, "Removing old storage")
-      cstep += 1
+      self.lu.LogStep(cstep.next(), steps_total, "Removing old storage")
       self._RemoveOldStorage(self.target_node, iv_names)
-      # WARNING: we release both node locks here, do not do other RPCs
-      # than WaitForSync to the primary node
-      _ReleaseLocks(self.lu, locking.LEVEL_NODE,
-                    names=[self.target_node, self.other_node])
+      # TODO: Check if releasing locks early still makes sense
+      _ReleaseLocks(self.lu, locking.LEVEL_NODE_RES)
+    else:
+      # Release all resource locks except those used by the instance
+      _ReleaseLocks(self.lu, locking.LEVEL_NODE_RES,
+                    keep=self.node_secondary_ip.keys())
+
+    # Release all node locks while waiting for sync
+    _ReleaseLocks(self.lu, locking.LEVEL_NODE)
+
+    # TODO: Can the instance lock be downgraded here? Take the optional disk
+    # shutdown in the caller into consideration.
 
     # Wait for sync
     # This can fail as the old devices are degraded and _WaitForSync
     # does a combined result over all disks, so we don't check its return value
-    self.lu.LogStep(cstep, steps_total, "Sync devices")
-    cstep += 1
+    self.lu.LogStep(cstep.next(), steps_total, "Sync devices")
     _WaitForSync(self.lu, self.instance)
 
     # Check all devices manually
@@ -9874,8 +11064,7 @@ class TLReplaceDisks(Tasklet):
 
     # Step: remove old storage
     if not self.early_release:
-      self.lu.LogStep(cstep, steps_total, "Removing old storage")
-      cstep += 1
+      self.lu.LogStep(cstep.next(), steps_total, "Removing old storage")
       self._RemoveOldStorage(self.target_node, iv_names)
 
   def _ExecDrbd8Secondary(self, feedback_fn):
@@ -9912,13 +11101,14 @@ class TLReplaceDisks(Tasklet):
 
     # Step: create new storage
     self.lu.LogStep(3, steps_total, "Allocate new storage")
-    for idx, dev in enumerate(self.instance.disks):
+    disks = _AnnotateDiskParams(self.instance, self.instance.disks, self.cfg)
+    for idx, dev in enumerate(disks):
       self.lu.LogInfo("Adding new local storage on %s for disk/%d" %
                       (self.new_node, idx))
       # we pass force_create=True to force LVM creation
       for new_lv in dev.children:
-        _CreateBlockDev(self.lu, self.new_node, self.instance, new_lv, True,
-                        _GetInstanceInfoText(self.instance), False)
+        _CreateBlockDevInner(self.lu, self.new_node, self.instance, new_lv,
+                             True, _GetInstanceInfoText(self.instance), False)
 
     # Step 4: dbrd minors and drbd setups changes
     # after this, we must manually remove the drbd minors on both the
@@ -9955,9 +11145,13 @@ class TLReplaceDisks(Tasklet):
       new_drbd = objects.Disk(dev_type=constants.LD_DRBD8,
                               logical_id=new_alone_id,
                               children=dev.children,
-                              size=dev.size)
+                              size=dev.size,
+                              params={})
+      (anno_new_drbd,) = _AnnotateDiskParams(self.instance, [new_drbd],
+                                             self.cfg)
       try:
-        _CreateSingleBlockDev(self.lu, self.new_node, self.instance, new_drbd,
+        _CreateSingleBlockDev(self.lu, self.new_node, self.instance,
+                              anno_new_drbd,
                               _GetInstanceInfoText(self.instance), False)
       except errors.GenericError:
         self.cfg.ReleaseDRBDMinors(self.instance.name)
@@ -9967,7 +11161,8 @@ class TLReplaceDisks(Tasklet):
     for idx, dev in enumerate(self.instance.disks):
       self.lu.LogInfo("Shutting down drbd for disk/%d on old node" % idx)
       self.cfg.SetDiskID(dev, self.target_node)
-      msg = self.rpc.call_blockdev_shutdown(self.target_node, dev).fail_msg
+      msg = self.rpc.call_blockdev_shutdown(self.target_node,
+                                            (dev, self.instance)).fail_msg
       if msg:
         self.lu.LogWarning("Failed to shutdown drbd for disk/%d on old"
                            "node: %s" % (idx, msg),
@@ -9994,13 +11189,16 @@ class TLReplaceDisks(Tasklet):
 
     self.cfg.Update(self.instance, feedback_fn)
 
+    # Release all node locks (the configuration has been updated)
+    _ReleaseLocks(self.lu, locking.LEVEL_NODE)
+
     # and now perform the drbd attach
     self.lu.LogInfo("Attaching primary drbds to new secondary"
                     " (standalone => connected)")
     result = self.rpc.call_drbd_attach_net([self.instance.primary_node,
                                             self.new_node],
                                            self.node_secondary_ip,
-                                           self.instance.disks,
+                                           (self.instance.disks, self.instance),
                                            self.instance.name,
                                            False)
     for to_node, to_result in result.items():
@@ -10010,23 +11208,26 @@ class TLReplaceDisks(Tasklet):
                            to_node, msg,
                            hint=("please do a gnt-instance info to see the"
                                  " status of disks"))
-    cstep = 5
+
+    cstep = itertools.count(5)
+
     if self.early_release:
-      self.lu.LogStep(cstep, steps_total, "Removing old storage")
-      cstep += 1
+      self.lu.LogStep(cstep.next(), steps_total, "Removing old storage")
       self._RemoveOldStorage(self.target_node, iv_names)
-      # WARNING: we release all node locks here, do not do other RPCs
-      # than WaitForSync to the primary node
-      _ReleaseLocks(self.lu, locking.LEVEL_NODE,
-                    names=[self.instance.primary_node,
-                           self.target_node,
-                           self.new_node])
+      # TODO: Check if releasing locks early still makes sense
+      _ReleaseLocks(self.lu, locking.LEVEL_NODE_RES)
+    else:
+      # Release all resource locks except those used by the instance
+      _ReleaseLocks(self.lu, locking.LEVEL_NODE_RES,
+                    keep=self.node_secondary_ip.keys())
+
+    # TODO: Can the instance lock be downgraded here? Take the optional disk
+    # shutdown in the caller into consideration.
 
     # Wait for sync
     # This can fail as the old devices are degraded and _WaitForSync
     # does a combined result over all disks, so we don't check its return value
-    self.lu.LogStep(cstep, steps_total, "Sync devices")
-    cstep += 1
+    self.lu.LogStep(cstep.next(), steps_total, "Sync devices")
     _WaitForSync(self.lu, self.instance)
 
     # Check all devices manually
@@ -10034,7 +11235,7 @@ class TLReplaceDisks(Tasklet):
 
     # Step: remove old storage
     if not self.early_release:
-      self.lu.LogStep(cstep, steps_total, "Removing old storage")
+      self.lu.LogStep(cstep.next(), steps_total, "Removing old storage")
       self._RemoveOldStorage(self.target_node, iv_names)
 
 
@@ -10080,7 +11281,7 @@ class LURepairNodeStorage(NoHooksLU):
     """
     # Check whether any instance on this node has faulty disks
     for inst in _GetNodeInstances(self.cfg, self.op.node_name):
-      if not inst.admin_up:
+      if inst.admin_state != constants.ADMINST_UP:
         continue
       check_nodes = set(inst.all_nodes)
       check_nodes.discard(self.op.node_name)
@@ -10106,6 +11307,15 @@ class LUNodeEvacuate(NoHooksLU):
   """
   REQ_BGL = False
 
+  _MODE2IALLOCATOR = {
+    constants.NODE_EVAC_PRI: constants.IALLOCATOR_NEVAC_PRI,
+    constants.NODE_EVAC_SEC: constants.IALLOCATOR_NEVAC_SEC,
+    constants.NODE_EVAC_ALL: constants.IALLOCATOR_NEVAC_ALL,
+    }
+  assert frozenset(_MODE2IALLOCATOR.keys()) == constants.NODE_EVAC_MODES
+  assert (frozenset(_MODE2IALLOCATOR.values()) ==
+          constants.IALLOCATOR_NEVAC_MODES)
+
   def CheckArguments(self):
     _CheckIAllocatorOrNode(self, "iallocator", "remote_node")
 
@@ -10120,7 +11330,7 @@ class LUNodeEvacuate(NoHooksLU):
         raise errors.OpPrereqError("Can not use evacuated node as a new"
                                    " secondary node", errors.ECODE_INVAL)
 
-      if self.op.mode != constants.IALLOCATOR_NEVAC_SEC:
+      if self.op.mode != constants.NODE_EVAC_SEC:
         raise errors.OpPrereqError("Without the use of an iallocator only"
                                    " secondary instances can be evacuated",
                                    errors.ECODE_INVAL)
@@ -10154,19 +11364,19 @@ class LUNodeEvacuate(NoHooksLU):
     """Builds list of instances to operate on.
 
     """
-    assert self.op.mode in constants.IALLOCATOR_NEVAC_MODES
+    assert self.op.mode in constants.NODE_EVAC_MODES
 
-    if self.op.mode == constants.IALLOCATOR_NEVAC_PRI:
+    if self.op.mode == constants.NODE_EVAC_PRI:
       # Primary instances only
       inst_fn = _GetNodePrimaryInstances
       assert self.op.remote_node is None, \
         "Evacuating primary instances requires iallocator"
-    elif self.op.mode == constants.IALLOCATOR_NEVAC_SEC:
+    elif self.op.mode == constants.NODE_EVAC_SEC:
       # Secondary instances only
       inst_fn = _GetNodeSecondaryInstances
     else:
       # All instances
-      assert self.op.mode == constants.IALLOCATOR_NEVAC_ALL
+      assert self.op.mode == constants.NODE_EVAC_ALL
       inst_fn = _GetNodeInstances
       # TODO: In 2.6, change the iallocator interface to take an evacuation mode
       # per instance
@@ -10260,7 +11470,7 @@ class LUNodeEvacuate(NoHooksLU):
     elif self.op.iallocator is not None:
       # TODO: Implement relocation to other group
       ial = IAllocator(self.cfg, self.rpc, constants.IALLOCATOR_MODE_NODE_EVAC,
-                       evac_mode=self.op.mode,
+                       evac_mode=self._MODE2IALLOCATOR[self.op.mode],
                        instances=list(self.instance_names))
 
       ial.Run(self.op.iallocator)
@@ -10274,7 +11484,7 @@ class LUNodeEvacuate(NoHooksLU):
       jobs = _LoadNodeEvacResult(self, ial.result, self.op.early_release, True)
 
     elif self.op.remote_node is not None:
-      assert self.op.mode == constants.IALLOCATOR_NEVAC_SEC
+      assert self.op.mode == constants.NODE_EVAC_SEC
       jobs = [
         [opcodes.OpInstanceReplaceDisks(instance_name=instance_name,
                                         remote_node=self.op.remote_node,
@@ -10358,11 +11568,17 @@ class LUInstanceGrowDisk(LogicalUnit):
   def ExpandNames(self):
     self._ExpandAndLockInstance()
     self.needed_locks[locking.LEVEL_NODE] = []
+    self.needed_locks[locking.LEVEL_NODE_RES] = []
     self.recalculate_locks[locking.LEVEL_NODE] = constants.LOCKS_REPLACE
+    self.recalculate_locks[locking.LEVEL_NODE_RES] = constants.LOCKS_REPLACE
 
   def DeclareLocks(self, level):
     if level == locking.LEVEL_NODE:
       self._LockInstancesNodes()
+    elif level == locking.LEVEL_NODE_RES:
+      # Copy node locks
+      self.needed_locks[locking.LEVEL_NODE_RES] = \
+        self.needed_locks[locking.LEVEL_NODE][:]
 
   def BuildHooksEnv(self):
     """Build hooks env.
@@ -10373,6 +11589,7 @@ class LUInstanceGrowDisk(LogicalUnit):
     env = {
       "DISK": self.op.disk,
       "AMOUNT": self.op.amount,
+      "ABSOLUTE": self.op.absolute,
       }
     env.update(_BuildInstanceHookEnvByObject(self, self.instance))
     return env
@@ -10405,12 +11622,30 @@ class LUInstanceGrowDisk(LogicalUnit):
 
     self.disk = instance.FindDisk(self.op.disk)
 
+    if self.op.absolute:
+      self.target = self.op.amount
+      self.delta = self.target - self.disk.size
+      if self.delta < 0:
+        raise errors.OpPrereqError("Requested size (%s) is smaller than "
+                                   "current disk size (%s)" %
+                                   (utils.FormatUnit(self.target, "h"),
+                                    utils.FormatUnit(self.disk.size, "h")),
+                                   errors.ECODE_STATE)
+    else:
+      self.delta = self.op.amount
+      self.target = self.disk.size + self.delta
+      if self.delta < 0:
+        raise errors.OpPrereqError("Requested increment (%s) is negative" %
+                                   utils.FormatUnit(self.delta, "h"),
+                                   errors.ECODE_INVAL)
+
     if instance.disk_template not in (constants.DT_FILE,
-                                      constants.DT_SHARED_FILE):
+                                      constants.DT_SHARED_FILE,
+                                      constants.DT_RBD):
       # TODO: check the free disk space for file, when that feature will be
       # supported
       _CheckNodesFreeDiskPerVG(self, nodenames,
-                               self.disk.ComputeGrowth(self.op.amount))
+                               self.disk.ComputeGrowth(self.delta))
 
   def Exec(self, feedback_fn):
     """Execute disk grow.
@@ -10419,21 +11654,32 @@ class LUInstanceGrowDisk(LogicalUnit):
     instance = self.instance
     disk = self.disk
 
+    assert set([instance.name]) == self.owned_locks(locking.LEVEL_INSTANCE)
+    assert (self.owned_locks(locking.LEVEL_NODE) ==
+            self.owned_locks(locking.LEVEL_NODE_RES))
+
     disks_ok, _ = _AssembleInstanceDisks(self, self.instance, disks=[disk])
     if not disks_ok:
       raise errors.OpExecError("Cannot activate block device to grow")
 
+    feedback_fn("Growing disk %s of instance '%s' by %s to %s" %
+                (self.op.disk, instance.name,
+                 utils.FormatUnit(self.delta, "h"),
+                 utils.FormatUnit(self.target, "h")))
+
     # First run all grow ops in dry-run mode
     for node in instance.all_nodes:
       self.cfg.SetDiskID(disk, node)
-      result = self.rpc.call_blockdev_grow(node, disk, self.op.amount, True)
+      result = self.rpc.call_blockdev_grow(node, (disk, instance), self.delta,
+                                           True)
       result.Raise("Grow request failed to node %s" % node)
 
     # We know that (as far as we can test) operations across different
     # nodes will succeed, time to run it for real
     for node in instance.all_nodes:
       self.cfg.SetDiskID(disk, node)
-      result = self.rpc.call_blockdev_grow(node, disk, self.op.amount, False)
+      result = self.rpc.call_blockdev_grow(node, (disk, instance), self.delta,
+                                           False)
       result.Raise("Grow request failed to node %s" % node)
 
       # TODO: Rewrite code to work properly
@@ -10443,20 +11689,30 @@ class LUInstanceGrowDisk(LogicalUnit):
       # time is a work-around.
       time.sleep(5)
 
-    disk.RecordGrow(self.op.amount)
+    disk.RecordGrow(self.delta)
     self.cfg.Update(instance, feedback_fn)
+
+    # Changes have been recorded, release node lock
+    _ReleaseLocks(self, locking.LEVEL_NODE)
+
+    # Downgrade lock while waiting for sync
+    self.glm.downgrade(locking.LEVEL_INSTANCE)
+
     if self.op.wait_for_sync:
       disk_abort = not _WaitForSync(self, instance, disks=[disk])
       if disk_abort:
         self.proc.LogWarning("Disk sync-ing has not returned a good"
                              " status; please check the instance")
-      if not instance.admin_up:
+      if instance.admin_state != constants.ADMINST_UP:
         _SafeShutdownInstanceDisks(self, instance, disks=[disk])
-    elif not instance.admin_up:
+    elif instance.admin_state != constants.ADMINST_UP:
       self.proc.LogWarning("Not shutting down the disk even if the instance is"
                            " not supposed to be running because no wait for"
                            " sync mode was requested")
 
+    assert self.owned_locks(locking.LEVEL_NODE_RES)
+    assert set([instance.name]) == self.owned_locks(locking.LEVEL_INSTANCE)
+
 
 class LUInstanceQueryData(NoHooksLU):
   """Query runtime instance data.
@@ -10487,12 +11743,25 @@ class LUInstanceQueryData(NoHooksLU):
       else:
         self.needed_locks[locking.LEVEL_INSTANCE] = self.wanted_names
 
+      self.needed_locks[locking.LEVEL_NODEGROUP] = []
       self.needed_locks[locking.LEVEL_NODE] = []
       self.recalculate_locks[locking.LEVEL_NODE] = constants.LOCKS_REPLACE
 
   def DeclareLocks(self, level):
-    if self.op.use_locking and level == locking.LEVEL_NODE:
-      self._LockInstancesNodes()
+    if self.op.use_locking:
+      if level == locking.LEVEL_NODEGROUP:
+        owned_instances = self.owned_locks(locking.LEVEL_INSTANCE)
+
+        # Lock all groups used by instances optimistically; this requires going
+        # via the node before it's locked, requiring verification later on
+        self.needed_locks[locking.LEVEL_NODEGROUP] = \
+          frozenset(group_uuid
+                    for instance_name in owned_instances
+                    for group_uuid in
+                      self.cfg.GetInstanceNodeGroups(instance_name))
+
+      elif level == locking.LEVEL_NODE:
+        self._LockInstancesNodes()
 
   def CheckPrereq(self):
     """Check prerequisites.
@@ -10500,14 +11769,25 @@ class LUInstanceQueryData(NoHooksLU):
     This only checks the optional instance list against the existing names.
 
     """
+    owned_instances = frozenset(self.owned_locks(locking.LEVEL_INSTANCE))
+    owned_groups = frozenset(self.owned_locks(locking.LEVEL_NODEGROUP))
+    owned_nodes = frozenset(self.owned_locks(locking.LEVEL_NODE))
+
     if self.wanted_names is None:
       assert self.op.use_locking, "Locking was not used"
-      self.wanted_names = self.owned_locks(locking.LEVEL_INSTANCE)
+      self.wanted_names = owned_instances
 
-    self.wanted_instances = \
-        map(compat.snd, self.cfg.GetMultiInstanceInfo(self.wanted_names))
+    instances = dict(self.cfg.GetMultiInstanceInfo(self.wanted_names))
 
-  def _ComputeBlockdevStatus(self, node, instance_name, dev):
+    if self.op.use_locking:
+      _CheckInstancesNodeGroups(self.cfg, instances, owned_groups, owned_nodes,
+                                None)
+    else:
+      assert not (owned_instances or owned_groups or owned_nodes)
+
+    self.wanted_instances = instances.values()
+
+  def _ComputeBlockdevStatus(self, node, instance, dev):
     """Returns the status of a block device
 
     """
@@ -10520,7 +11800,7 @@ class LUInstanceQueryData(NoHooksLU):
     if result.offline:
       return None
 
-    result.Raise("Can't compute disk status for %s" % instance_name)
+    result.Raise("Can't compute disk status for %s" % instance.name)
 
     status = result.payload
     if status is None:
@@ -10534,19 +11814,29 @@ class LUInstanceQueryData(NoHooksLU):
     """Compute block device status.
 
     """
-    if dev.dev_type in constants.LDS_DRBD:
-      # we change the snode then (otherwise we use the one passed in)
-      if dev.logical_id[0] == instance.primary_node:
-        snode = dev.logical_id[1]
+    (anno_dev,) = _AnnotateDiskParams(instance, [dev], self.cfg)
+
+    return self._ComputeDiskStatusInner(instance, snode, anno_dev)
+
+  def _ComputeDiskStatusInner(self, instance, snode, dev):
+    """Compute block device status.
+
+    @attention: The device has to be annotated already.
+
+    """
+    if dev.dev_type in constants.LDS_DRBD:
+      # we change the snode then (otherwise we use the one passed in)
+      if dev.logical_id[0] == instance.primary_node:
+        snode = dev.logical_id[1]
       else:
         snode = dev.logical_id[0]
 
     dev_pstatus = self._ComputeBlockdevStatus(instance.primary_node,
-                                              instance.name, dev)
-    dev_sstatus = self._ComputeBlockdevStatus(snode, instance.name, dev)
+                                              instance, dev)
+    dev_sstatus = self._ComputeBlockdevStatus(snode, instance, dev)
 
     if dev.children:
-      dev_children = map(compat.partial(self._ComputeDiskStatus,
+      dev_children = map(compat.partial(self._ComputeDiskStatusInner,
                                         instance, snode),
                          dev.children)
     else:
@@ -10570,9 +11860,17 @@ class LUInstanceQueryData(NoHooksLU):
 
     cluster = self.cfg.GetClusterInfo()
 
-    pri_nodes = self.cfg.GetMultiNodeInfo(i.primary_node
-                                          for i in self.wanted_instances)
-    for instance, (_, pnode) in zip(self.wanted_instances, pri_nodes):
+    node_names = itertools.chain(*(i.all_nodes for i in self.wanted_instances))
+    nodes = dict(self.cfg.GetMultiNodeInfo(node_names))
+
+    groups = dict(self.cfg.GetMultiNodeGroupInfo(node.group
+                                                 for node in nodes.values()))
+
+    group2name_fn = lambda uuid: groups[uuid].name
+
+    for instance in self.wanted_instances:
+      pnode = nodes[instance.primary_node]
+
       if self.op.static or pnode.offline:
         remote_state = None
         if pnode.offline:
@@ -10588,22 +11886,27 @@ class LUInstanceQueryData(NoHooksLU):
         if remote_info and "state" in remote_info:
           remote_state = "up"
         else:
-          remote_state = "down"
-
-      if instance.admin_up:
-        config_state = "up"
-      else:
-        config_state = "down"
+          if instance.admin_state == constants.ADMINST_UP:
+            remote_state = "down"
+          else:
+            remote_state = instance.admin_state
 
       disks = map(compat.partial(self._ComputeDiskStatus, instance, None),
                   instance.disks)
 
+      snodes_group_uuids = [nodes[snode_name].group
+                            for snode_name in instance.secondary_nodes]
+
       result[instance.name] = {
         "name": instance.name,
-        "config_state": config_state,
+        "config_state": instance.admin_state,
         "run_state": remote_state,
         "pnode": instance.primary_node,
+        "pnode_group_uuid": pnode.group,
+        "pnode_group_name": group2name_fn(pnode.group),
         "snodes": instance.secondary_nodes,
+        "snodes_group_uuids": snodes_group_uuids,
+        "snodes_group_names": map(group2name_fn, snodes_group_uuids),
         "os": instance.os,
         # this happens to be the same format used for hooks
         "nics": _NICListToTuple(self, instance.nics),
@@ -10626,6 +11929,144 @@ class LUInstanceQueryData(NoHooksLU):
     return result
 
 
+def PrepareContainerMods(mods, private_fn):
+  """Prepares a list of container modifications by adding a private data field.
+
+  @type mods: list of tuples; (operation, index, parameters)
+  @param mods: List of modifications
+  @type private_fn: callable or None
+  @param private_fn: Callable for constructing a private data field for a
+    modification
+  @rtype: list
+
+  """
+  if private_fn is None:
+    fn = lambda: None
+  else:
+    fn = private_fn
+
+  return [(op, idx, params, fn()) for (op, idx, params) in mods]
+
+
+#: Type description for changes as returned by L{ApplyContainerMods}'s
+#: callbacks
+_TApplyContModsCbChanges = \
+  ht.TMaybeListOf(ht.TAnd(ht.TIsLength(2), ht.TItems([
+    ht.TNonEmptyString,
+    ht.TAny,
+    ])))
+
+
+def ApplyContainerMods(kind, container, chgdesc, mods,
+                       create_fn, modify_fn, remove_fn):
+  """Applies descriptions in C{mods} to C{container}.
+
+  @type kind: string
+  @param kind: One-word item description
+  @type container: list
+  @param container: Container to modify
+  @type chgdesc: None or list
+  @param chgdesc: List of applied changes
+  @type mods: list
+  @param mods: Modifications as returned by L{PrepareContainerMods}
+  @type create_fn: callable
+  @param create_fn: Callback for creating a new item (L{constants.DDM_ADD});
+    receives absolute item index, parameters and private data object as added
+    by L{PrepareContainerMods}, returns tuple containing new item and changes
+    as list
+  @type modify_fn: callable
+  @param modify_fn: Callback for modifying an existing item
+    (L{constants.DDM_MODIFY}); receives absolute item index, item, parameters
+    and private data object as added by L{PrepareContainerMods}, returns
+    changes as list
+  @type remove_fn: callable
+  @param remove_fn: Callback on removing item; receives absolute item index,
+    item and private data object as added by L{PrepareContainerMods}
+
+  """
+  for (op, idx, params, private) in mods:
+    if idx == -1:
+      # Append
+      absidx = len(container) - 1
+    elif idx < 0:
+      raise IndexError("Not accepting negative indices other than -1")
+    elif idx > len(container):
+      raise IndexError("Got %s index %s, but there are only %s" %
+                       (kind, idx, len(container)))
+    else:
+      absidx = idx
+
+    changes = None
+
+    if op == constants.DDM_ADD:
+      # Calculate where item will be added
+      if idx == -1:
+        addidx = len(container)
+      else:
+        addidx = idx
+
+      if create_fn is None:
+        item = params
+      else:
+        (item, changes) = create_fn(addidx, params, private)
+
+      if idx == -1:
+        container.append(item)
+      else:
+        assert idx >= 0
+        assert idx <= len(container)
+        # list.insert does so before the specified index
+        container.insert(idx, item)
+    else:
+      # Retrieve existing item
+      try:
+        item = container[absidx]
+      except IndexError:
+        raise IndexError("Invalid %s index %s" % (kind, idx))
+
+      if op == constants.DDM_REMOVE:
+        assert not params
+
+        if remove_fn is not None:
+          remove_fn(absidx, item, private)
+
+        changes = [("%s/%s" % (kind, absidx), "remove")]
+
+        assert container[absidx] == item
+        del container[absidx]
+      elif op == constants.DDM_MODIFY:
+        if modify_fn is not None:
+          changes = modify_fn(absidx, item, params, private)
+      else:
+        raise errors.ProgrammerError("Unhandled operation '%s'" % op)
+
+    assert _TApplyContModsCbChanges(changes)
+
+    if not (chgdesc is None or changes is None):
+      chgdesc.extend(changes)
+
+
+def _UpdateIvNames(base_index, disks):
+  """Updates the C{iv_name} attribute of disks.
+
+  @type disks: list of L{objects.Disk}
+
+  """
+  for (idx, disk) in enumerate(disks):
+    disk.iv_name = "disk/%s" % (base_index + idx, )
+
+
+class _InstNicModPrivate:
+  """Data structure for network interface modifications.
+
+  Used by L{LUInstanceSetParams}.
+
+  """
+  def __init__(self):
+    self.params = None
+    self.filled = None
+
+
 class LUInstanceSetParams(LogicalUnit):
   """Modifies an instances's parameters.
 
@@ -10634,54 +12075,140 @@ class LUInstanceSetParams(LogicalUnit):
   HTYPE = constants.HTYPE_INSTANCE
   REQ_BGL = False
 
+  @staticmethod
+  def _UpgradeDiskNicMods(kind, mods, verify_fn):
+    assert ht.TList(mods)
+    assert not mods or len(mods[0]) in (2, 3)
+
+    if mods and len(mods[0]) == 2:
+      result = []
+
+      addremove = 0
+      for op, params in mods:
+        if op in (constants.DDM_ADD, constants.DDM_REMOVE):
+          result.append((op, -1, params))
+          addremove += 1
+
+          if addremove > 1:
+            raise errors.OpPrereqError("Only one %s add or remove operation is"
+                                       " supported at a time" % kind,
+                                       errors.ECODE_INVAL)
+        else:
+          result.append((constants.DDM_MODIFY, op, params))
+
+      assert verify_fn(result)
+    else:
+      result = mods
+
+    return result
+
+  @staticmethod
+  def _CheckMods(kind, mods, key_types, item_fn):
+    """Ensures requested disk/NIC modifications are valid.
+
+    """
+    for (op, _, params) in mods:
+      assert ht.TDict(params)
+
+      utils.ForceDictType(params, key_types)
+
+      if op == constants.DDM_REMOVE:
+        if params:
+          raise errors.OpPrereqError("No settings should be passed when"
+                                     " removing a %s" % kind,
+                                     errors.ECODE_INVAL)
+      elif op in (constants.DDM_ADD, constants.DDM_MODIFY):
+        item_fn(op, params)
+      else:
+        raise errors.ProgrammerError("Unhandled operation '%s'" % op)
+
+  @staticmethod
+  def _VerifyDiskModification(op, params):
+    """Verifies a disk modification.
+
+    """
+    if op == constants.DDM_ADD:
+      mode = params.setdefault(constants.IDISK_MODE, constants.DISK_RDWR)
+      if mode not in constants.DISK_ACCESS_SET:
+        raise errors.OpPrereqError("Invalid disk access mode '%s'" % mode,
+                                   errors.ECODE_INVAL)
+
+      size = params.get(constants.IDISK_SIZE, None)
+      if size is None:
+        raise errors.OpPrereqError("Required disk parameter '%s' missing" %
+                                   constants.IDISK_SIZE, errors.ECODE_INVAL)
+
+      try:
+        size = int(size)
+      except (TypeError, ValueError), err:
+        raise errors.OpPrereqError("Invalid disk size parameter: %s" % err,
+                                   errors.ECODE_INVAL)
+
+      params[constants.IDISK_SIZE] = size
+
+    elif op == constants.DDM_MODIFY and constants.IDISK_SIZE in params:
+      raise errors.OpPrereqError("Disk size change not possible, use"
+                                 " grow-disk", errors.ECODE_INVAL)
+
+  @staticmethod
+  def _VerifyNicModification(op, params):
+    """Verifies a network interface modification.
+
+    """
+    if op in (constants.DDM_ADD, constants.DDM_MODIFY):
+      ip = params.get(constants.INIC_IP, None)
+      if ip is None:
+        pass
+      elif ip.lower() == constants.VALUE_NONE:
+        params[constants.INIC_IP] = None
+      elif not netutils.IPAddress.IsValid(ip):
+        raise errors.OpPrereqError("Invalid IP address '%s'" % ip,
+                                   errors.ECODE_INVAL)
+
+      bridge = params.get("bridge", None)
+      link = params.get(constants.INIC_LINK, None)
+      if bridge and link:
+        raise errors.OpPrereqError("Cannot pass 'bridge' and 'link'"
+                                   " at the same time", errors.ECODE_INVAL)
+      elif bridge and bridge.lower() == constants.VALUE_NONE:
+        params["bridge"] = None
+      elif link and link.lower() == constants.VALUE_NONE:
+        params[constants.INIC_LINK] = None
+
+      if op == constants.DDM_ADD:
+        macaddr = params.get(constants.INIC_MAC, None)
+        if macaddr is None:
+          params[constants.INIC_MAC] = constants.VALUE_AUTO
+
+      if constants.INIC_MAC in params:
+        macaddr = params[constants.INIC_MAC]
+        if macaddr not in (constants.VALUE_AUTO, constants.VALUE_GENERATE):
+          macaddr = utils.NormalizeAndValidateMac(macaddr)
+
+        if op == constants.DDM_MODIFY and macaddr == constants.VALUE_AUTO:
+          raise errors.OpPrereqError("'auto' is not a valid MAC address when"
+                                     " modifying an existing NIC",
+                                     errors.ECODE_INVAL)
+
   def CheckArguments(self):
     if not (self.op.nics or self.op.disks or self.op.disk_template or
-            self.op.hvparams or self.op.beparams or self.op.os_name):
+            self.op.hvparams or self.op.beparams or self.op.os_name or
+            self.op.offline is not None or self.op.runtime_mem):
       raise errors.OpPrereqError("No changes submitted", errors.ECODE_INVAL)
 
     if self.op.hvparams:
       _CheckGlobalHvParams(self.op.hvparams)
 
-    # Disk validation
-    disk_addremove = 0
-    for disk_op, disk_dict in self.op.disks:
-      utils.ForceDictType(disk_dict, constants.IDISK_PARAMS_TYPES)
-      if disk_op == constants.DDM_REMOVE:
-        disk_addremove += 1
-        continue
-      elif disk_op == constants.DDM_ADD:
-        disk_addremove += 1
-      else:
-        if not isinstance(disk_op, int):
-          raise errors.OpPrereqError("Invalid disk index", errors.ECODE_INVAL)
-        if not isinstance(disk_dict, dict):
-          msg = "Invalid disk value: expected dict, got '%s'" % disk_dict
-          raise errors.OpPrereqError(msg, errors.ECODE_INVAL)
-
-      if disk_op == constants.DDM_ADD:
-        mode = disk_dict.setdefault(constants.IDISK_MODE, constants.DISK_RDWR)
-        if mode not in constants.DISK_ACCESS_SET:
-          raise errors.OpPrereqError("Invalid disk access mode '%s'" % mode,
-                                     errors.ECODE_INVAL)
-        size = disk_dict.get(constants.IDISK_SIZE, None)
-        if size is None:
-          raise errors.OpPrereqError("Required disk parameter size missing",
-                                     errors.ECODE_INVAL)
-        try:
-          size = int(size)
-        except (TypeError, ValueError), err:
-          raise errors.OpPrereqError("Invalid disk size parameter: %s" %
-                                     str(err), errors.ECODE_INVAL)
-        disk_dict[constants.IDISK_SIZE] = size
-      else:
-        # modification of disk
-        if constants.IDISK_SIZE in disk_dict:
-          raise errors.OpPrereqError("Disk size change not possible, use"
-                                     " grow-disk", errors.ECODE_INVAL)
+    self.op.disks = \
+      self._UpgradeDiskNicMods("disk", self.op.disks,
+        opcodes.OpInstanceSetParams.TestDiskModifications)
+    self.op.nics = \
+      self._UpgradeDiskNicMods("NIC", self.op.nics,
+        opcodes.OpInstanceSetParams.TestNicModifications)
 
-    if disk_addremove > 1:
-      raise errors.OpPrereqError("Only one disk add or remove operation"
-                                 " supported at a time", errors.ECODE_INVAL)
+    # Check disk modifications
+    self._CheckMods("disk", self.op.disks, constants.IDISK_PARAMS_TYPES,
+                    self._VerifyDiskModification)
 
     if self.op.disks and self.op.disk_template is not None:
       raise errors.OpPrereqError("Disk template conversion and other disk"
@@ -10695,72 +12222,29 @@ class LUInstanceSetParams(LogicalUnit):
                                  " one requires specifying a secondary node",
                                  errors.ECODE_INVAL)
 
-    # NIC validation
-    nic_addremove = 0
-    for nic_op, nic_dict in self.op.nics:
-      utils.ForceDictType(nic_dict, constants.INIC_PARAMS_TYPES)
-      if nic_op == constants.DDM_REMOVE:
-        nic_addremove += 1
-        continue
-      elif nic_op == constants.DDM_ADD:
-        nic_addremove += 1
-      else:
-        if not isinstance(nic_op, int):
-          raise errors.OpPrereqError("Invalid nic index", errors.ECODE_INVAL)
-        if not isinstance(nic_dict, dict):
-          msg = "Invalid nic value: expected dict, got '%s'" % nic_dict
-          raise errors.OpPrereqError(msg, errors.ECODE_INVAL)
-
-      # nic_dict should be a dict
-      nic_ip = nic_dict.get(constants.INIC_IP, None)
-      if nic_ip is not None:
-        if nic_ip.lower() == constants.VALUE_NONE:
-          nic_dict[constants.INIC_IP] = None
-        else:
-          if not netutils.IPAddress.IsValid(nic_ip):
-            raise errors.OpPrereqError("Invalid IP address '%s'" % nic_ip,
-                                       errors.ECODE_INVAL)
-
-      nic_bridge = nic_dict.get("bridge", None)
-      nic_link = nic_dict.get(constants.INIC_LINK, None)
-      if nic_bridge and nic_link:
-        raise errors.OpPrereqError("Cannot pass 'bridge' and 'link'"
-                                   " at the same time", errors.ECODE_INVAL)
-      elif nic_bridge and nic_bridge.lower() == constants.VALUE_NONE:
-        nic_dict["bridge"] = None
-      elif nic_link and nic_link.lower() == constants.VALUE_NONE:
-        nic_dict[constants.INIC_LINK] = None
-
-      if nic_op == constants.DDM_ADD:
-        nic_mac = nic_dict.get(constants.INIC_MAC, None)
-        if nic_mac is None:
-          nic_dict[constants.INIC_MAC] = constants.VALUE_AUTO
-
-      if constants.INIC_MAC in nic_dict:
-        nic_mac = nic_dict[constants.INIC_MAC]
-        if nic_mac not in (constants.VALUE_AUTO, constants.VALUE_GENERATE):
-          nic_mac = utils.NormalizeAndValidateMac(nic_mac)
-
-        if nic_op != constants.DDM_ADD and nic_mac == constants.VALUE_AUTO:
-          raise errors.OpPrereqError("'auto' is not a valid MAC address when"
-                                     " modifying an existing nic",
-                                     errors.ECODE_INVAL)
-
-    if nic_addremove > 1:
-      raise errors.OpPrereqError("Only one NIC add or remove operation"
-                                 " supported at a time", errors.ECODE_INVAL)
+    # Check NIC modifications
+    self._CheckMods("NIC", self.op.nics, constants.INIC_PARAMS_TYPES,
+                    self._VerifyNicModification)
 
   def ExpandNames(self):
     self._ExpandAndLockInstance()
+    # Can't even acquire node locks in shared mode as upcoming changes in
+    # Ganeti 2.6 will start to modify the node object on disk conversion
     self.needed_locks[locking.LEVEL_NODE] = []
+    self.needed_locks[locking.LEVEL_NODE_RES] = []
     self.recalculate_locks[locking.LEVEL_NODE] = constants.LOCKS_REPLACE
 
   def DeclareLocks(self, level):
+    # TODO: Acquire group lock in shared mode (disk parameters)
     if level == locking.LEVEL_NODE:
       self._LockInstancesNodes()
       if self.op.disk_template and self.op.remote_node:
         self.op.remote_node = _ExpandNodeName(self.cfg, self.op.remote_node)
         self.needed_locks[locking.LEVEL_NODE].append(self.op.remote_node)
+    elif level == locking.LEVEL_NODE_RES and self.op.disk_template:
+      # Copy node locks
+      self.needed_locks[locking.LEVEL_NODE_RES] = \
+        self.needed_locks[locking.LEVEL_NODE][:]
 
   def BuildHooksEnv(self):
     """Build hooks env.
@@ -10769,48 +12253,31 @@ class LUInstanceSetParams(LogicalUnit):
 
     """
     args = dict()
-    if constants.BE_MEMORY in self.be_new:
-      args["memory"] = self.be_new[constants.BE_MEMORY]
+    if constants.BE_MINMEM in self.be_new:
+      args["minmem"] = self.be_new[constants.BE_MINMEM]
+    if constants.BE_MAXMEM in self.be_new:
+      args["maxmem"] = self.be_new[constants.BE_MAXMEM]
     if constants.BE_VCPUS in self.be_new:
       args["vcpus"] = self.be_new[constants.BE_VCPUS]
     # TODO: export disk changes. Note: _BuildInstanceHookEnv* don't export disk
     # information at all.
-    if self.op.nics:
-      args["nics"] = []
-      nic_override = dict(self.op.nics)
-      for idx, nic in enumerate(self.instance.nics):
-        if idx in nic_override:
-          this_nic_override = nic_override[idx]
-        else:
-          this_nic_override = {}
-        if constants.INIC_IP in this_nic_override:
-          ip = this_nic_override[constants.INIC_IP]
-        else:
-          ip = nic.ip
-        if constants.INIC_MAC in this_nic_override:
-          mac = this_nic_override[constants.INIC_MAC]
-        else:
-          mac = nic.mac
-        if idx in self.nic_pnew:
-          nicparams = self.nic_pnew[idx]
-        else:
-          nicparams = self.cluster.SimpleFillNIC(nic.nicparams)
-        mode = nicparams[constants.NIC_MODE]
-        link = nicparams[constants.NIC_LINK]
-        args["nics"].append((ip, mac, mode, link))
-      if constants.DDM_ADD in nic_override:
-        ip = nic_override[constants.DDM_ADD].get(constants.INIC_IP, None)
-        mac = nic_override[constants.DDM_ADD][constants.INIC_MAC]
-        nicparams = self.nic_pnew[constants.DDM_ADD]
+
+    if self._new_nics is not None:
+      nics = []
+
+      for nic in self._new_nics:
+        nicparams = self.cluster.SimpleFillNIC(nic.nicparams)
         mode = nicparams[constants.NIC_MODE]
         link = nicparams[constants.NIC_LINK]
-        args["nics"].append((ip, mac, mode, link))
-      elif constants.DDM_REMOVE in nic_override:
-        del args["nics"][-1]
+        nics.append((nic.ip, nic.mac, mode, link))
+
+      args["nics"] = nics
 
     env = _BuildInstanceHookEnvByObject(self, self.instance, override=args)
     if self.op.disk_template:
       env["NEW_DISK_TEMPLATE"] = self.op.disk_template
+    if self.op.runtime_mem:
+      env["RUNTIME_MEMORY"] = self.op.runtime_mem
 
     return env
 
@@ -10821,6 +12288,59 @@ class LUInstanceSetParams(LogicalUnit):
     nl = [self.cfg.GetMasterNode()] + list(self.instance.all_nodes)
     return (nl, nl)
 
+  def _PrepareNicModification(self, params, private, old_ip, old_params,
+                              cluster, pnode):
+    update_params_dict = dict([(key, params[key])
+                               for key in constants.NICS_PARAMETERS
+                               if key in params])
+
+    if "bridge" in params:
+      update_params_dict[constants.NIC_LINK] = params["bridge"]
+
+    new_params = _GetUpdatedParams(old_params, update_params_dict)
+    utils.ForceDictType(new_params, constants.NICS_PARAMETER_TYPES)
+
+    new_filled_params = cluster.SimpleFillNIC(new_params)
+    objects.NIC.CheckParameterSyntax(new_filled_params)
+
+    new_mode = new_filled_params[constants.NIC_MODE]
+    if new_mode == constants.NIC_MODE_BRIDGED:
+      bridge = new_filled_params[constants.NIC_LINK]
+      msg = self.rpc.call_bridges_exist(pnode, [bridge]).fail_msg
+      if msg:
+        msg = "Error checking bridges on node '%s': %s" % (pnode, msg)
+        if self.op.force:
+          self.warn.append(msg)
+        else:
+          raise errors.OpPrereqError(msg, errors.ECODE_ENVIRON)
+
+    elif new_mode == constants.NIC_MODE_ROUTED:
+      ip = params.get(constants.INIC_IP, old_ip)
+      if ip is None:
+        raise errors.OpPrereqError("Cannot set the NIC IP address to None"
+                                   " on a routed NIC", errors.ECODE_INVAL)
+
+    if constants.INIC_MAC in params:
+      mac = params[constants.INIC_MAC]
+      if mac is None:
+        raise errors.OpPrereqError("Cannot unset the NIC MAC address",
+                                   errors.ECODE_INVAL)
+      elif mac in (constants.VALUE_AUTO, constants.VALUE_GENERATE):
+        # otherwise generate the MAC address
+        params[constants.INIC_MAC] = \
+          self.cfg.GenerateMAC(self.proc.GetECId())
+      else:
+        # or validate/reserve the current one
+        try:
+          self.cfg.ReserveMAC(mac, self.proc.GetECId())
+        except errors.ReservationError:
+          raise errors.OpPrereqError("MAC address '%s' already in use"
+                                     " in cluster" % mac,
+                                     errors.ECODE_NOTUNIQUE)
+
+    private.params = new_params
+    private.filled = new_filled_params
+
   def CheckPrereq(self):
     """Check prerequisites.
 
@@ -10835,6 +12355,12 @@ class LUInstanceSetParams(LogicalUnit):
       "Cannot retrieve locked instance %s" % self.op.instance_name
     pnode = instance.primary_node
     nodelist = list(instance.all_nodes)
+    pnode_info = self.cfg.GetNodeInfo(pnode)
+    self.diskparams = self.cfg.GetInstanceDiskParams(instance)
+
+    # Prepare disk/NIC modifications
+    self.diskmod = PrepareContainerMods(self.op.disks, None)
+    self.nicmod = PrepareContainerMods(self.op.nics, _InstNicModPrivate)
 
     # OS change
     if self.op.os_name and not self.op.force:
@@ -10844,6 +12370,9 @@ class LUInstanceSetParams(LogicalUnit):
     else:
       instance_os = instance.os
 
+    assert not (self.op.disk_template and self.op.disks), \
+      "Can't modify disk template and apply disk changes at the same time"
+
     if self.op.disk_template:
       if instance.disk_template == self.op.disk_template:
         raise errors.OpPrereqError("Instance already has disk template %s" %
@@ -10855,7 +12384,8 @@ class LUInstanceSetParams(LogicalUnit):
                                    " %s to %s" % (instance.disk_template,
                                                   self.op.disk_template),
                                    errors.ECODE_INVAL)
-      _CheckInstanceDown(self, instance, "cannot change disk template")
+      _CheckInstanceState(self, instance, INSTANCE_DOWN,
+                          msg="cannot change disk template")
       if self.op.disk_template in constants.DTS_INT_MIRROR:
         if self.op.remote_node == pnode:
           raise errors.OpPrereqError("Given new secondary node %s is the same"
@@ -10871,6 +12401,17 @@ class LUInstanceSetParams(LogicalUnit):
         required = _ComputeDiskSizePerVG(self.op.disk_template, disks)
         _CheckNodesFreeDiskPerVG(self, [self.op.remote_node], required)
 
+        snode_info = self.cfg.GetNodeInfo(self.op.remote_node)
+        snode_group = self.cfg.GetNodeGroup(snode_info.group)
+        ipolicy = _CalculateGroupIPolicy(cluster, snode_group)
+        _CheckTargetNodeIPolicy(self, ipolicy, instance, snode_info,
+                                ignore=self.op.ignore_ipolicy)
+        if pnode_info.group != snode_info.group:
+          self.LogWarning("The primary and secondary nodes are in two"
+                          " different node groups; the disk parameters"
+                          " from the first disk's node group will be"
+                          " used")
+
     # hvparams processing
     if self.op.hvparams:
       hv_type = instance.hypervisor
@@ -10881,23 +12422,54 @@ class LUInstanceSetParams(LogicalUnit):
       # local check
       hypervisor.GetHypervisor(hv_type).CheckParameterSyntax(hv_new)
       _CheckHVParams(self, nodelist, instance.hypervisor, hv_new)
-      self.hv_new = hv_new # the new actual values
+      self.hv_proposed = self.hv_new = hv_new # the new actual values
       self.hv_inst = i_hvdict # the new dict (without defaults)
     else:
+      self.hv_proposed = cluster.SimpleFillHV(instance.hypervisor, instance.os,
+                                              instance.hvparams)
       self.hv_new = self.hv_inst = {}
 
     # beparams processing
     if self.op.beparams:
       i_bedict = _GetUpdatedParams(instance.beparams, self.op.beparams,
                                    use_none=True)
+      objects.UpgradeBeParams(i_bedict)
       utils.ForceDictType(i_bedict, constants.BES_PARAMETER_TYPES)
       be_new = cluster.SimpleFillBE(i_bedict)
-      self.be_new = be_new # the new actual values
+      self.be_proposed = self.be_new = be_new # the new actual values
       self.be_inst = i_bedict # the new dict (without defaults)
     else:
       self.be_new = self.be_inst = {}
+      self.be_proposed = cluster.SimpleFillBE(instance.beparams)
     be_old = cluster.FillBE(instance)
 
+    # CPU param validation -- checking every time a parameter is
+    # changed to cover all cases where either CPU mask or vcpus have
+    # changed
+    if (constants.BE_VCPUS in self.be_proposed and
+        constants.HV_CPU_MASK in self.hv_proposed):
+      cpu_list = \
+        utils.ParseMultiCpuMask(self.hv_proposed[constants.HV_CPU_MASK])
+      # Verify mask is consistent with number of vCPUs. Can skip this
+      # test if only 1 entry in the CPU mask, which means same mask
+      # is applied to all vCPUs.
+      if (len(cpu_list) > 1 and
+          len(cpu_list) != self.be_proposed[constants.BE_VCPUS]):
+        raise errors.OpPrereqError("Number of vCPUs [%d] does not match the"
+                                   " CPU mask [%s]" %
+                                   (self.be_proposed[constants.BE_VCPUS],
+                                    self.hv_proposed[constants.HV_CPU_MASK]),
+                                   errors.ECODE_INVAL)
+
+      # Only perform this test if a new CPU mask is given
+      if constants.HV_CPU_MASK in self.hv_new:
+        # Calculate the largest CPU number requested
+        max_requested_cpu = max(map(max, cpu_list))
+        # Check that all of the instance's nodes have enough physical CPUs to
+        # satisfy the requested CPU mask
+        _CheckNodesPhysicalCPUs(self, instance.all_nodes,
+                                max_requested_cpu + 1, instance.hypervisor)
+
     # osparams processing
     if self.op.osparams:
       i_osdict = _GetUpdatedParams(instance.osparams, self.op.osparams)
@@ -10908,8 +12480,9 @@ class LUInstanceSetParams(LogicalUnit):
 
     self.warn = []
 
-    if (constants.BE_MEMORY in self.op.beparams and not self.op.force and
-        be_new[constants.BE_MEMORY] > be_old[constants.BE_MEMORY]):
+    #TODO(dynmem): do the appropriate check involving MINMEM
+    if (constants.BE_MAXMEM in self.op.beparams and not self.op.force and
+        be_new[constants.BE_MAXMEM] > be_old[constants.BE_MAXMEM]):
       mem_check_list = [pnode]
       if be_new[constants.BE_AUTO_BALANCE]:
         # either we changed auto_balance to yes or it was from before
@@ -10917,34 +12490,39 @@ class LUInstanceSetParams(LogicalUnit):
       instance_info = self.rpc.call_instance_info(pnode, instance.name,
                                                   instance.hypervisor)
       nodeinfo = self.rpc.call_node_info(mem_check_list, None,
-                                         instance.hypervisor)
+                                         [instance.hypervisor])
       pninfo = nodeinfo[pnode]
       msg = pninfo.fail_msg
       if msg:
         # Assume the primary node is unreachable and go ahead
         self.warn.append("Can't get info from primary node %s: %s" %
                          (pnode, msg))
-      elif not isinstance(pninfo.payload.get("memory_free", None), int):
-        self.warn.append("Node data from primary node %s doesn't contain"
-                         " free memory information" % pnode)
-      elif instance_info.fail_msg:
-        self.warn.append("Can't get instance runtime information: %s" %
-                        instance_info.fail_msg)
       else:
-        if instance_info.payload:
-          current_mem = int(instance_info.payload["memory"])
+        (_, _, (pnhvinfo, )) = pninfo.payload
+        if not isinstance(pnhvinfo.get("memory_free", None), int):
+          self.warn.append("Node data from primary node %s doesn't contain"
+                           " free memory information" % pnode)
+        elif instance_info.fail_msg:
+          self.warn.append("Can't get instance runtime information: %s" %
+                          instance_info.fail_msg)
         else:
-          # Assume instance not running
-          # (there is a slight race condition here, but it's not very probable,
-          # and we have no other way to check)
-          current_mem = 0
-        miss_mem = (be_new[constants.BE_MEMORY] - current_mem -
-                    pninfo.payload["memory_free"])
-        if miss_mem > 0:
-          raise errors.OpPrereqError("This change will prevent the instance"
-                                     " from starting, due to %d MB of memory"
-                                     " missing on its primary node" % miss_mem,
-                                     errors.ECODE_NORES)
+          if instance_info.payload:
+            current_mem = int(instance_info.payload["memory"])
+          else:
+            # Assume instance not running
+            # (there is a slight race condition here, but it's not very
+            # probable, and we have no other way to check)
+            # TODO: Describe race condition
+            current_mem = 0
+          #TODO(dynmem): do the appropriate check involving MINMEM
+          miss_mem = (be_new[constants.BE_MAXMEM] - current_mem -
+                      pnhvinfo["memory_free"])
+          if miss_mem > 0:
+            raise errors.OpPrereqError("This change will prevent the instance"
+                                       " from starting, due to %d MB of memory"
+                                       " missing on its primary node" %
+                                       miss_mem,
+                                       errors.ECODE_NORES)
 
       if be_new[constants.BE_AUTO_BALANCE]:
         for node, nres in nodeinfo.items():
@@ -10952,119 +12530,93 @@ class LUInstanceSetParams(LogicalUnit):
             continue
           nres.Raise("Can't get info from secondary node %s" % node,
                      prereq=True, ecode=errors.ECODE_STATE)
-          if not isinstance(nres.payload.get("memory_free", None), int):
+          (_, _, (nhvinfo, )) = nres.payload
+          if not isinstance(nhvinfo.get("memory_free", None), int):
             raise errors.OpPrereqError("Secondary node %s didn't return free"
                                        " memory information" % node,
                                        errors.ECODE_STATE)
-          elif be_new[constants.BE_MEMORY] > nres.payload["memory_free"]:
+          #TODO(dynmem): do the appropriate check involving MINMEM
+          elif be_new[constants.BE_MAXMEM] > nhvinfo["memory_free"]:
             raise errors.OpPrereqError("This change will prevent the instance"
                                        " from failover to its secondary node"
                                        " %s, due to not enough memory" % node,
                                        errors.ECODE_STATE)
 
-    # NIC processing
-    self.nic_pnew = {}
-    self.nic_pinst = {}
-    for nic_op, nic_dict in self.op.nics:
-      if nic_op == constants.DDM_REMOVE:
-        if not instance.nics:
-          raise errors.OpPrereqError("Instance has no NICs, cannot remove",
-                                     errors.ECODE_INVAL)
-        continue
-      if nic_op != constants.DDM_ADD:
-        # an existing nic
-        if not instance.nics:
-          raise errors.OpPrereqError("Invalid NIC index %s, instance has"
-                                     " no NICs" % nic_op,
-                                     errors.ECODE_INVAL)
-        if nic_op < 0 or nic_op >= len(instance.nics):
-          raise errors.OpPrereqError("Invalid NIC index %s, valid values"
-                                     " are 0 to %d" %
-                                     (nic_op, len(instance.nics) - 1),
-                                     errors.ECODE_INVAL)
-        old_nic_params = instance.nics[nic_op].nicparams
-        old_nic_ip = instance.nics[nic_op].ip
-      else:
-        old_nic_params = {}
-        old_nic_ip = None
-
-      update_params_dict = dict([(key, nic_dict[key])
-                                 for key in constants.NICS_PARAMETERS
-                                 if key in nic_dict])
-
-      if "bridge" in nic_dict:
-        update_params_dict[constants.NIC_LINK] = nic_dict["bridge"]
-
-      new_nic_params = _GetUpdatedParams(old_nic_params,
-                                         update_params_dict)
-      utils.ForceDictType(new_nic_params, constants.NICS_PARAMETER_TYPES)
-      new_filled_nic_params = cluster.SimpleFillNIC(new_nic_params)
-      objects.NIC.CheckParameterSyntax(new_filled_nic_params)
-      self.nic_pinst[nic_op] = new_nic_params
-      self.nic_pnew[nic_op] = new_filled_nic_params
-      new_nic_mode = new_filled_nic_params[constants.NIC_MODE]
-
-      if new_nic_mode == constants.NIC_MODE_BRIDGED:
-        nic_bridge = new_filled_nic_params[constants.NIC_LINK]
-        msg = self.rpc.call_bridges_exist(pnode, [nic_bridge]).fail_msg
-        if msg:
-          msg = "Error checking bridges on node %s: %s" % (pnode, msg)
-          if self.op.force:
-            self.warn.append(msg)
-          else:
-            raise errors.OpPrereqError(msg, errors.ECODE_ENVIRON)
-      if new_nic_mode == constants.NIC_MODE_ROUTED:
-        if constants.INIC_IP in nic_dict:
-          nic_ip = nic_dict[constants.INIC_IP]
-        else:
-          nic_ip = old_nic_ip
-        if nic_ip is None:
-          raise errors.OpPrereqError("Cannot set the nic ip to None"
-                                     " on a routed nic", errors.ECODE_INVAL)
-      if constants.INIC_MAC in nic_dict:
-        nic_mac = nic_dict[constants.INIC_MAC]
-        if nic_mac is None:
-          raise errors.OpPrereqError("Cannot set the nic mac to None",
-                                     errors.ECODE_INVAL)
-        elif nic_mac in (constants.VALUE_AUTO, constants.VALUE_GENERATE):
-          # otherwise generate the mac
-          nic_dict[constants.INIC_MAC] = \
-            self.cfg.GenerateMAC(self.proc.GetECId())
-        else:
-          # or validate/reserve the current one
-          try:
-            self.cfg.ReserveMAC(nic_mac, self.proc.GetECId())
-          except errors.ReservationError:
-            raise errors.OpPrereqError("MAC address %s already in use"
-                                       " in cluster" % nic_mac,
-                                       errors.ECODE_NOTUNIQUE)
+    if self.op.runtime_mem:
+      remote_info = self.rpc.call_instance_info(instance.primary_node,
+                                                instance.name,
+                                                instance.hypervisor)
+      remote_info.Raise("Error checking node %s" % instance.primary_node)
+      if not remote_info.payload: # not running already
+        raise errors.OpPrereqError("Instance %s is not running" % instance.name,
+                                   errors.ECODE_STATE)
+
+      current_memory = remote_info.payload["memory"]
+      if (not self.op.force and
+           (self.op.runtime_mem > self.be_proposed[constants.BE_MAXMEM] or
+            self.op.runtime_mem < self.be_proposed[constants.BE_MINMEM])):
+        raise errors.OpPrereqError("Instance %s must have memory between %d"
+                                   " and %d MB of memory unless --force is"
+                                   " given" % (instance.name,
+                                    self.be_proposed[constants.BE_MINMEM],
+                                    self.be_proposed[constants.BE_MAXMEM]),
+                                   errors.ECODE_INVAL)
+
+      if self.op.runtime_mem > current_memory:
+        _CheckNodeFreeMemory(self, instance.primary_node,
+                             "ballooning memory for instance %s" %
+                             instance.name,
+                             self.op.memory - current_memory,
+                             instance.hypervisor)
 
-    # DISK processing
     if self.op.disks and instance.disk_template == constants.DT_DISKLESS:
       raise errors.OpPrereqError("Disk operations not supported for"
                                  " diskless instances",
                                  errors.ECODE_INVAL)
-    for disk_op, _ in self.op.disks:
-      if disk_op == constants.DDM_REMOVE:
-        if len(instance.disks) == 1:
-          raise errors.OpPrereqError("Cannot remove the last disk of"
-                                     " an instance", errors.ECODE_INVAL)
-        _CheckInstanceDown(self, instance, "cannot remove disks")
-
-      if (disk_op == constants.DDM_ADD and
-          len(instance.disks) >= constants.MAX_DISKS):
-        raise errors.OpPrereqError("Instance has too many disks (%d), cannot"
-                                   " add more" % constants.MAX_DISKS,
-                                   errors.ECODE_STATE)
-      if disk_op not in (constants.DDM_ADD, constants.DDM_REMOVE):
-        # an existing disk
-        if disk_op < 0 or disk_op >= len(instance.disks):
-          raise errors.OpPrereqError("Invalid disk index %s, valid values"
-                                     " are 0 to %d" %
-                                     (disk_op, len(instance.disks)),
-                                     errors.ECODE_INVAL)
 
-    return
+    def _PrepareNicCreate(_, params, private):
+      self._PrepareNicModification(params, private, None, {}, cluster, pnode)
+      return (None, None)
+
+    def _PrepareNicMod(_, nic, params, private):
+      self._PrepareNicModification(params, private, nic.ip,
+                                   nic.nicparams, cluster, pnode)
+      return None
+
+    # Verify NIC changes (operating on copy)
+    nics = instance.nics[:]
+    ApplyContainerMods("NIC", nics, None, self.nicmod,
+                       _PrepareNicCreate, _PrepareNicMod, None)
+    if len(nics) > constants.MAX_NICS:
+      raise errors.OpPrereqError("Instance has too many network interfaces"
+                                 " (%d), cannot add more" % constants.MAX_NICS,
+                                 errors.ECODE_STATE)
+
+    # Verify disk changes (operating on a copy)
+    disks = instance.disks[:]
+    ApplyContainerMods("disk", disks, None, self.diskmod, None, None, None)
+    if len(disks) > constants.MAX_DISKS:
+      raise errors.OpPrereqError("Instance has too many disks (%d), cannot add"
+                                 " more" % constants.MAX_DISKS,
+                                 errors.ECODE_STATE)
+
+    if self.op.offline is not None:
+      if self.op.offline:
+        msg = "can't change to offline"
+      else:
+        msg = "can't change to online"
+      _CheckInstanceState(self, instance, CAN_CHANGE_INSTANCE_OFFLINE, msg=msg)
+
+    # Pre-compute NIC changes (necessary to use result in hooks)
+    self._nic_chgdesc = []
+    if self.nicmod:
+      # Operate on copies as this is still in prereq
+      nics = [nic.Copy() for nic in instance.nics]
+      ApplyContainerMods("NIC", nics, self._nic_chgdesc, self.nicmod,
+                         self._CreateNewNic, self._ApplyNicMods, None)
+      self._new_nics = nics
+    else:
+      self._new_nics = None
 
   def _ConvertPlainToDrbd(self, feedback_fn):
     """Converts an instance from plain to drbd.
@@ -11075,17 +12627,22 @@ class LUInstanceSetParams(LogicalUnit):
     pnode = instance.primary_node
     snode = self.op.remote_node
 
+    assert instance.disk_template == constants.DT_PLAIN
+
     # create a fake disk info for _GenerateDiskTemplate
     disk_info = [{constants.IDISK_SIZE: d.size, constants.IDISK_MODE: d.mode,
                   constants.IDISK_VG: d.logical_id[0]}
                  for d in instance.disks]
     new_disks = _GenerateDiskTemplate(self, self.op.disk_template,
                                       instance.name, pnode, [snode],
-                                      disk_info, None, None, 0, feedback_fn)
+                                      disk_info, None, None, 0, feedback_fn,
+                                      self.diskparams)
+    anno_disks = rpc.AnnotateDiskParams(constants.DT_DRBD8, new_disks,
+                                        self.diskparams)
     info = _GetInstanceInfoText(instance)
-    feedback_fn("Creating aditional volumes...")
+    feedback_fn("Creating additional volumes...")
     # first, create the missing data and meta devices
-    for disk in new_disks:
+    for disk in anno_disks:
       # unfortunately this is... not too nice
       _CreateSingleBlockDev(self, pnode, instance, disk.children[1],
                             info, True)
@@ -11101,7 +12658,7 @@ class LUInstanceSetParams(LogicalUnit):
 
     feedback_fn("Initializing DRBD devices...")
     # all child devices are in place, we can now create the DRBD devices
-    for disk in new_disks:
+    for disk in anno_disks:
       for node in [pnode, snode]:
         f_create = node == pnode
         _CreateSingleBlockDev(self, node, instance, disk, info, f_create)
@@ -11111,6 +12668,9 @@ class LUInstanceSetParams(LogicalUnit):
     instance.disks = new_disks
     self.cfg.Update(instance, feedback_fn)
 
+    # Release node locks while waiting for sync
+    _ReleaseLocks(self, locking.LEVEL_NODE)
+
     # disks are created, waiting for sync
     disk_abort = not _WaitForSync(self, instance,
                                   oneshot=not self.op.wait_for_sync)
@@ -11118,18 +12678,23 @@ class LUInstanceSetParams(LogicalUnit):
       raise errors.OpExecError("There are some degraded disks for"
                                " this instance, please cleanup manually")
 
+    # Node resource locks will be released by caller
+
   def _ConvertDrbdToPlain(self, feedback_fn):
     """Converts an instance from drbd to plain.
 
     """
     instance = self.instance
+
     assert len(instance.secondary_nodes) == 1
+    assert instance.disk_template == constants.DT_DRBD8
+
     pnode = instance.primary_node
     snode = instance.secondary_nodes[0]
     feedback_fn("Converting template to plain")
 
-    old_disks = instance.disks
-    new_disks = [d.children[0] for d in old_disks]
+    old_disks = _AnnotateDiskParams(instance, instance.disks, self.cfg)
+    new_disks = [d.children[0] for d in instance.disks]
 
     # copy over size and mode
     for parent, child in zip(old_disks, new_disks):
@@ -11147,6 +12712,9 @@ class LUInstanceSetParams(LogicalUnit):
     instance.disk_template = constants.DT_PLAIN
     self.cfg.Update(instance, feedback_fn)
 
+    # Release locks in case removing disks takes a while
+    _ReleaseLocks(self, locking.LEVEL_NODE)
+
     feedback_fn("Removing volumes on the secondary node...")
     for disk in old_disks:
       self.cfg.SetDiskID(disk, snode)
@@ -11164,6 +12732,106 @@ class LUInstanceSetParams(LogicalUnit):
         self.LogWarning("Could not remove metadata for disk %d on node %s,"
                         " continuing anyway: %s", idx, pnode, msg)
 
+  def _CreateNewDisk(self, idx, params, _):
+    """Creates a new disk.
+
+    """
+    instance = self.instance
+
+    # add a new disk
+    if instance.disk_template in constants.DTS_FILEBASED:
+      (file_driver, file_path) = instance.disks[0].logical_id
+      file_path = os.path.dirname(file_path)
+    else:
+      file_driver = file_path = None
+
+    disk = \
+      _GenerateDiskTemplate(self, instance.disk_template, instance.name,
+                            instance.primary_node, instance.secondary_nodes,
+                            [params], file_path, file_driver, idx,
+                            self.Log, self.diskparams)[0]
+
+    info = _GetInstanceInfoText(instance)
+
+    logging.info("Creating volume %s for instance %s",
+                 disk.iv_name, instance.name)
+    # Note: this needs to be kept in sync with _CreateDisks
+    #HARDCODE
+    for node in instance.all_nodes:
+      f_create = (node == instance.primary_node)
+      try:
+        _CreateBlockDev(self, node, instance, disk, f_create, info, f_create)
+      except errors.OpExecError, err:
+        self.LogWarning("Failed to create volume %s (%s) on node '%s': %s",
+                        disk.iv_name, disk, node, err)
+
+    return (disk, [
+      ("disk/%d" % idx, "add:size=%s,mode=%s" % (disk.size, disk.mode)),
+      ])
+
+  @staticmethod
+  def _ModifyDisk(idx, disk, params, _):
+    """Modifies a disk.
+
+    """
+    disk.mode = params[constants.IDISK_MODE]
+
+    return [
+      ("disk.mode/%d" % idx, disk.mode),
+      ]
+
+  def _RemoveDisk(self, idx, root, _):
+    """Removes a disk.
+
+    """
+    (anno_disk,) = _AnnotateDiskParams(self.instance, [root], self.cfg)
+    for node, disk in anno_disk.ComputeNodeTree(self.instance.primary_node):
+      self.cfg.SetDiskID(disk, node)
+      msg = self.rpc.call_blockdev_remove(node, disk).fail_msg
+      if msg:
+        self.LogWarning("Could not remove disk/%d on node '%s': %s,"
+                        " continuing anyway", idx, node, msg)
+
+    # if this is a DRBD disk, return its port to the pool
+    if root.dev_type in constants.LDS_DRBD:
+      self.cfg.AddTcpUdpPort(root.logical_id[2])
+
+  @staticmethod
+  def _CreateNewNic(idx, params, private):
+    """Creates data structure for a new network interface.
+
+    """
+    mac = params[constants.INIC_MAC]
+    ip = params.get(constants.INIC_IP, None)
+    nicparams = private.params
+
+    return (objects.NIC(mac=mac, ip=ip, nicparams=nicparams), [
+      ("nic.%d" % idx,
+       "add:mac=%s,ip=%s,mode=%s,link=%s" %
+       (mac, ip, private.filled[constants.NIC_MODE],
+       private.filled[constants.NIC_LINK])),
+      ])
+
+  @staticmethod
+  def _ApplyNicMods(idx, nic, params, private):
+    """Modifies a network interface.
+
+    """
+    changes = []
+
+    for key in [constants.INIC_MAC, constants.INIC_IP]:
+      if key in params:
+        changes.append(("nic.%s/%d" % (key, idx), params[key]))
+        setattr(nic, key, params[key])
+
+    if private.params:
+      nic.nicparams = private.params
+
+      for (key, val) in params.items():
+        changes.append(("nic.%s/%d" % (key, idx), val))
+
+    return changes
+
   def Exec(self, feedback_fn):
     """Modifies an instance.
 
@@ -11172,71 +12840,41 @@ class LUInstanceSetParams(LogicalUnit):
     """
     # Process here the warnings from CheckPrereq, as we don't have a
     # feedback_fn there.
+    # TODO: Replace with self.LogWarning
     for warn in self.warn:
       feedback_fn("WARNING: %s" % warn)
 
+    assert ((self.op.disk_template is None) ^
+            bool(self.owned_locks(locking.LEVEL_NODE_RES))), \
+      "Not owning any node resource locks"
+
     result = []
     instance = self.instance
-    # disk changes
-    for disk_op, disk_dict in self.op.disks:
-      if disk_op == constants.DDM_REMOVE:
-        # remove the last disk
-        device = instance.disks.pop()
-        device_idx = len(instance.disks)
-        for node, disk in device.ComputeNodeTree(instance.primary_node):
-          self.cfg.SetDiskID(disk, node)
-          msg = self.rpc.call_blockdev_remove(node, disk).fail_msg
-          if msg:
-            self.LogWarning("Could not remove disk/%d on node %s: %s,"
-                            " continuing anyway", device_idx, node, msg)
-        result.append(("disk/%d" % device_idx, "remove"))
-
-        # if this is a DRBD disk, return its port to the pool
-        if device.dev_type in constants.LDS_DRBD:
-          tcp_port = device.logical_id[2]
-          self.cfg.AddTcpUdpPort(tcp_port)
-      elif disk_op == constants.DDM_ADD:
-        # add a new disk
-        if instance.disk_template in (constants.DT_FILE,
-                                        constants.DT_SHARED_FILE):
-          file_driver, file_path = instance.disks[0].logical_id
-          file_path = os.path.dirname(file_path)
-        else:
-          file_driver = file_path = None
-        disk_idx_base = len(instance.disks)
-        new_disk = _GenerateDiskTemplate(self,
-                                         instance.disk_template,
-                                         instance.name, instance.primary_node,
-                                         instance.secondary_nodes,
-                                         [disk_dict],
-                                         file_path,
-                                         file_driver,
-                                         disk_idx_base, feedback_fn)[0]
-        instance.disks.append(new_disk)
-        info = _GetInstanceInfoText(instance)
-
-        logging.info("Creating volume %s for instance %s",
-                     new_disk.iv_name, instance.name)
-        # Note: this needs to be kept in sync with _CreateDisks
-        #HARDCODE
-        for node in instance.all_nodes:
-          f_create = node == instance.primary_node
-          try:
-            _CreateBlockDev(self, node, instance, new_disk,
-                            f_create, info, f_create)
-          except errors.OpExecError, err:
-            self.LogWarning("Failed to create volume %s (%s) on"
-                            " node %s: %s",
-                            new_disk.iv_name, new_disk, node, err)
-        result.append(("disk/%d" % disk_idx_base, "add:size=%s,mode=%s" %
-                       (new_disk.size, new_disk.mode)))
-      else:
-        # change a given disk
-        instance.disks[disk_op].mode = disk_dict[constants.IDISK_MODE]
-        result.append(("disk.mode/%d" % disk_op,
-                       disk_dict[constants.IDISK_MODE]))
+
+    # runtime memory
+    if self.op.runtime_mem:
+      rpcres = self.rpc.call_instance_balloon_memory(instance.primary_node,
+                                                     instance,
+                                                     self.op.runtime_mem)
+      rpcres.Raise("Cannot modify instance runtime memory")
+      result.append(("runtime_memory", self.op.runtime_mem))
+
+    # Apply disk changes
+    ApplyContainerMods("disk", instance.disks, result, self.diskmod,
+                       self._CreateNewDisk, self._ModifyDisk, self._RemoveDisk)
+    _UpdateIvNames(0, instance.disks)
 
     if self.op.disk_template:
+      if __debug__:
+        check_nodes = set(instance.all_nodes)
+        if self.op.remote_node:
+          check_nodes.add(self.op.remote_node)
+        for level in [locking.LEVEL_NODE, locking.LEVEL_NODE_RES]:
+          owned = self.owned_locks(level)
+          assert not (check_nodes - owned), \
+            ("Not owning the correct locks, owning %r, expected at least %r" %
+             (owned, check_nodes))
+
       r_shut = _ShutdownInstanceDisks(self, instance)
       if not r_shut:
         raise errors.OpExecError("Cannot shutdown instance disks, unable to"
@@ -11249,33 +12887,19 @@ class LUInstanceSetParams(LogicalUnit):
         raise
       result.append(("disk_template", self.op.disk_template))
 
-    # NIC changes
-    for nic_op, nic_dict in self.op.nics:
-      if nic_op == constants.DDM_REMOVE:
-        # remove the last nic
-        del instance.nics[-1]
-        result.append(("nic.%d" % len(instance.nics), "remove"))
-      elif nic_op == constants.DDM_ADD:
-        # mac and bridge should be set, by now
-        mac = nic_dict[constants.INIC_MAC]
-        ip = nic_dict.get(constants.INIC_IP, None)
-        nicparams = self.nic_pinst[constants.DDM_ADD]
-        new_nic = objects.NIC(mac=mac, ip=ip, nicparams=nicparams)
-        instance.nics.append(new_nic)
-        result.append(("nic.%d" % (len(instance.nics) - 1),
-                       "add:mac=%s,ip=%s,mode=%s,link=%s" %
-                       (new_nic.mac, new_nic.ip,
-                        self.nic_pnew[constants.DDM_ADD][constants.NIC_MODE],
-                        self.nic_pnew[constants.DDM_ADD][constants.NIC_LINK]
-                       )))
-      else:
-        for key in (constants.INIC_MAC, constants.INIC_IP):
-          if key in nic_dict:
-            setattr(instance.nics[nic_op], key, nic_dict[key])
-        if nic_op in self.nic_pinst:
-          instance.nics[nic_op].nicparams = self.nic_pinst[nic_op]
-        for key, val in nic_dict.iteritems():
-          result.append(("nic.%s/%d" % (key, nic_op), val))
+      assert instance.disk_template == self.op.disk_template, \
+        ("Expected disk template '%s', found '%s'" %
+         (self.op.disk_template, instance.disk_template))
+
+    # Release node and resource locks if there are any (they might already have
+    # been released during disk conversion)
+    _ReleaseLocks(self, locking.LEVEL_NODE)
+    _ReleaseLocks(self, locking.LEVEL_NODE_RES)
+
+    # Apply NIC changes
+    if self._new_nics is not None:
+      instance.nics = self._new_nics
+      result.extend(self._nic_chgdesc)
 
     # hvparams changes
     if self.op.hvparams:
@@ -11299,8 +12923,24 @@ class LUInstanceSetParams(LogicalUnit):
       for key, val in self.op.osparams.iteritems():
         result.append(("os/%s" % key, val))
 
+    if self.op.offline is None:
+      # Ignore
+      pass
+    elif self.op.offline:
+      # Mark instance as offline
+      self.cfg.MarkInstanceOffline(instance.name)
+      result.append(("admin_state", constants.ADMINST_OFFLINE))
+    else:
+      # Mark instance as online, but stopped
+      self.cfg.MarkInstanceDown(instance.name)
+      result.append(("admin_state", constants.ADMINST_DOWN))
+
     self.cfg.Update(instance, feedback_fn)
 
+    assert not (self.owned_locks(locking.LEVEL_NODE_RES) or
+                self.owned_locks(locking.LEVEL_NODE)), \
+      "All node locks should have been released by now"
+
     return result
 
   _DISK_CONVERSIONS = {
@@ -11456,32 +13096,74 @@ class LUBackupQuery(NoHooksLU):
   """
   REQ_BGL = False
 
+  def CheckArguments(self):
+    self.expq = _ExportQuery(qlang.MakeSimpleFilter("node", self.op.nodes),
+                             ["node", "export"], self.op.use_locking)
+
   def ExpandNames(self):
-    self.needed_locks = {}
-    self.share_locks[locking.LEVEL_NODE] = 1
-    if not self.op.nodes:
-      self.needed_locks[locking.LEVEL_NODE] = locking.ALL_SET
-    else:
-      self.needed_locks[locking.LEVEL_NODE] = \
-        _GetWantedNodes(self, self.op.nodes)
+    self.expq.ExpandNames(self)
+
+  def DeclareLocks(self, level):
+    self.expq.DeclareLocks(self, level)
 
   def Exec(self, feedback_fn):
-    """Compute the list of all the exported system images.
+    result = {}
 
-    @rtype: dict
-    @return: a dictionary with the structure node->(export-list)
-        where export-list is a list of the instances exported on
-        that node.
+    for (node, expname) in self.expq.OldStyleQuery(self):
+      if expname is None:
+        result[node] = False
+      else:
+        result.setdefault(node, []).append(expname)
+
+    return result
+
+
+class _ExportQuery(_QueryBase):
+  FIELDS = query.EXPORT_FIELDS
+
+  #: The node name is not a unique key for this query
+  SORT_FIELD = "node"
+
+  def ExpandNames(self, lu):
+    lu.needed_locks = {}
+
+    # The following variables interact with _QueryBase._GetNames
+    if self.names:
+      self.wanted = _GetWantedNodes(lu, self.names)
+    else:
+      self.wanted = locking.ALL_SET
+
+    self.do_locking = self.use_locking
+
+    if self.do_locking:
+      lu.share_locks = _ShareAll()
+      lu.needed_locks = {
+        locking.LEVEL_NODE: self.wanted,
+        }
+
+  def DeclareLocks(self, lu, level):
+    pass
+
+  def _GetQueryData(self, lu):
+    """Computes the list of nodes and their attributes.
 
     """
-    self.nodes = self.owned_locks(locking.LEVEL_NODE)
-    rpcresult = self.rpc.call_export_list(self.nodes)
-    result = {}
-    for node in rpcresult:
-      if rpcresult[node].fail_msg:
-        result[node] = False
+    # Locking is not used
+    # TODO
+    assert not (compat.any(lu.glm.is_owned(level)
+                           for level in locking.LEVELS
+                           if level != locking.LEVEL_CLUSTER) or
+                self.do_locking or self.use_locking)
+
+    nodes = self._GetNames(lu, lu.cfg.GetNodeList(), locking.LEVEL_NODE)
+
+    result = []
+
+    for (node, nres) in lu.rpc.call_export_list(nodes).items():
+      if nres.fail_msg:
+        result.append((node, None))
       else:
-        result[node] = rpcresult[node].payload
+        result.extend((node, expname) for expname in nres.payload)
 
     return result
 
@@ -11623,7 +13305,8 @@ class LUBackupExport(LogicalUnit):
           "Cannot retrieve locked instance %s" % self.op.instance_name
     _CheckNodeOnline(self, self.instance.primary_node)
 
-    if (self.op.remove_instance and self.instance.admin_up and
+    if (self.op.remove_instance and
+        self.instance.admin_state == constants.ADMINST_UP and
         not self.op.shutdown):
       raise errors.OpPrereqError("Can not remove instance without shutting it"
                                  " down before")
@@ -11753,7 +13436,7 @@ class LUBackupExport(LogicalUnit):
     for disk in instance.disks:
       self.cfg.SetDiskID(disk, src_node)
 
-    activate_disks = (not instance.admin_up)
+    activate_disks = (instance.admin_state != constants.ADMINST_UP)
 
     if activate_disks:
       # Activate the instance disks if we'exporting a stopped instance
@@ -11766,12 +13449,13 @@ class LUBackupExport(LogicalUnit):
 
       helper.CreateSnapshots()
       try:
-        if (self.op.shutdown and instance.admin_up and
+        if (self.op.shutdown and
+            instance.admin_state == constants.ADMINST_UP and
             not self.op.remove_instance):
           assert not activate_disks
           feedback_fn("Starting instance %s" % instance.name)
-          result = self.rpc.call_instance_start(src_node, instance,
-                                                None, None, False)
+          result = self.rpc.call_instance_start(src_node,
+                                                (instance, None, None), False)
           msg = result.fail_msg
           if msg:
             feedback_fn("Failed to start instance: %s" % msg)
@@ -11915,6 +13599,39 @@ class LUGroupAdd(LogicalUnit):
     if self.op.ndparams:
       utils.ForceDictType(self.op.ndparams, constants.NDS_PARAMETER_TYPES)
 
+    if self.op.hv_state:
+      self.new_hv_state = _MergeAndVerifyHvState(self.op.hv_state, None)
+    else:
+      self.new_hv_state = None
+
+    if self.op.disk_state:
+      self.new_disk_state = _MergeAndVerifyDiskState(self.op.disk_state, None)
+    else:
+      self.new_disk_state = None
+
+    if self.op.diskparams:
+      for templ in constants.DISK_TEMPLATES:
+        if templ in self.op.diskparams:
+          utils.ForceDictType(self.op.diskparams[templ],
+                              constants.DISK_DT_TYPES)
+      self.new_diskparams = self.op.diskparams
+      try:
+        utils.VerifyDictOptions(self.new_diskparams, constants.DISK_DT_DEFAULTS)
+      except errors.OpPrereqError, err:
+        raise errors.OpPrereqError("While verify diskparams options: %s" % err,
+                                   errors.ECODE_INVAL)
+    else:
+      self.new_diskparams = {}
+
+    if self.op.ipolicy:
+      cluster = self.cfg.GetClusterInfo()
+      full_ipolicy = cluster.SimpleFillIPolicy(self.op.ipolicy)
+      try:
+        objects.InstancePolicy.CheckParameterSyntax(full_ipolicy, False)
+      except errors.ConfigurationError, err:
+        raise errors.OpPrereqError("Invalid instance policy: %s" % err,
+                                   errors.ECODE_INVAL)
+
   def BuildHooksEnv(self):
     """Build hooks env.
 
@@ -11937,7 +13654,11 @@ class LUGroupAdd(LogicalUnit):
     group_obj = objects.NodeGroup(name=self.op.group_name, members=[],
                                   uuid=self.group_uuid,
                                   alloc_policy=self.op.alloc_policy,
-                                  ndparams=self.op.ndparams)
+                                  ndparams=self.op.ndparams,
+                                  diskparams=self.new_diskparams,
+                                  ipolicy=self.op.ipolicy,
+                                  hv_state_static=self.new_hv_state,
+                                  disk_state_static=self.new_disk_state)
 
     self.cfg.AddNodeGroup(group_obj, self.proc.GetECId(), check_uuid=False)
     del self.remove_locks[locking.LEVEL_NODEGROUP]
@@ -12083,6 +13804,7 @@ class _GroupQuery(_QueryBase):
     lu.needed_locks = {}
 
     self._all_groups = lu.cfg.GetAllNodeGroupsInfo()
+    self._cluster = lu.cfg.GetClusterInfo()
     name_to_uuid = dict((g.name, g.uuid) for g in self._all_groups.values())
 
     if not self.names:
@@ -12148,9 +13870,11 @@ class _GroupQuery(_QueryBase):
           # Do not pass on node information if it was not requested.
           group_to_nodes = None
 
-    return query.GroupQueryData([self._all_groups[uuid]
+    return query.GroupQueryData(self._cluster,
+                                [self._all_groups[uuid]
                                  for uuid in self.wanted],
-                                group_to_nodes, group_to_instances)
+                                group_to_nodes, group_to_instances,
+                                query.GQ_DISKPARAMS in self.requested_data)
 
 
 class LUGroupQuery(NoHooksLU):
@@ -12184,7 +13908,11 @@ class LUGroupSetParams(LogicalUnit):
   def CheckArguments(self):
     all_changes = [
       self.op.ndparams,
+      self.op.diskparams,
       self.op.alloc_policy,
+      self.op.hv_state,
+      self.op.disk_state,
+      self.op.ipolicy,
       ]
 
     if all_changes.count(None) == len(all_changes):
@@ -12196,14 +13924,41 @@ class LUGroupSetParams(LogicalUnit):
     self.group_uuid = self.cfg.LookupNodeGroup(self.op.group_name)
 
     self.needed_locks = {
+      locking.LEVEL_INSTANCE: [],
       locking.LEVEL_NODEGROUP: [self.group_uuid],
       }
 
+    self.share_locks[locking.LEVEL_INSTANCE] = 1
+
+  def DeclareLocks(self, level):
+    if level == locking.LEVEL_INSTANCE:
+      assert not self.needed_locks[locking.LEVEL_INSTANCE]
+
+      # Lock instances optimistically, needs verification once group lock has
+      # been acquired
+      self.needed_locks[locking.LEVEL_INSTANCE] = \
+          self.cfg.GetNodeGroupInstances(self.group_uuid)
+
+  @staticmethod
+  def _UpdateAndVerifyDiskParams(old, new):
+    """Updates and verifies disk parameters.
+
+    """
+    new_params = _GetUpdatedParams(old, new)
+    utils.ForceDictType(new_params, constants.DISK_DT_TYPES)
+    return new_params
+
   def CheckPrereq(self):
     """Check prerequisites.
 
     """
+    owned_instances = frozenset(self.owned_locks(locking.LEVEL_INSTANCE))
+
+    # Check if locked instances are still correct
+    _CheckNodeGroupInstances(self.cfg, self.group_uuid, owned_instances)
+
     self.group = self.cfg.GetNodeGroup(self.group_uuid)
+    cluster = self.cfg.GetClusterInfo()
 
     if self.group is None:
       raise errors.OpExecError("Could not retrieve group '%s' (UUID: %s)" %
@@ -12214,6 +13969,51 @@ class LUGroupSetParams(LogicalUnit):
       utils.ForceDictType(self.op.ndparams, constants.NDS_PARAMETER_TYPES)
       self.new_ndparams = new_ndparams
 
+    if self.op.diskparams:
+      diskparams = self.group.diskparams
+      uavdp = self._UpdateAndVerifyDiskParams
+      # For each disktemplate subdict update and verify the values
+      new_diskparams = dict((dt,
+                             uavdp(diskparams.get(dt, {}),
+                                   self.op.diskparams[dt]))
+                            for dt in constants.DISK_TEMPLATES
+                            if dt in self.op.diskparams)
+      # As we've all subdicts of diskparams ready, lets merge the actual
+      # dict with all updated subdicts
+      self.new_diskparams = objects.FillDict(diskparams, new_diskparams)
+      try:
+        utils.VerifyDictOptions(self.new_diskparams, constants.DISK_DT_DEFAULTS)
+      except errors.OpPrereqError, err:
+        raise errors.OpPrereqError("While verify diskparams options: %s" % err,
+                                   errors.ECODE_INVAL)
+
+    if self.op.hv_state:
+      self.new_hv_state = _MergeAndVerifyHvState(self.op.hv_state,
+                                                 self.group.hv_state_static)
+
+    if self.op.disk_state:
+      self.new_disk_state = \
+        _MergeAndVerifyDiskState(self.op.disk_state,
+                                 self.group.disk_state_static)
+
+    if self.op.ipolicy:
+      self.new_ipolicy = _GetUpdatedIPolicy(self.group.ipolicy,
+                                            self.op.ipolicy,
+                                            group_policy=True)
+
+      new_ipolicy = cluster.SimpleFillIPolicy(self.new_ipolicy)
+      inst_filter = lambda inst: inst.name in owned_instances
+      instances = self.cfg.GetInstancesInfoByFilter(inst_filter).values()
+      violations = \
+          _ComputeNewInstanceViolations(_CalculateGroupIPolicy(cluster,
+                                                               self.group),
+                                        new_ipolicy, instances)
+
+      if violations:
+        self.LogWarning("After the ipolicy change the following instances"
+                        " violate them: %s",
+                        utils.CommaJoin(violations))
+
   def BuildHooksEnv(self):
     """Build hooks env.
 
@@ -12240,9 +14040,22 @@ class LUGroupSetParams(LogicalUnit):
       self.group.ndparams = self.new_ndparams
       result.append(("ndparams", str(self.group.ndparams)))
 
+    if self.op.diskparams:
+      self.group.diskparams = self.new_diskparams
+      result.append(("diskparams", str(self.group.diskparams)))
+
     if self.op.alloc_policy:
       self.group.alloc_policy = self.op.alloc_policy
 
+    if self.op.hv_state:
+      self.group.hv_state_static = self.new_hv_state
+
+    if self.op.disk_state:
+      self.group.disk_state_static = self.new_disk_state
+
+    if self.op.ipolicy:
+      self.group.ipolicy = self.new_ipolicy
+
     self.cfg.Update(self.group, feedback_fn)
     return result
 
@@ -12471,16 +14284,8 @@ class LUGroupEvacuate(LogicalUnit):
     self.instances = dict(self.cfg.GetMultiInstanceInfo(owned_instances))
 
     # Check if node groups for locked instances are still correct
-    for instance_name in owned_instances:
-      inst = self.instances[instance_name]
-      assert owned_nodes.issuperset(inst.all_nodes), \
-        "Instance %s's nodes changed while we kept the lock" % instance_name
-
-      inst_groups = _CheckInstanceNodeGroups(self.cfg, instance_name,
-                                             owned_groups)
-
-      assert self.group_uuid in inst_groups, \
-        "Instance %s has no node in group %s" % (instance_name, self.group_uuid)
+    _CheckInstancesNodeGroups(self.cfg, self.instances,
+                              owned_groups, owned_nodes, self.group_uuid)
 
     if self.req_target_uuids:
       # User requested specific target groups
@@ -12548,14 +14353,25 @@ class TagsLU(NoHooksLU): # pylint: disable=W0223
   def ExpandNames(self):
     self.group_uuid = None
     self.needed_locks = {}
+
     if self.op.kind == constants.TAG_NODE:
       self.op.name = _ExpandNodeName(self.cfg, self.op.name)
-      self.needed_locks[locking.LEVEL_NODE] = self.op.name
+      lock_level = locking.LEVEL_NODE
+      lock_name = self.op.name
     elif self.op.kind == constants.TAG_INSTANCE:
       self.op.name = _ExpandInstanceName(self.cfg, self.op.name)
-      self.needed_locks[locking.LEVEL_INSTANCE] = self.op.name
+      lock_level = locking.LEVEL_INSTANCE
+      lock_name = self.op.name
     elif self.op.kind == constants.TAG_NODEGROUP:
       self.group_uuid = self.cfg.LookupNodeGroup(self.op.name)
+      lock_level = locking.LEVEL_NODEGROUP
+      lock_name = self.group_uuid
+    else:
+      lock_level = None
+      lock_name = None
+
+    if lock_level and getattr(self.op, "use_locking", True):
+      self.needed_locks[lock_level] = lock_name
 
     # FIXME: Acquire BGL for cluster tag operations (as of this writing it's
     # not possible to acquire the BGL based on opcode parameters)
@@ -12899,14 +14715,14 @@ class IAllocator(object):
   # pylint: disable=R0902
   # lots of instance attributes
 
-  def __init__(self, cfg, rpc, mode, **kwargs):
+  def __init__(self, cfg, rpc_runner, mode, **kwargs):
     self.cfg = cfg
-    self.rpc = rpc
+    self.rpc = rpc_runner
     # init buffer variables
     self.in_text = self.out_text = self.in_data = self.out_data = None
     # init all input fields so that pylint is happy
     self.mode = mode
-    self.memory = self.disks = self.disk_template = None
+    self.memory = self.disks = self.disk_template = self.spindle_use = None
     self.os = self.tags = self.nics = self.vcpus = None
     self.hypervisor = None
     self.relocate_from = None
@@ -12953,7 +14769,7 @@ class IAllocator(object):
       "cluster_name": cfg.GetClusterName(),
       "cluster_tags": list(cluster_info.GetTags()),
       "enabled_hypervisors": list(cluster_info.enabled_hypervisors),
-      # we don't have job IDs
+      "ipolicy": cluster_info.ipolicy,
       }
     ninfo = cfg.GetAllNodesInfo()
     iinfo = cfg.GetAllInstancesInfo().values()
@@ -12967,17 +14783,17 @@ class IAllocator(object):
     elif self.mode == constants.IALLOCATOR_MODE_RELOC:
       hypervisor_name = cfg.GetInstanceInfo(self.name).hypervisor
     else:
-      hypervisor_name = cluster_info.enabled_hypervisors[0]
+      hypervisor_name = cluster_info.primary_hypervisor
 
-    node_data = self.rpc.call_node_info(node_list, cfg.GetVGName(),
-                                        hypervisor_name)
+    node_data = self.rpc.call_node_info(node_list, [cfg.GetVGName()],
+                                        [hypervisor_name])
     node_iinfo = \
       self.rpc.call_all_instances_info(node_list,
                                        cluster_info.enabled_hypervisors)
 
     data["nodegroups"] = self._ComputeNodeGroupData(cfg)
 
-    config_ndata = self._ComputeBasicNodeData(ninfo)
+    config_ndata = self._ComputeBasicNodeData(cfg, ninfo)
     data["nodes"] = self._ComputeDynamicNodeData(ninfo, node_data, node_iinfo,
                                                  i_list, config_ndata)
     assert len(data["nodes"]) == len(ninfo), \
@@ -12992,16 +14808,18 @@ class IAllocator(object):
     """Compute node groups data.
 
     """
+    cluster = cfg.GetClusterInfo()
     ng = dict((guuid, {
       "name": gdata.name,
       "alloc_policy": gdata.alloc_policy,
+      "ipolicy": _CalculateGroupIPolicy(cluster, gdata),
       })
       for guuid, gdata in cfg.GetAllNodeGroupsInfo().items())
 
     return ng
 
   @staticmethod
-  def _ComputeBasicNodeData(node_cfg):
+  def _ComputeBasicNodeData(cfg, node_cfg):
     """Compute global node data.
 
     @rtype: dict
@@ -13019,6 +14837,7 @@ class IAllocator(object):
       "group": ninfo.group,
       "master_capable": ninfo.master_capable,
       "vm_capable": ninfo.vm_capable,
+      "ndparams": cfg.GetNdParams(ninfo),
       })
       for ninfo in node_cfg.values())
 
@@ -13032,6 +14851,7 @@ class IAllocator(object):
     @param node_results: the basic node structures as filled from the config
 
     """
+    #TODO(dynmem): compute the right data on MAX and MIN memory
     # make a copy of the current dict
     node_results = dict(node_results)
     for nname, nresult in node_data.items():
@@ -13042,7 +14862,7 @@ class IAllocator(object):
         nresult.Raise("Can't get data for node %s" % nname)
         node_iinfo[nname].Raise("Can't get node instance info from node %s" %
                                 nname)
-        remote_info = nresult.payload
+        remote_info = _MakeLegacyNodeInfo(nresult.payload)
 
         for attr in ["memory_total", "memory_free", "memory_dom0",
                      "vg_size", "vg_free", "cpu_total"]:
@@ -13057,16 +14877,16 @@ class IAllocator(object):
         i_p_mem = i_p_up_mem = 0
         for iinfo, beinfo in i_list:
           if iinfo.primary_node == nname:
-            i_p_mem += beinfo[constants.BE_MEMORY]
+            i_p_mem += beinfo[constants.BE_MAXMEM]
             if iinfo.name not in node_iinfo[nname].payload:
               i_used_mem = 0
             else:
               i_used_mem = int(node_iinfo[nname].payload[iinfo.name]["memory"])
-            i_mem_diff = beinfo[constants.BE_MEMORY] - i_used_mem
+            i_mem_diff = beinfo[constants.BE_MAXMEM] - i_used_mem
             remote_info["memory_free"] -= max(0, i_mem_diff)
 
-            if iinfo.admin_up:
-              i_p_up_mem += beinfo[constants.BE_MEMORY]
+            if iinfo.admin_state == constants.ADMINST_UP:
+              i_p_up_mem += beinfo[constants.BE_MAXMEM]
 
         # compute memory used by instances
         pnr_dyn = {
@@ -13105,9 +14925,10 @@ class IAllocator(object):
         nic_data.append(nic_dict)
       pir = {
         "tags": list(iinfo.GetTags()),
-        "admin_up": iinfo.admin_up,
+        "admin_state": iinfo.admin_state,
         "vcpus": beinfo[constants.BE_VCPUS],
-        "memory": beinfo[constants.BE_MEMORY],
+        "memory": beinfo[constants.BE_MAXMEM],
+        "spindle_use": beinfo[constants.BE_SPINDLE_USE],
         "os": iinfo.os,
         "nodes": [iinfo.primary_node] + list(iinfo.secondary_nodes),
         "nics": nic_data,
@@ -13147,6 +14968,7 @@ class IAllocator(object):
       "os": self.os,
       "vcpus": self.vcpus,
       "memory": self.memory,
+      "spindle_use": self.spindle_use,
       "disks": self.disks,
       "disk_space_total": disk_space,
       "nics": self.nics,
@@ -13260,6 +15082,7 @@ class IAllocator(object):
        [
         ("name", ht.TString),
         ("memory", ht.TInt),
+        ("spindle_use", ht.TInt),
         ("disks", ht.TListOf(ht.TDict)),
         ("disk_template", ht.TString),
         ("os", ht.TString),
@@ -13496,10 +15319,12 @@ class LUTestAllocator(NoHooksLU):
 
 #: Query type implementations
 _QUERY_IMPL = {
+  constants.QR_CLUSTER: _ClusterQuery,
   constants.QR_INSTANCE: _InstanceQuery,
   constants.QR_NODE: _NodeQuery,
   constants.QR_GROUP: _GroupQuery,
   constants.QR_OS: _OsQuery,
+  constants.QR_EXPORT: _ExportQuery,
   }
 
 assert set(_QUERY_IMPL.keys()) == constants.QR_VIA_OP
index 53ee1fb..6515af1 100644 (file)
@@ -45,11 +45,10 @@ except ImportError:
 # modules (hmac, for example) which have changed their behavior as well from
 # one version to the other.
 try:
-  # pylint: disable=F0401
+  # Yes, these don't always exist, that's why we're testing
   # Yes, we're not using the imports in this module.
-  # pylint: disable=W0611
-  from hashlib import md5 as md5_hash
-  from hashlib import sha1 as sha1_hash
+  from hashlib import md5 as md5_hash # pylint: disable=W0611,E0611,F0401
+  from hashlib import sha1 as sha1_hash # pylint: disable=W0611,E0611,F0401
   # this additional version is needed for compatibility with the hmac module
   sha1 = sha1_hash
 except ImportError:
index 6527a93..c9cfc07 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2009 Google Inc.
+# Copyright (C) 2009, 2012 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
 
 from ganeti import constants
 from ganeti import errors
+from ganeti import ht
 
 
 _FOURCC_LEN = 4
 
 
+#: Items in the individual rows of the NodeDrbd query
+_HTNodeDrbdItems = [ht.TString, ht.TInt, ht.TString,
+                    ht.TString, ht.TString, ht.TString]
+#: Type for the (top-level) result of NodeDrbd query
+HTNodeDrbd = ht.TListOf(ht.TAnd(ht.TList, ht.TIsLength(len(_HTNodeDrbdItems)),
+                                ht.TItems(_HTNodeDrbdItems)))
+
+
 def PackMagic(payload):
   """Prepend the confd magic fourcc to a payload.
 
   """
-  return ''.join([constants.CONFD_MAGIC_FOURCC, payload])
+  return "".join([constants.CONFD_MAGIC_FOURCC, payload])
 
 
 def UnpackMagic(payload):
index 11baa4d..900b5f7 100644 (file)
@@ -171,7 +171,7 @@ class ConfdClient:
     """
     if now is None:
       now = time.time()
-    tstamp = '%d' % now
+    tstamp = "%d" % now
     req = serializer.DumpSignedJson(request.ToDict(), self._hmac_key, tstamp)
     return confd.PackMagic(req)
 
index e231a14..79467b5 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2009 Google Inc.
+# Copyright (C) 2009, 2012 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
@@ -63,7 +63,7 @@ class ConfdQuery(object):
 
     """
     status = constants.CONFD_REPL_STATUS_NOTIMPLEMENTED
-    answer = 'not implemented'
+    answer = "not implemented"
     return status, answer
 
 
@@ -80,10 +80,10 @@ class PingQuery(ConfdQuery):
     """
     if query is None:
       status = constants.CONFD_REPL_STATUS_OK
-      answer = 'ok'
+      answer = "ok"
     else:
       status = constants.CONFD_REPL_STATUS_ERROR
-      answer = 'non-empty ping query'
+      answer = "non-empty ping query"
 
     return status, answer
 
@@ -292,3 +292,11 @@ class InstancesIpsQuery(ConfdQuery):
     answer = self.reader.GetInstancesIps(link)
 
     return status, answer
+
+
+class NodeDrbdQuery(ConfdQuery):
+  """A query for node drbd minors.
+
+  This is not implemented in the Python confd.
+
+  """
index 74fb519..d96729d 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2009 Google Inc.
+# Copyright (C) 2009, 2012 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
@@ -55,6 +55,7 @@ class ConfdProcessor(object):
     constants.CONFD_REQ_NODE_PIP_LIST: querylib.NodesPipsQuery,
     constants.CONFD_REQ_MC_PIP_LIST: querylib.MasterCandidatesPipsQuery,
     constants.CONFD_REQ_INSTANCES_IPS_LIST: querylib.InstancesIpsQuery,
+    constants.CONFD_REQ_NODE_DRBD: querylib.NodeDrbdQuery,
     }
 
   def __init__(self):
@@ -92,7 +93,7 @@ class ConfdProcessor(object):
 
     """
     if self.disabled:
-      logging.debug('Confd is disabled. Ignoring query.')
+      logging.debug("Confd is disabled. Ignoring query.")
       return
     try:
       request = self.ExtractRequest(payload_in)
@@ -100,7 +101,7 @@ class ConfdProcessor(object):
       payload_out = self.PackReply(reply, rsalt)
       return payload_out
     except errors.ConfdRequestError, err:
-      logging.info('Ignoring broken query from %s:%d: %s', ip, port, err)
+      logging.info("Ignoring broken query from %s:%d: %s", ip, port, err)
       return None
 
   def ExtractRequest(self, payload):
@@ -130,7 +131,7 @@ class ConfdProcessor(object):
     try:
       request = objects.ConfdRequest.FromDict(message)
     except AttributeError, err:
-      raise errors.ConfdRequestError('%s' % err)
+      raise errors.ConfdRequestError(str(err))
 
     return request
 
index d71e01a..d29a612 100644 (file)
@@ -133,6 +133,26 @@ def _MatchNameComponentIgnoreCase(short_name, names):
   return utils.MatchNameComponent(short_name, names, case_sensitive=False)
 
 
+def _CheckInstanceDiskIvNames(disks):
+  """Checks if instance's disks' C{iv_name} attributes are in order.
+
+  @type disks: list of L{objects.Disk}
+  @param disks: List of disks
+  @rtype: list of tuples; (int, string, string)
+  @return: List of wrongly named disks, each tuple contains disk index,
+    expected and actual name
+
+  """
+  result = []
+
+  for (idx, disk) in enumerate(disks):
+    exp_iv_name = "disk/%s" % idx
+    if disk.iv_name != exp_iv_name:
+      result.append((idx, exp_iv_name, disk.iv_name))
+
+  return result
+
+
 class ConfigWriter:
   """The interface to the cluster configuration.
 
@@ -165,8 +185,21 @@ class ConfigWriter:
     self._my_hostname = netutils.Hostname.GetSysName()
     self._last_cluster_serial = -1
     self._cfg_id = None
+    self._context = None
     self._OpenConfig(accept_foreign)
 
+  def _GetRpc(self, address_list):
+    """Returns RPC runner for configuration.
+
+    """
+    return rpc.ConfigRunner(self._context, address_list)
+
+  def SetContext(self, context):
+    """Sets Ganeti context.
+
+    """
+    self._context = context
+
   # this method needs to be static, so that we can call it on the class
   @staticmethod
   def IsCluster():
@@ -199,6 +232,40 @@ class ConfigWriter:
     return self._config_data.cluster.FillND(node, nodegroup)
 
   @locking.ssynchronized(_config_lock, shared=1)
+  def GetInstanceDiskParams(self, instance):
+    """Get the disk params populated with inherit chain.
+
+    @type instance: L{objects.Instance}
+    @param instance: The instance we want to know the params for
+    @return: A dict with the filled in disk params
+
+    """
+    node = self._UnlockedGetNodeInfo(instance.primary_node)
+    nodegroup = self._UnlockedGetNodeGroup(node.group)
+    return self._UnlockedGetGroupDiskParams(nodegroup)
+
+  @locking.ssynchronized(_config_lock, shared=1)
+  def GetGroupDiskParams(self, group):
+    """Get the disk params populated with inherit chain.
+
+    @type group: L{objects.NodeGroup}
+    @param group: The group we want to know the params for
+    @return: A dict with the filled in disk params
+
+    """
+    return self._UnlockedGetGroupDiskParams(group)
+
+  def _UnlockedGetGroupDiskParams(self, group):
+    """Get the disk params populated with inherit chain down to node-group.
+
+    @type group: L{objects.NodeGroup}
+    @param group: The group we want to know the params for
+    @return: A dict with the filled in disk params
+
+    """
+    return self._config_data.cluster.SimpleFillDP(group.diskparams)
+
+  @locking.ssynchronized(_config_lock, shared=1)
   def GenerateMAC(self, ec_id):
     """Generate a MAC for an instance.
 
@@ -413,6 +480,28 @@ class ConfigWriter:
       except errors.ConfigurationError, err:
         result.append("%s has invalid nicparams: %s" % (owner, err))
 
+    def _helper_ipolicy(owner, params, check_std):
+      try:
+        objects.InstancePolicy.CheckParameterSyntax(params, check_std)
+      except errors.ConfigurationError, err:
+        result.append("%s has invalid instance policy: %s" % (owner, err))
+
+    def _helper_ispecs(owner, params):
+      for key, value in params.items():
+        if key in constants.IPOLICY_ISPECS:
+          fullkey = "ipolicy/" + key
+          _helper(owner, fullkey, value, constants.ISPECS_PARAMETER_TYPES)
+        else:
+          # FIXME: assuming list type
+          if key in constants.IPOLICY_PARAMETERS:
+            exp_type = float
+          else:
+            exp_type = list
+          if not isinstance(value, exp_type):
+            result.append("%s has invalid instance policy: for %s,"
+                          " expecting %s, got %s" %
+                          (owner, key, exp_type.__name__, type(value)))
+
     # check cluster parameters
     _helper("cluster", "beparams", cluster.SimpleFillBE({}),
             constants.BES_PARAMETER_TYPES)
@@ -421,6 +510,8 @@ class ConfigWriter:
     _helper_nic("cluster", cluster.SimpleFillNIC({}))
     _helper("cluster", "ndparams", cluster.SimpleFillND({}),
             constants.NDS_PARAMETER_TYPES)
+    _helper_ipolicy("cluster", cluster.SimpleFillIPolicy({}), True)
+    _helper_ispecs("cluster", cluster.SimpleFillIPolicy({}))
 
     # per-instance checks
     for instance_name in data.instances:
@@ -454,12 +545,12 @@ class ConfigWriter:
                 cluster.FillBE(instance), constants.BES_PARAMETER_TYPES)
 
       # gather the drbd ports for duplicate checks
-      for dsk in instance.disks:
+      for (idx, dsk) in enumerate(instance.disks):
         if dsk.dev_type in constants.LDS_DRBD:
           tcp_port = dsk.logical_id[2]
           if tcp_port not in ports:
             ports[tcp_port] = []
-          ports[tcp_port].append((instance.name, "drbd disk %s" % dsk.iv_name))
+          ports[tcp_port].append((instance.name, "drbd disk %s" % idx))
       # gather network port reservation
       net_port = getattr(instance, "network_port", None)
       if net_port is not None:
@@ -473,6 +564,15 @@ class ConfigWriter:
                        (instance.name, idx, msg) for msg in disk.Verify()])
         result.extend(self._CheckDiskIDs(disk, seen_lids, seen_pids))
 
+      wrong_names = _CheckInstanceDiskIvNames(instance.disks)
+      if wrong_names:
+        tmp = "; ".join(("name of disk %s should be '%s', but is '%s'" %
+                         (idx, exp_name, actual_name))
+                        for (idx, exp_name, actual_name) in wrong_names)
+
+        result.append("Instance '%s' has wrongly named disks: %s" %
+                      (instance.name, tmp))
+
     # cluster-wide pool of free ports
     for free_port in cluster.tcpudp_port_pool:
       if free_port not in ports:
@@ -535,8 +635,12 @@ class ConfigWriter:
         result.append("duplicate node group name '%s'" % nodegroup.name)
       else:
         nodegroups_names.add(nodegroup.name)
+      group_name = "group %s" % nodegroup.name
+      _helper_ipolicy(group_name, cluster.SimpleFillIPolicy(nodegroup.ipolicy),
+                      False)
+      _helper_ispecs(group_name, cluster.SimpleFillIPolicy(nodegroup.ipolicy))
       if nodegroup.ndparams:
-        _helper("group %s" % nodegroup.name, "ndparams",
+        _helper(group_name, "ndparams",
                 cluster.SimpleFillND(nodegroup.ndparams),
                 constants.NDS_PARAMETER_TYPES)
 
@@ -881,6 +985,20 @@ class ConfigWriter:
     return self._config_data.cluster.master_netdev
 
   @locking.ssynchronized(_config_lock, shared=1)
+  def GetMasterNetmask(self):
+    """Get the netmask of the master node for this cluster.
+
+    """
+    return self._config_data.cluster.master_netmask
+
+  @locking.ssynchronized(_config_lock, shared=1)
+  def GetUseExternalMipScript(self):
+    """Get flag representing whether to use the external master IP setup script.
+
+    """
+    return self._config_data.cluster.use_external_mip_script
+
+  @locking.ssynchronized(_config_lock, shared=1)
   def GetFileStorageDir(self):
     """Get the file storage dir for this cluster.
 
@@ -927,6 +1045,23 @@ class ConfigWriter:
     """
     return self._config_data.cluster.primary_ip_family
 
+  @locking.ssynchronized(_config_lock, shared=1)
+  def GetMasterNetworkParameters(self):
+    """Get network parameters of the master node.
+
+    @rtype: L{object.MasterNetworkParameters}
+    @return: network parameters of the master node
+
+    """
+    cluster = self._config_data.cluster
+    result = objects.MasterNetworkParameters(name=cluster.master_node,
+      ip=cluster.master_ip,
+      netmask=cluster.master_netmask,
+      netdev=cluster.master_netdev,
+      ip_family=cluster.primary_ip_family)
+
+    return result
+
   @locking.ssynchronized(_config_lock)
   def AddNodeGroup(self, group, ec_id, check_uuid=True):
     """Add a node group to the configuration.
@@ -1085,6 +1220,17 @@ class ConfigWriter:
                      for member_name in
                        self._UnlockedGetNodeGroup(ngfn(node_name)).members)
 
+  @locking.ssynchronized(_config_lock, shared=1)
+  def GetMultiNodeGroupInfo(self, group_uuids):
+    """Get the configuration of multiple node groups.
+
+    @param group_uuids: List of node group UUIDs
+    @rtype: list
+    @return: List of tuples of (group_uuid, group_info)
+
+    """
+    return [(uuid, self._UnlockedGetNodeGroup(uuid)) for uuid in group_uuids]
+
   @locking.ssynchronized(_config_lock)
   def AddInstance(self, instance, ec_id):
     """Add an instance to the config.
@@ -1135,15 +1281,15 @@ class ConfigWriter:
     """Set the instance's status to a given value.
 
     """
-    assert isinstance(status, bool), \
+    assert status in constants.ADMINST_ALL, \
            "Invalid status '%s' passed to SetInstanceStatus" % (status,)
 
     if instance_name not in self._config_data.instances:
       raise errors.ConfigurationError("Unknown instance '%s'" %
                                       instance_name)
     instance = self._config_data.instances[instance_name]
-    if instance.admin_up != status:
-      instance.admin_up = status
+    if instance.admin_state != status:
+      instance.admin_state = status
       instance.serial_no += 1
       instance.mtime = time.time()
       self._WriteConfig()
@@ -1153,7 +1299,14 @@ class ConfigWriter:
     """Mark the instance status to up in the config.
 
     """
-    self._SetInstanceStatus(instance_name, True)
+    self._SetInstanceStatus(instance_name, constants.ADMINST_UP)
+
+  @locking.ssynchronized(_config_lock)
+  def MarkInstanceOffline(self, instance_name):
+    """Mark the instance status to down in the config.
+
+    """
+    self._SetInstanceStatus(instance_name, constants.ADMINST_OFFLINE)
 
   @locking.ssynchronized(_config_lock)
   def RemoveInstance(self, instance_name):
@@ -1185,24 +1338,27 @@ class ConfigWriter:
     """
     if old_name not in self._config_data.instances:
       raise errors.ConfigurationError("Unknown instance '%s'" % old_name)
-    inst = self._config_data.instances[old_name]
-    del self._config_data.instances[old_name]
+
+    # Operate on a copy to not loose instance object in case of a failure
+    inst = self._config_data.instances[old_name].Copy()
     inst.name = new_name
 
-    for disk in inst.disks:
+    for (idx, disk) in enumerate(inst.disks):
       if disk.dev_type == constants.LD_FILE:
         # rename the file paths in logical and physical id
         file_storage_dir = os.path.dirname(os.path.dirname(disk.logical_id[1]))
-        disk_fname = "disk%s" % disk.iv_name.split("/")[1]
-        disk.physical_id = disk.logical_id = (disk.logical_id[0],
-                                              utils.PathJoin(file_storage_dir,
-                                                             inst.name,
-                                                             disk_fname))
+        disk.logical_id = (disk.logical_id[0],
+                           utils.PathJoin(file_storage_dir, inst.name,
+                                          "disk%s" % idx))
+        disk.physical_id = disk.logical_id
+
+    # Actually replace instance object
+    del self._config_data.instances[old_name]
+    self._config_data.instances[inst.name] = inst
 
     # Force update of ssconf files
     self._config_data.cluster.serial_no += 1
 
-    self._config_data.instances[inst.name] = inst
     self._WriteConfig()
 
   @locking.ssynchronized(_config_lock)
@@ -1210,7 +1366,7 @@ class ConfigWriter:
     """Mark the status of an instance to down in the configuration.
 
     """
-    self._SetInstanceStatus(instance_name, False)
+    self._SetInstanceStatus(instance_name, constants.ADMINST_DOWN)
 
   def _UnlockedGetInstanceList(self):
     """Get the list of instances.
@@ -1309,6 +1465,22 @@ class ConfigWriter:
                     for instance in self._UnlockedGetInstanceList()])
     return my_dict
 
+  @locking.ssynchronized(_config_lock, shared=1)
+  def GetInstancesInfoByFilter(self, filter_fn):
+    """Get instance configuration with a filter.
+
+    @type filter_fn: callable
+    @param filter_fn: Filter function receiving instance object as parameter,
+      returning boolean. Important: this function is called while the
+      configuration locks is held. It must not do any complex work or call
+      functions potentially leading to a deadlock. Ideally it doesn't call any
+      other functions and just compares instance attributes.
+
+    """
+    return dict((name, inst)
+                for (name, inst) in self._config_data.instances.items()
+                if filter_fn(inst))
+
   @locking.ssynchronized(_config_lock)
   def AddNode(self, node, ec_id):
     """Add a node to the configuration.
@@ -1493,9 +1665,16 @@ class ConfigWriter:
               would GetNodeInfo return for the node
 
     """
-    my_dict = dict([(node, self._UnlockedGetNodeInfo(node))
-                    for node in self._UnlockedGetNodeList()])
-    return my_dict
+    return self._UnlockedGetAllNodesInfo()
+
+  def _UnlockedGetAllNodesInfo(self):
+    """Gets configuration of all nodes.
+
+    @note: See L{GetAllNodesInfo}
+
+    """
+    return dict([(node, self._UnlockedGetNodeInfo(node))
+                 for node in self._UnlockedGetNodeList()])
 
   @locking.ssynchronized(_config_lock, shared=1)
   def GetNodeGroupsFromNodes(self, nodes):
@@ -1670,7 +1849,7 @@ class ConfigWriter:
 
     # Update timestamps and serials (only once per node/group object)
     now = time.time()
-    for obj in frozenset(itertools.chain(*resmod)): # pylint: disable-msg=W0142
+    for obj in frozenset(itertools.chain(*resmod)): # pylint: disable=W0142
       obj.serial_no += 1
       obj.mtime = now
 
@@ -1709,8 +1888,8 @@ class ConfigWriter:
     # Make sure the configuration has the right version
     _ValidateConfig(data)
 
-    if (not hasattr(data, 'cluster') or
-        not hasattr(data.cluster, 'rsahostkeypub')):
+    if (not hasattr(data, "cluster") or
+        not hasattr(data.cluster, "rsahostkeypub")):
       raise errors.ConfigurationError("Incomplete configuration"
                                       " (missing cluster.rsahostkeypub)")
 
@@ -1801,8 +1980,9 @@ class ConfigWriter:
       node_list.append(node_info.name)
       addr_list.append(node_info.primary_ip)
 
-    result = rpc.RpcRunner.call_upload_file(node_list, self._cfg_file,
-                                            address_list=addr_list)
+    # TODO: Use dedicated resolver talking to config writer for name resolution
+    result = \
+      self._GetRpc(addr_list).call_upload_file(node_list, self._cfg_file)
     for to_node, to_result in result.items():
       msg = to_result.fail_msg
       if msg:
@@ -1861,7 +2041,7 @@ class ConfigWriter:
     # Write ssconf files on all nodes (including locally)
     if self._last_cluster_serial < self._config_data.cluster.serial_no:
       if not self._offline:
-        result = rpc.RpcRunner.call_write_ssconf_files(
+        result = self._GetRpc(None).call_write_ssconf_files(
           self._UnlockedGetOnlineNodeList(),
           self._UnlockedGetSsconfValues())
 
@@ -1924,6 +2104,7 @@ class ConfigWriter:
       constants.SS_MASTER_CANDIDATES_IPS: mc_ips_data,
       constants.SS_MASTER_IP: cluster.master_ip,
       constants.SS_MASTER_NETDEV: cluster.master_netdev,
+      constants.SS_MASTER_NETMASK: str(cluster.master_netmask),
       constants.SS_MASTER_NODE: cluster.master_node,
       constants.SS_NODE_LIST: node_data,
       constants.SS_NODE_PRIMARY_IPS: node_pri_ips_data,
index 3b73479..02aa6f6 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 Google Inc.
+# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 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
@@ -22,6 +22,7 @@
 """Module holding different constants."""
 
 import re
+import socket
 
 from ganeti import _autoconf
 from ganeti import _vcsversion
@@ -99,6 +100,28 @@ CONFD_GROUP = _autoconf.CONFD_GROUP
 NODED_USER = _autoconf.NODED_USER
 NODED_GROUP = _autoconf.NODED_GROUP
 
+# cpu pinning separators and constants
+CPU_PINNING_SEP = ":"
+CPU_PINNING_ALL = "all"
+# internal representation of "all"
+CPU_PINNING_ALL_VAL = -1
+# one "all" entry in a CPU list means CPU pinning is off
+CPU_PINNING_OFF = [CPU_PINNING_ALL_VAL]
+
+# A Xen-specific implementation detail - there is no way to actually say
+# "use any cpu for pinning" in a Xen configuration file, as opposed to the
+# command line, where you can say "xm vcpu-pin <domain> <vcpu> all".
+# The workaround used in Xen is "0-63" (see source code function
+# xm_vcpu_pin in <xen-source>/tools/python/xen/xm/main.py).
+# To support future changes, the following constant is treated as a
+# blackbox string that simply means use-any-cpu-for-pinning-under-xen.
+CPU_PINNING_ALL_XEN = "0-63"
+
+# A KVM-specific implementation detail - the following value is used
+# to set CPU affinity to all processors (#0 through #31), per taskset
+# man page.
+# FIXME: This only works for machines with up to 32 CPU cores
+CPU_PINNING_ALL_KVM = 0xFFFFFFFF
 
 # Wipe
 DD_CMD = "dd"
@@ -117,19 +140,9 @@ RUN_DIRS_MODE = 0775
 SOCKET_DIR = RUN_GANETI_DIR + "/socket"
 SECURE_DIR_MODE = 0700
 SECURE_FILE_MODE = 0600
-SOCKET_DIR_MODE = 0750
 CRYPTO_KEYS_DIR = RUN_GANETI_DIR + "/crypto"
-CRYPTO_KEYS_DIR_MODE = SECURE_DIR_MODE
 IMPORT_EXPORT_DIR = RUN_GANETI_DIR + "/import-export"
-IMPORT_EXPORT_DIR_MODE = 0755
 ADOPTABLE_BLOCKDEV_ROOT = "/dev/disk/"
-# keep RUN_GANETI_DIR first here, to make sure all get created when the node
-# daemon is started (this takes care of RUN_DIR being tmpfs)
-SUB_RUN_DIRS = [
-  RUN_GANETI_DIR,
-  BDEV_CACHE_DIR,
-  DISK_LINKS_DIR,
-  ]
 LOCK_DIR = _autoconf.LOCALSTATEDIR + "/lock"
 SSCONF_LOCK_FILE = LOCK_DIR + "/ganeti-ssconf.lock"
 # User-id pool lock directory
@@ -139,6 +152,8 @@ CLUSTER_CONF_FILE = DATA_DIR + "/config.data"
 NODED_CERT_FILE = DATA_DIR + "/server.pem"
 RAPI_CERT_FILE = DATA_DIR + "/rapi.pem"
 CONFD_HMAC_KEY = DATA_DIR + "/hmac.key"
+SPICE_CERT_FILE = DATA_DIR + "/spice.pem"
+SPICE_CACERT_FILE = DATA_DIR + "/spice-ca.pem"
 CLUSTER_DOMAIN_SECRET_FILE = DATA_DIR + "/cluster-domain-secret"
 INSTANCE_STATUS_FILE = RUN_GANETI_DIR + "/instance-status"
 SSH_KNOWN_HOSTS_FILE = DATA_DIR + "/known_hosts"
@@ -157,6 +172,9 @@ ENABLE_SHARED_FILE_STORAGE = _autoconf.ENABLE_SHARED_FILE_STORAGE
 SYSCONFDIR = _autoconf.SYSCONFDIR
 TOOLSDIR = _autoconf.TOOLSDIR
 CONF_DIR = SYSCONFDIR + "/ganeti"
+USER_SCRIPTS_DIR = CONF_DIR + "/scripts"
+ENABLE_CONFD = _autoconf.ENABLE_CONFD
+HS_CONFD = _autoconf.HS_CONFD
 
 #: Lock file for watcher, locked in shared mode by watcher; lock in exclusive
 # mode to block watcher (see L{cli._RunWhileClusterStoppedHelper.Call}
@@ -172,7 +190,16 @@ WATCHER_GROUP_INSTANCE_STATUS_FILE = DATA_DIR + "/watcher.%s.instance-status"
 #: File containing Unix timestamp until which watcher should be paused
 WATCHER_PAUSEFILE = DATA_DIR + "/watcher.pause"
 
-ALL_CERT_FILES = frozenset([NODED_CERT_FILE, RAPI_CERT_FILE])
+# Master IP address setup scripts paths (default and user-provided)
+DEFAULT_MASTER_SETUP_SCRIPT = TOOLSDIR + "/master-ip-setup"
+EXTERNAL_MASTER_SETUP_SCRIPT = USER_SCRIPTS_DIR + "/master-ip-setup"
+
+ALL_CERT_FILES = frozenset([
+  NODED_CERT_FILE,
+  RAPI_CERT_FILE,
+  SPICE_CERT_FILE,
+  SPICE_CACERT_FILE,
+  ])
 
 MASTER_SOCKET = SOCKET_DIR + "/ganeti-master"
 
@@ -200,7 +227,6 @@ DEFAULT_NLD_PORT = DAEMONS_PORTS[NLD][1]
 
 FIRST_DRBD_PORT = 11000
 LAST_DRBD_PORT = 14999
-MASTER_SCRIPT = "ganeti-master"
 
 LOG_DIR = _autoconf.LOCALSTATEDIR + "/log/ganeti/"
 DAEMONS_LOGFILES = {
@@ -242,8 +268,13 @@ EXPORT_CONF_FILE = "config.ini"
 XEN_BOOTLOADER = _autoconf.XEN_BOOTLOADER
 XEN_KERNEL = _autoconf.XEN_KERNEL
 XEN_INITRD = _autoconf.XEN_INITRD
+XEN_CMD_XM = "xm"
+XEN_CMD_XL = "xl"
+# FIXME: This will be made configurable using hvparams in Ganeti 2.7
+XEN_CMD = _autoconf.XEN_CMD
 
 KVM_PATH = _autoconf.KVM_PATH
+KVM_KERNEL = _autoconf.KVM_KERNEL
 SOCAT_PATH = _autoconf.SOCAT_PATH
 SOCAT_USE_ESCAPE = _autoconf.SOCAT_USE_ESCAPE
 SOCAT_USE_COMPRESS = _autoconf.SOCAT_USE_COMPRESS
@@ -255,11 +286,14 @@ CONS_SSH = "ssh"
 #: Console as VNC server
 CONS_VNC = "vnc"
 
+#: Console as SPICE server
+CONS_SPICE = "spice"
+
 #: Display a message for console access
 CONS_MESSAGE = "msg"
 
 #: All console types
-CONS_ALL = frozenset([CONS_SSH, CONS_VNC, CONS_MESSAGE])
+CONS_ALL = frozenset([CONS_SSH, CONS_VNC, CONS_SPICE, CONS_MESSAGE])
 
 # For RSA keys more bits are better, but they also make operations more
 # expensive. NIST SP 800-131 recommends a minimum of 2048 bits from the year
@@ -392,18 +426,20 @@ DT_DRBD8 = "drbd"
 DT_FILE = "file"
 DT_SHARED_FILE = "sharedfile"
 DT_BLOCK = "blockdev"
+DT_RBD = "rbd"
 
 # the set of network-mirrored disk templates
 DTS_INT_MIRROR = frozenset([DT_DRBD8])
 
 # the set of externally-mirrored disk templates (e.g. SAN, NAS)
-DTS_EXT_MIRROR = frozenset([DT_SHARED_FILE, DT_BLOCK])
+DTS_EXT_MIRROR = frozenset([DT_SHARED_FILE, DT_BLOCK, DT_RBD])
 
 # the set of non-lvm-based disk templates
-DTS_NOT_LVM = frozenset([DT_DISKLESS, DT_FILE, DT_SHARED_FILE, DT_BLOCK])
+DTS_NOT_LVM = frozenset([DT_DISKLESS, DT_FILE, DT_SHARED_FILE,
+                         DT_BLOCK, DT_RBD])
 
 # the set of disk templates which can be grown
-DTS_GROWABLE = frozenset([DT_PLAIN, DT_DRBD8, DT_FILE, DT_SHARED_FILE])
+DTS_GROWABLE = frozenset([DT_PLAIN, DT_DRBD8, DT_FILE, DT_SHARED_FILE, DT_RBD])
 
 # the set of disk templates that allow adoption
 DTS_MAY_ADOPT = frozenset([DT_PLAIN, DT_BLOCK])
@@ -422,12 +458,42 @@ LD_LV = "lvm"
 LD_DRBD8 = "drbd8"
 LD_FILE = "file"
 LD_BLOCKDEV = "blockdev"
-LDS_BLOCK = frozenset([LD_LV, LD_DRBD8, LD_BLOCKDEV])
+LD_RBD = "rbd"
+LOGICAL_DISK_TYPES = frozenset([
+  LD_LV,
+  LD_DRBD8,
+  LD_FILE,
+  LD_BLOCKDEV,
+  LD_RBD,
+  ])
+
+LDS_BLOCK = frozenset([LD_LV, LD_DRBD8, LD_BLOCKDEV, LD_RBD])
 
 # drbd constants
 DRBD_HMAC_ALG = "md5"
 DRBD_NET_PROTOCOL = "C"
-DRBD_BARRIERS = _autoconf.DRBD_BARRIERS
+
+# drbd barrier types
+DRBD_B_NONE = "n"
+DRBD_B_DISK_BARRIERS = "b"
+DRBD_B_DISK_DRAIN = "d"
+DRBD_B_DISK_FLUSH = "f"
+
+# Valid barrier combinations: "n" or any non-null subset of "bfd"
+DRBD_VALID_BARRIER_OPT = frozenset([
+  frozenset([DRBD_B_NONE]),
+  frozenset([DRBD_B_DISK_BARRIERS]),
+  frozenset([DRBD_B_DISK_DRAIN]),
+  frozenset([DRBD_B_DISK_FLUSH]),
+  frozenset([DRBD_B_DISK_DRAIN, DRBD_B_DISK_FLUSH]),
+  frozenset([DRBD_B_DISK_DRAIN, DRBD_B_DISK_FLUSH]),
+  frozenset([DRBD_B_DISK_BARRIERS, DRBD_B_DISK_DRAIN]),
+  frozenset([DRBD_B_DISK_BARRIERS, DRBD_B_DISK_FLUSH]),
+  frozenset([DRBD_B_DISK_BARRIERS, DRBD_B_DISK_FLUSH, DRBD_B_DISK_DRAIN]),
+  ])
+
+# rbd tool command
+RBD_CMD = "rbd"
 
 # file backend driver
 FD_LOOP = "loop"
@@ -461,18 +527,6 @@ EXPORT_MODES = frozenset([
   EXPORT_MODE_REMOTE,
   ])
 
-# Lock recalculate mode
-LOCKS_REPLACE = "replace"
-LOCKS_APPEND = "append"
-
-# Lock timeout (sum) before we should go into blocking acquire (still
-# can be reset by priority change); computed as max time (10 hours)
-# before we should actually go into blocking acquire given that we
-# start from default priority level; in seconds
-LOCK_ATTEMPTS_TIMEOUT = 10 * 3600 / 20.0
-LOCK_ATTEMPTS_MAXWAIT = 15.0
-LOCK_ATTEMPTS_MINWAIT = 1.0
-
 # instance creation modes
 INSTANCE_CREATE = "create"
 INSTANCE_IMPORT = "import"
@@ -508,7 +562,8 @@ DISK_TEMPLATES = frozenset([
   DT_DRBD8,
   DT_FILE,
   DT_SHARED_FILE,
-  DT_BLOCK
+  DT_BLOCK,
+  DT_RBD
   ])
 
 FILE_DRIVER = frozenset([FD_LOOP, FD_BLKTAP])
@@ -522,8 +577,13 @@ INISECT_OSP = "os"
 
 # dynamic device modification
 DDM_ADD = "add"
+DDM_MODIFY = "modify"
 DDM_REMOVE = "remove"
 DDMS_VALUES = frozenset([DDM_ADD, DDM_REMOVE])
+DDMS_VALUES_WITH_MODIFY = (DDMS_VALUES | frozenset([
+  DDM_MODIFY,
+  ]))
+# TODO: DDM_SWAP, DDM_MOVE?
 
 # common exit codes
 EXIT_SUCCESS = 0
@@ -552,7 +612,7 @@ MAX_TAGS_PER_OBJ = 4096
 
 # others
 DEFAULT_BRIDGE = "xen-br0"
-SYNC_SPEED = 60 * 1024
+CLASSIC_DRBD_SYNC_SPEED = 60 * 1024  # 60 MiB, expressed in KiB
 IP4_ADDRESS_LOCALHOST = "127.0.0.1"
 IP4_ADDRESS_ANY = "0.0.0.0"
 IP6_ADDRESS_LOCALHOST = "::1"
@@ -560,13 +620,16 @@ IP6_ADDRESS_ANY = "::"
 IP4_VERSION = 4
 IP6_VERSION = 6
 VALID_IP_VERSIONS = frozenset([IP4_VERSION, IP6_VERSION])
+# for export to htools
+IP4_FAMILY = socket.AF_INET
+IP6_FAMILY = socket.AF_INET6
+
 TCP_PING_TIMEOUT = 10
 GANETI_RUNAS = "root"
 DEFAULT_VG = "xenvg"
 DEFAULT_DRBD_HELPER = "/bin/true"
 MIN_VG_SIZE = 20480
 DEFAULT_MAC_PREFIX = "aa:00:00"
-LVM_STRIPECOUNT = _autoconf.LVM_STRIPECOUNT
 # default maximum instance wait time, in seconds.
 DEFAULT_SHUTDOWN_TIMEOUT = 120
 NODE_MAX_CLOCK_SKEW = 150
@@ -671,6 +734,15 @@ HV_VNC_X509 = "vnc_x509_path"
 HV_VNC_X509_VERIFY = "vnc_x509_verify"
 HV_KVM_SPICE_BIND = "spice_bind"
 HV_KVM_SPICE_IP_VERSION = "spice_ip_version"
+HV_KVM_SPICE_PASSWORD_FILE = "spice_password_file"
+HV_KVM_SPICE_LOSSLESS_IMG_COMPR = "spice_image_compression"
+HV_KVM_SPICE_JPEG_IMG_COMPR = "spice_jpeg_wan_compression"
+HV_KVM_SPICE_ZLIB_GLZ_IMG_COMPR = "spice_zlib_glz_wan_compression"
+HV_KVM_SPICE_STREAMING_VIDEO_DETECTION = "spice_streaming_video"
+HV_KVM_SPICE_AUDIO_COMPR = "spice_playback_compression"
+HV_KVM_SPICE_USE_TLS = "spice_use_tls"
+HV_KVM_SPICE_TLS_CIPHERS = "spice_tls_ciphers"
+HV_KVM_SPICE_USE_VDAGENT = "spice_use_vdagent"
 HV_ACPI = "acpi"
 HV_PAE = "pae"
 HV_USE_BOOTLOADER = "use_bootloader"
@@ -716,6 +788,15 @@ HVS_PARAMETER_TYPES = {
   HV_VNC_X509_VERIFY: VTYPE_BOOL,
   HV_KVM_SPICE_BIND: VTYPE_STRING,
   HV_KVM_SPICE_IP_VERSION: VTYPE_INT,
+  HV_KVM_SPICE_PASSWORD_FILE: VTYPE_STRING,
+  HV_KVM_SPICE_LOSSLESS_IMG_COMPR: VTYPE_STRING,
+  HV_KVM_SPICE_JPEG_IMG_COMPR: VTYPE_STRING,
+  HV_KVM_SPICE_ZLIB_GLZ_IMG_COMPR: VTYPE_STRING,
+  HV_KVM_SPICE_STREAMING_VIDEO_DETECTION: VTYPE_STRING,
+  HV_KVM_SPICE_AUDIO_COMPR: VTYPE_BOOL,
+  HV_KVM_SPICE_USE_TLS: VTYPE_BOOL,
+  HV_KVM_SPICE_TLS_CIPHERS: VTYPE_STRING,
+  HV_KVM_SPICE_USE_VDAGENT: VTYPE_BOOL,
   HV_ACPI: VTYPE_BOOL,
   HV_PAE: VTYPE_BOOL,
   HV_USE_BOOTLOADER: VTYPE_BOOL,
@@ -749,28 +830,224 @@ HVS_PARAMETER_TYPES = {
 
 HVS_PARAMETERS = frozenset(HVS_PARAMETER_TYPES.keys())
 
+# Migration statuses
+HV_MIGRATION_COMPLETED = "completed"
+HV_MIGRATION_ACTIVE = "active"
+HV_MIGRATION_FAILED = "failed"
+HV_MIGRATION_CANCELLED = "cancelled"
+
+HV_MIGRATION_VALID_STATUSES = frozenset([
+  HV_MIGRATION_COMPLETED,
+  HV_MIGRATION_ACTIVE,
+  HV_MIGRATION_FAILED,
+  HV_MIGRATION_CANCELLED,
+  ])
+
+HV_MIGRATION_FAILED_STATUSES = frozenset([
+  HV_MIGRATION_FAILED,
+  HV_MIGRATION_CANCELLED,
+  ])
+
+# KVM-specific statuses
+HV_KVM_MIGRATION_VALID_STATUSES = HV_MIGRATION_VALID_STATUSES
+
+# Node info keys
+HV_NODEINFO_KEY_VERSION = "hv_version"
+
+# Hypervisor state
+HVST_MEMORY_TOTAL = "mem_total"
+HVST_MEMORY_NODE = "mem_node"
+HVST_MEMORY_HV = "mem_hv"
+HVST_CPU_TOTAL = "cpu_total"
+HVST_CPU_NODE = "cpu_node"
+
+HVST_DEFAULTS = {
+  HVST_MEMORY_TOTAL: 0,
+  HVST_MEMORY_NODE: 0,
+  HVST_MEMORY_HV: 0,
+  HVST_CPU_TOTAL: 1,
+  HVST_CPU_NODE: 1,
+  }
+
+HVSTS_PARAMETER_TYPES = {
+  HVST_MEMORY_TOTAL: VTYPE_INT,
+  HVST_MEMORY_NODE: VTYPE_INT,
+  HVST_MEMORY_HV: VTYPE_INT,
+  HVST_CPU_TOTAL: VTYPE_INT,
+  HVST_CPU_NODE: VTYPE_INT,
+  }
+
+HVSTS_PARAMETERS = frozenset(HVSTS_PARAMETER_TYPES.keys())
+
+# Disk state
+DS_DISK_TOTAL = "disk_total"
+DS_DISK_RESERVED = "disk_reserved"
+DS_DISK_OVERHEAD = "disk_overhead"
+
+DS_DEFAULTS = {
+  DS_DISK_TOTAL: 0,
+  DS_DISK_RESERVED: 0,
+  DS_DISK_OVERHEAD: 0,
+  }
+
+DSS_PARAMETER_TYPES = {
+  DS_DISK_TOTAL: VTYPE_INT,
+  DS_DISK_RESERVED: VTYPE_INT,
+  DS_DISK_OVERHEAD: VTYPE_INT,
+  }
+
+DSS_PARAMETERS = frozenset(DSS_PARAMETER_TYPES.keys())
+DS_VALID_TYPES = frozenset([LD_LV])
+
 # Backend parameter names
-BE_MEMORY = "memory"
+BE_MEMORY = "memory" # deprecated and replaced by max and min mem
+BE_MAXMEM = "maxmem"
+BE_MINMEM = "minmem"
 BE_VCPUS = "vcpus"
 BE_AUTO_BALANCE = "auto_balance"
+BE_ALWAYS_FAILOVER = "always_failover"
+BE_SPINDLE_USE = "spindle_use"
 
 BES_PARAMETER_TYPES = {
-    BE_MEMORY: VTYPE_SIZE,
-    BE_VCPUS: VTYPE_INT,
-    BE_AUTO_BALANCE: VTYPE_BOOL,
-    }
+  BE_MAXMEM: VTYPE_SIZE,
+  BE_MINMEM: VTYPE_SIZE,
+  BE_VCPUS: VTYPE_INT,
+  BE_AUTO_BALANCE: VTYPE_BOOL,
+  BE_ALWAYS_FAILOVER: VTYPE_BOOL,
+  BE_SPINDLE_USE: VTYPE_INT,
+  }
+
+BES_PARAMETER_COMPAT = {
+  BE_MEMORY: VTYPE_SIZE,
+  }
+BES_PARAMETER_COMPAT.update(BES_PARAMETER_TYPES)
 
 BES_PARAMETERS = frozenset(BES_PARAMETER_TYPES.keys())
 
+# instance specs
+ISPEC_MEM_SIZE = "memory-size"
+ISPEC_CPU_COUNT = "cpu-count"
+ISPEC_DISK_COUNT = "disk-count"
+ISPEC_DISK_SIZE = "disk-size"
+ISPEC_NIC_COUNT = "nic-count"
+ISPEC_SPINDLE_USE = "spindle-use"
+
+ISPECS_PARAMETER_TYPES = {
+  ISPEC_MEM_SIZE: VTYPE_INT,
+  ISPEC_CPU_COUNT: VTYPE_INT,
+  ISPEC_DISK_COUNT: VTYPE_INT,
+  ISPEC_DISK_SIZE: VTYPE_INT,
+  ISPEC_NIC_COUNT: VTYPE_INT,
+  ISPEC_SPINDLE_USE: VTYPE_INT,
+  }
+
+ISPECS_PARAMETERS = frozenset(ISPECS_PARAMETER_TYPES.keys())
+
+ISPECS_MIN = "min"
+ISPECS_MAX = "max"
+ISPECS_STD = "std"
+IPOLICY_DTS = "disk-templates"
+IPOLICY_VCPU_RATIO = "vcpu-ratio"
+IPOLICY_SPINDLE_RATIO = "spindle-ratio"
+
+IPOLICY_ISPECS = frozenset([
+  ISPECS_MIN,
+  ISPECS_MAX,
+  ISPECS_STD,
+  ])
+
+IPOLICY_PARAMETERS = frozenset([
+  IPOLICY_VCPU_RATIO,
+  IPOLICY_SPINDLE_RATIO,
+  ])
+
+IPOLICY_ALL_KEYS = (IPOLICY_ISPECS |
+                    IPOLICY_PARAMETERS |
+                    frozenset([IPOLICY_DTS]))
+
 # Node parameter names
 ND_OOB_PROGRAM = "oob_program"
+ND_SPINDLE_COUNT = "spindle_count"
 
 NDS_PARAMETER_TYPES = {
-    ND_OOB_PROGRAM: VTYPE_MAYBE_STRING,
-    }
+  ND_OOB_PROGRAM: VTYPE_MAYBE_STRING,
+  ND_SPINDLE_COUNT: VTYPE_INT,
+  }
 
 NDS_PARAMETERS = frozenset(NDS_PARAMETER_TYPES.keys())
 
+# Logical Disks parameters
+LDP_RESYNC_RATE = "resync-rate"
+LDP_STRIPES = "stripes"
+LDP_BARRIERS = "disabled-barriers"
+LDP_NO_META_FLUSH = "disable-meta-flush"
+LDP_DEFAULT_METAVG = "default-metavg"
+LDP_DISK_CUSTOM = "disk-custom"
+LDP_NET_CUSTOM = "net-custom"
+LDP_DYNAMIC_RESYNC = "dynamic-resync"
+LDP_PLAN_AHEAD = "c-plan-ahead"
+LDP_FILL_TARGET = "c-fill-target"
+LDP_DELAY_TARGET = "c-delay-target"
+LDP_MAX_RATE = "c-max-rate"
+LDP_MIN_RATE = "c-min-rate"
+LDP_POOL = "pool"
+DISK_LD_TYPES = {
+  LDP_RESYNC_RATE: VTYPE_INT,
+  LDP_STRIPES: VTYPE_INT,
+  LDP_BARRIERS: VTYPE_STRING,
+  LDP_NO_META_FLUSH: VTYPE_BOOL,
+  LDP_DEFAULT_METAVG: VTYPE_STRING,
+  LDP_DISK_CUSTOM: VTYPE_STRING,
+  LDP_NET_CUSTOM: VTYPE_STRING,
+  LDP_DYNAMIC_RESYNC: VTYPE_BOOL,
+  LDP_PLAN_AHEAD: VTYPE_INT,
+  LDP_FILL_TARGET: VTYPE_INT,
+  LDP_DELAY_TARGET: VTYPE_INT,
+  LDP_MAX_RATE: VTYPE_INT,
+  LDP_MIN_RATE: VTYPE_INT,
+  LDP_POOL: VTYPE_STRING,
+  }
+DISK_LD_PARAMETERS = frozenset(DISK_LD_TYPES.keys())
+
+# Disk template parameters (can be set/changed by the user via gnt-cluster and
+# gnt-group)
+DRBD_RESYNC_RATE = "resync-rate"
+DRBD_DATA_STRIPES = "data-stripes"
+DRBD_META_STRIPES = "meta-stripes"
+DRBD_DISK_BARRIERS = "disk-barriers"
+DRBD_META_BARRIERS = "meta-barriers"
+DRBD_DEFAULT_METAVG = "metavg"
+DRBD_DISK_CUSTOM = "disk-custom"
+DRBD_NET_CUSTOM = "net-custom"
+DRBD_DYNAMIC_RESYNC = "dynamic-resync"
+DRBD_PLAN_AHEAD = "c-plan-ahead"
+DRBD_FILL_TARGET = "c-fill-target"
+DRBD_DELAY_TARGET = "c-delay-target"
+DRBD_MAX_RATE = "c-max-rate"
+DRBD_MIN_RATE = "c-min-rate"
+LV_STRIPES = "stripes"
+RBD_POOL = "pool"
+DISK_DT_TYPES = {
+  DRBD_RESYNC_RATE: VTYPE_INT,
+  DRBD_DATA_STRIPES: VTYPE_INT,
+  DRBD_META_STRIPES: VTYPE_INT,
+  DRBD_DISK_BARRIERS: VTYPE_STRING,
+  DRBD_META_BARRIERS: VTYPE_BOOL,
+  DRBD_DEFAULT_METAVG: VTYPE_STRING,
+  DRBD_DISK_CUSTOM: VTYPE_STRING,
+  DRBD_NET_CUSTOM: VTYPE_STRING,
+  DRBD_DYNAMIC_RESYNC: VTYPE_BOOL,
+  DRBD_PLAN_AHEAD: VTYPE_INT,
+  DRBD_FILL_TARGET: VTYPE_INT,
+  DRBD_DELAY_TARGET: VTYPE_INT,
+  DRBD_MAX_RATE: VTYPE_INT,
+  DRBD_MIN_RATE: VTYPE_INT,
+  LV_STRIPES: VTYPE_INT,
+  RBD_POOL: VTYPE_STRING,
+  }
+
+DISK_DT_PARAMETERS = frozenset(DISK_DT_TYPES.keys())
+
 # OOB supported commands
 OOB_POWER_ON = "power-on"
 OOB_POWER_OFF = "power-off"
@@ -816,9 +1093,9 @@ NIC_MODE_ROUTED = "routed"
 NIC_VALID_MODES = frozenset([NIC_MODE_BRIDGED, NIC_MODE_ROUTED])
 
 NICS_PARAMETER_TYPES = {
-    NIC_MODE: VTYPE_STRING,
-    NIC_LINK: VTYPE_STRING,
-    }
+  NIC_MODE: VTYPE_STRING,
+  NIC_LINK: VTYPE_STRING,
+  }
 
 NICS_PARAMETERS = frozenset(NICS_PARAMETER_TYPES.keys())
 
@@ -949,6 +1226,45 @@ HT_KVM_VALID_BO_TYPES = frozenset([
   HT_BO_NETWORK
   ])
 
+# SPICE lossless image compression options
+HT_KVM_SPICE_LOSSLESS_IMG_COMPR_AUTO_GLZ = "auto_glz"
+HT_KVM_SPICE_LOSSLESS_IMG_COMPR_AUTO_LZ = "auto_lz"
+HT_KVM_SPICE_LOSSLESS_IMG_COMPR_QUIC = "quic"
+HT_KVM_SPICE_LOSSLESS_IMG_COMPR_GLZ = "glz"
+HT_KVM_SPICE_LOSSLESS_IMG_COMPR_LZ = "lz"
+HT_KVM_SPICE_LOSSLESS_IMG_COMPR_OFF = "off"
+
+HT_KVM_SPICE_VALID_LOSSLESS_IMG_COMPR_OPTIONS = frozenset([
+  HT_KVM_SPICE_LOSSLESS_IMG_COMPR_AUTO_GLZ,
+  HT_KVM_SPICE_LOSSLESS_IMG_COMPR_AUTO_LZ,
+  HT_KVM_SPICE_LOSSLESS_IMG_COMPR_QUIC,
+  HT_KVM_SPICE_LOSSLESS_IMG_COMPR_GLZ,
+  HT_KVM_SPICE_LOSSLESS_IMG_COMPR_LZ,
+  HT_KVM_SPICE_LOSSLESS_IMG_COMPR_OFF,
+  ])
+
+# SPICE lossy image compression options (valid for both jpeg and zlib-glz)
+HT_KVM_SPICE_LOSSY_IMG_COMPR_AUTO = "auto"
+HT_KVM_SPICE_LOSSY_IMG_COMPR_NEVER = "never"
+HT_KVM_SPICE_LOSSY_IMG_COMPR_ALWAYS = "always"
+
+HT_KVM_SPICE_VALID_LOSSY_IMG_COMPR_OPTIONS = frozenset([
+  HT_KVM_SPICE_LOSSY_IMG_COMPR_AUTO,
+  HT_KVM_SPICE_LOSSY_IMG_COMPR_NEVER,
+  HT_KVM_SPICE_LOSSY_IMG_COMPR_ALWAYS,
+  ])
+
+# SPICE video stream detection
+HT_KVM_SPICE_VIDEO_STREAM_DETECTION_OFF = "off"
+HT_KVM_SPICE_VIDEO_STREAM_DETECTION_ALL = "all"
+HT_KVM_SPICE_VIDEO_STREAM_DETECTION_FILTER = "filter"
+
+HT_KVM_SPICE_VALID_VIDEO_STREAM_DETECTION_OPTIONS = frozenset([
+  HT_KVM_SPICE_VIDEO_STREAM_DETECTION_OFF,
+  HT_KVM_SPICE_VIDEO_STREAM_DETECTION_ALL,
+  HT_KVM_SPICE_VIDEO_STREAM_DETECTION_FILTER,
+  ])
+
 # Security models
 HT_SM_NONE = "none"
 HT_SM_USER = "user"
@@ -971,6 +1287,122 @@ HT_MIGRATION_MODES = frozenset([HT_MIGRATION_LIVE, HT_MIGRATION_NONLIVE])
 VERIFY_NPLUSONE_MEM = "nplusone_mem"
 VERIFY_OPTIONAL_CHECKS = frozenset([VERIFY_NPLUSONE_MEM])
 
+# Cluster Verify error classes
+CV_TCLUSTER = "cluster"
+CV_TNODE = "node"
+CV_TINSTANCE = "instance"
+
+# Cluster Verify error codes and documentation
+CV_ECLUSTERCFG = \
+  (CV_TCLUSTER, "ECLUSTERCFG", "Cluster configuration verification failure")
+CV_ECLUSTERCERT = \
+  (CV_TCLUSTER, "ECLUSTERCERT",
+   "Cluster certificate files verification failure")
+CV_ECLUSTERFILECHECK = \
+  (CV_TCLUSTER, "ECLUSTERFILECHECK",
+   "Cluster configuration verification failure")
+CV_ECLUSTERDANGLINGNODES = \
+  (CV_TNODE, "ECLUSTERDANGLINGNODES",
+   "Some nodes belong to non-existing groups")
+CV_ECLUSTERDANGLINGINST = \
+  (CV_TNODE, "ECLUSTERDANGLINGINST",
+   "Some instances have a non-existing primary node")
+CV_EINSTANCEBADNODE = \
+  (CV_TINSTANCE, "EINSTANCEBADNODE",
+   "Instance marked as running lives on an offline node")
+CV_EINSTANCEDOWN = \
+  (CV_TINSTANCE, "EINSTANCEDOWN", "Instance not running on its primary node")
+CV_EINSTANCELAYOUT = \
+  (CV_TINSTANCE, "EINSTANCELAYOUT", "Instance has multiple secondary nodes")
+CV_EINSTANCEMISSINGDISK = \
+  (CV_TINSTANCE, "EINSTANCEMISSINGDISK", "Missing volume on an instance")
+CV_EINSTANCEFAULTYDISK = \
+  (CV_TINSTANCE, "EINSTANCEFAULTYDISK",
+   "Impossible to retrieve status for a disk")
+CV_EINSTANCEWRONGNODE = \
+  (CV_TINSTANCE, "EINSTANCEWRONGNODE", "Instance running on the wrong node")
+CV_EINSTANCESPLITGROUPS = \
+  (CV_TINSTANCE, "EINSTANCESPLITGROUPS",
+   "Instance with primary and secondary nodes in different groups")
+CV_EINSTANCEPOLICY = \
+  (CV_TINSTANCE, "EINSTANCEPOLICY",
+   "Instance does not meet policy")
+CV_ENODEDRBD = \
+  (CV_TNODE, "ENODEDRBD", "Error parsing the DRBD status file")
+CV_ENODEDRBDHELPER = \
+  (CV_TNODE, "ENODEDRBDHELPER", "Error caused by the DRBD helper")
+CV_ENODEFILECHECK = \
+  (CV_TNODE, "ENODEFILECHECK",
+   "Error retrieving the checksum of the node files")
+CV_ENODEHOOKS = \
+  (CV_TNODE, "ENODEHOOKS", "Communication failure in hooks execution")
+CV_ENODEHV = \
+  (CV_TNODE, "ENODEHV", "Hypervisor parameters verification failure")
+CV_ENODELVM = \
+  (CV_TNODE, "ENODELVM", "LVM-related node error")
+CV_ENODEN1 = \
+  (CV_TNODE, "ENODEN1", "Not enough memory to accommodate instance failovers")
+CV_ENODENET = \
+  (CV_TNODE, "ENODENET", "Network-related node error")
+CV_ENODEOS = \
+  (CV_TNODE, "ENODEOS", "OS-related node error")
+CV_ENODEORPHANINSTANCE = \
+  (CV_TNODE, "ENODEORPHANINSTANCE", "Unknown intance running on a node")
+CV_ENODEORPHANLV = \
+  (CV_TNODE, "ENODEORPHANLV", "Unknown LVM logical volume")
+CV_ENODERPC = \
+  (CV_TNODE, "ENODERPC",
+   "Error during connection to the primary node of an instance")
+CV_ENODESSH = \
+  (CV_TNODE, "ENODESSH", "SSH-related node error")
+CV_ENODEVERSION = \
+  (CV_TNODE, "ENODEVERSION",
+   "Protocol version mismatch or Ganeti version mismatch")
+CV_ENODESETUP = \
+  (CV_TNODE, "ENODESETUP", "Node setup error")
+CV_ENODETIME = \
+  (CV_TNODE, "ENODETIME", "Node returned invalid time")
+CV_ENODEOOBPATH = \
+  (CV_TNODE, "ENODEOOBPATH", "Invalid Out Of Band path")
+CV_ENODEUSERSCRIPTS = \
+  (CV_TNODE, "ENODEUSERSCRIPTS", "User scripts not present or not executable")
+
+CV_ALL_ECODES = frozenset([
+  CV_ECLUSTERCFG,
+  CV_ECLUSTERCERT,
+  CV_ECLUSTERFILECHECK,
+  CV_ECLUSTERDANGLINGNODES,
+  CV_ECLUSTERDANGLINGINST,
+  CV_EINSTANCEBADNODE,
+  CV_EINSTANCEDOWN,
+  CV_EINSTANCELAYOUT,
+  CV_EINSTANCEMISSINGDISK,
+  CV_EINSTANCEFAULTYDISK,
+  CV_EINSTANCEWRONGNODE,
+  CV_EINSTANCESPLITGROUPS,
+  CV_EINSTANCEPOLICY,
+  CV_ENODEDRBD,
+  CV_ENODEDRBDHELPER,
+  CV_ENODEFILECHECK,
+  CV_ENODEHOOKS,
+  CV_ENODEHV,
+  CV_ENODELVM,
+  CV_ENODEN1,
+  CV_ENODENET,
+  CV_ENODEOS,
+  CV_ENODEORPHANINSTANCE,
+  CV_ENODEORPHANLV,
+  CV_ENODERPC,
+  CV_ENODESSH,
+  CV_ENODEVERSION,
+  CV_ENODESETUP,
+  CV_ENODETIME,
+  CV_ENODEOOBPATH,
+  CV_ENODEUSERSCRIPTS,
+  ])
+
+CV_ALL_ECODES_STRINGS = frozenset(estr for (_, estr, _) in CV_ALL_ECODES)
+
 # Node verify constants
 NV_DRBDHELPER = "drbd-helper"
 NV_DRBDLIST = "drbd-list"
@@ -992,10 +1424,12 @@ NV_VGLIST = "vglist"
 NV_VMNODES = "vmnodes"
 NV_OOB_PATHS = "oob-paths"
 NV_BRIDGES = "bridges"
+NV_USERSCRIPTS = "user-scripts"
 
 # Instance status
 INSTST_RUNNING = "running"
 INSTST_ADMINDOWN = "ADMIN_down"
+INSTST_ADMINOFFLINE = "ADMIN_offline"
 INSTST_NODEOFFLINE = "ERROR_nodeoffline"
 INSTST_NODEDOWN = "ERROR_nodedown"
 INSTST_WRONGNODE = "ERROR_wrongnode"
@@ -1004,6 +1438,7 @@ INSTST_ERRORDOWN = "ERROR_down"
 INSTST_ALL = frozenset([
   INSTST_RUNNING,
   INSTST_ADMINDOWN,
+  INSTST_ADMINOFFLINE,
   INSTST_NODEOFFLINE,
   INSTST_NODEDOWN,
   INSTST_WRONGNODE,
@@ -1011,6 +1446,16 @@ INSTST_ALL = frozenset([
   INSTST_ERRORDOWN,
   ])
 
+# Admin states
+ADMINST_UP = "up"
+ADMINST_DOWN = "down"
+ADMINST_OFFLINE = "offline"
+ADMINST_ALL = frozenset([
+  ADMINST_UP,
+  ADMINST_DOWN,
+  ADMINST_OFFLINE,
+  ])
+
 # Node roles
 NR_REGULAR = "R"
 NR_MASTER = "M"
@@ -1058,6 +1503,16 @@ IALLOCATOR_NEVAC_MODES = frozenset([
   IALLOCATOR_NEVAC_ALL,
   ])
 
+# Node evacuation
+NODE_EVAC_PRI = "primary-only"
+NODE_EVAC_SEC = "secondary-only"
+NODE_EVAC_ALL = "all"
+NODE_EVAC_MODES = frozenset([
+  NODE_EVAC_PRI,
+  NODE_EVAC_SEC,
+  NODE_EVAC_ALL,
+  ])
+
 # Job queue
 JOB_QUEUE_VERSION = 1
 JOB_QUEUE_LOCK_FILE = QUEUE_DIR + "/lock"
@@ -1066,8 +1521,6 @@ JOB_QUEUE_SERIAL_FILE = QUEUE_DIR + "/serial"
 JOB_QUEUE_ARCHIVE_DIR = QUEUE_DIR + "/archive"
 JOB_QUEUE_DRAIN_FILE = QUEUE_DIR + "/drain"
 JOB_QUEUE_SIZE_HARD_LIMIT = 5000
-JOB_QUEUE_DIRS = [QUEUE_DIR, JOB_QUEUE_ARCHIVE_DIR]
-JOB_QUEUE_DIRS_MODE = SECURE_DIR_MODE
 
 JOB_ID_TEMPLATE = r"\d+"
 JOB_FILE_RE = re.compile(r"^job-(%s)$" % JOB_ID_TEMPLATE)
@@ -1127,9 +1580,21 @@ OP_PRIO_SUBMIT_VALID = frozenset([
 
 OP_PRIO_DEFAULT = OP_PRIO_NORMAL
 
+# Lock recalculate mode
+LOCKS_REPLACE = "replace"
+LOCKS_APPEND = "append"
+
+# Lock timeout (sum) before we should go into blocking acquire (still
+# can be reset by priority change); computed as max time (10 hours)
+# before we should actually go into blocking acquire given that we
+# start from default priority level; in seconds
+# TODO
+LOCK_ATTEMPTS_TIMEOUT = 10 * 3600 / (OP_PRIO_DEFAULT - OP_PRIO_HIGHEST)
+LOCK_ATTEMPTS_MAXWAIT = 15.0
+LOCK_ATTEMPTS_MINWAIT = 1.0
+
 # Execution log types
 ELOG_MESSAGE = "message"
-ELOG_PROGRESS = "progress"
 ELOG_REMOTE_IMPORT = "remote-import"
 ELOG_JQUEUE_TEST = "jqueue-test"
 
@@ -1151,18 +1616,29 @@ JQT_ALL = frozenset([
   ])
 
 # Query resources
+QR_CLUSTER = "cluster"
 QR_INSTANCE = "instance"
 QR_NODE = "node"
 QR_LOCK = "lock"
 QR_GROUP = "group"
 QR_OS = "os"
+QR_JOB = "job"
+QR_EXPORT = "export"
 
 #: List of resources which can be queried using L{opcodes.OpQuery}
-QR_VIA_OP = frozenset([QR_INSTANCE, QR_NODE, QR_GROUP, QR_OS])
+QR_VIA_OP = frozenset([
+  QR_CLUSTER,
+  QR_INSTANCE,
+  QR_NODE,
+  QR_GROUP,
+  QR_OS,
+  QR_EXPORT,
+  ])
 
 #: List of resources which can be queried using Local UniX Interface
 QR_VIA_LUXI = QR_VIA_OP.union([
   QR_LOCK,
+  QR_JOB,
   ])
 
 #: List of resources which can be queried using RAPI
@@ -1223,6 +1699,8 @@ RSS_DESCRIPTION = {
 MAX_NICS = 8
 MAX_DISKS = 16
 
+# SSCONF file prefix
+SSCONF_FILEPREFIX = "ssconf_"
 # SSCONF keys
 SS_CLUSTER_NAME = "cluster_name"
 SS_CLUSTER_TAGS = "cluster_tags"
@@ -1232,6 +1710,7 @@ SS_MASTER_CANDIDATES = "master_candidates"
 SS_MASTER_CANDIDATES_IPS = "master_candidates_ips"
 SS_MASTER_IP = "master_ip"
 SS_MASTER_NETDEV = "master_netdev"
+SS_MASTER_NETMASK = "master_netmask"
 SS_MASTER_NODE = "master_node"
 SS_NODE_LIST = "node_list"
 SS_NODE_PRIMARY_IPS = "node_primary_ips"
@@ -1264,6 +1743,7 @@ HVC_DEFAULTS = {
     HV_MIGRATION_MODE: HT_MIGRATION_LIVE,
     HV_BLOCKDEV_PREFIX: "sd",
     HV_REBOOT_BEHAVIOR: INSTANCE_REBOOT_ALLOWED,
+    HV_CPU_MASK: CPU_PINNING_ALL,
     },
   HT_XEN_HVM: {
     HV_BOOT_ORDER: "cd",
@@ -1281,9 +1761,10 @@ HVC_DEFAULTS = {
     HV_USE_LOCALTIME: False,
     HV_BLOCKDEV_PREFIX: "hd",
     HV_REBOOT_BEHAVIOR: INSTANCE_REBOOT_ALLOWED,
+    HV_CPU_MASK: CPU_PINNING_ALL,
     },
   HT_KVM: {
-    HV_KERNEL_PATH: "/boot/vmlinuz-2.6-kvmU",
+    HV_KERNEL_PATH: KVM_KERNEL,
     HV_INITRD_PATH: "",
     HV_KERNEL_ARGS: "ro",
     HV_ROOT_PATH: "/dev/vda1",
@@ -1296,6 +1777,15 @@ HVC_DEFAULTS = {
     HV_VNC_PASSWORD_FILE: "",
     HV_KVM_SPICE_BIND: "",
     HV_KVM_SPICE_IP_VERSION: IFACE_NO_IP_VERSION_SPECIFIED,
+    HV_KVM_SPICE_PASSWORD_FILE: "",
+    HV_KVM_SPICE_LOSSLESS_IMG_COMPR: "",
+    HV_KVM_SPICE_JPEG_IMG_COMPR: "",
+    HV_KVM_SPICE_ZLIB_GLZ_IMG_COMPR: "",
+    HV_KVM_SPICE_STREAMING_VIDEO_DETECTION: "",
+    HV_KVM_SPICE_AUDIO_COMPR: True,
+    HV_KVM_SPICE_USE_TLS: False,
+    HV_KVM_SPICE_TLS_CIPHERS: OPENSSL_CIPHERS,
+    HV_KVM_SPICE_USE_VDAGENT: True,
     HV_KVM_FLOPPY_IMAGE_PATH: "",
     HV_CDROM_IMAGE_PATH: "",
     HV_KVM_CDROM2_IMAGE_PATH: "",
@@ -1318,6 +1808,7 @@ HVC_DEFAULTS = {
     HV_KVM_USE_CHROOT: False,
     HV_MEM_PATH: "",
     HV_REBOOT_BEHAVIOR: INSTANCE_REBOOT_ALLOWED,
+    HV_CPU_MASK: CPU_PINNING_ALL,
     },
   HT_FAKE: {
     },
@@ -1336,20 +1827,129 @@ HVC_GLOBALS = frozenset([
   ])
 
 BEC_DEFAULTS = {
-  BE_MEMORY: 128,
+  BE_MINMEM: 128,
+  BE_MAXMEM: 128,
   BE_VCPUS: 1,
   BE_AUTO_BALANCE: True,
+  BE_ALWAYS_FAILOVER: False,
+  BE_SPINDLE_USE: 1,
   }
 
 NDC_DEFAULTS = {
   ND_OOB_PROGRAM: None,
+  ND_SPINDLE_COUNT: 1,
   }
 
+DISK_LD_DEFAULTS = {
+  LD_DRBD8: {
+    LDP_RESYNC_RATE: CLASSIC_DRBD_SYNC_SPEED,
+    LDP_BARRIERS: _autoconf.DRBD_BARRIERS,
+    LDP_NO_META_FLUSH: _autoconf.DRBD_NO_META_FLUSH,
+    LDP_DEFAULT_METAVG: DEFAULT_VG,
+    LDP_DISK_CUSTOM: "",
+    LDP_NET_CUSTOM: "",
+    LDP_DYNAMIC_RESYNC: False,
+
+    # The default values for the DRBD dynamic resync speed algorithm are taken
+    # from the drbsetup 8.3.11 man page, except for c-plan-ahead (that we
+    # don't need to set to 0, because we have a separate option to enable it)
+    # and for c-max-rate, that we cap to the default value for the static resync
+    # rate.
+    LDP_PLAN_AHEAD: 20, # ds
+    LDP_FILL_TARGET: 0, # sectors
+    LDP_DELAY_TARGET: 1, # ds
+    LDP_MAX_RATE: CLASSIC_DRBD_SYNC_SPEED, # KiB/s
+    LDP_MIN_RATE: 4 * 1024, # KiB/s
+    },
+  LD_LV: {
+    LDP_STRIPES: _autoconf.LVM_STRIPECOUNT
+    },
+  LD_FILE: {
+    },
+  LD_BLOCKDEV: {
+    },
+  LD_RBD: {
+    LDP_POOL: "rbd"
+    },
+  }
+
+# readability shortcuts
+_LV_DEFAULTS = DISK_LD_DEFAULTS[LD_LV]
+_DRBD_DEFAULTS = DISK_LD_DEFAULTS[LD_DRBD8]
+
+DISK_DT_DEFAULTS = {
+  DT_PLAIN: {
+    LV_STRIPES: DISK_LD_DEFAULTS[LD_LV][LDP_STRIPES],
+    },
+  DT_DRBD8: {
+    DRBD_RESYNC_RATE: _DRBD_DEFAULTS[LDP_RESYNC_RATE],
+    DRBD_DATA_STRIPES: _LV_DEFAULTS[LDP_STRIPES],
+    DRBD_META_STRIPES: _LV_DEFAULTS[LDP_STRIPES],
+    DRBD_DISK_BARRIERS: _DRBD_DEFAULTS[LDP_BARRIERS],
+    DRBD_META_BARRIERS: _DRBD_DEFAULTS[LDP_NO_META_FLUSH],
+    DRBD_DEFAULT_METAVG: _DRBD_DEFAULTS[LDP_DEFAULT_METAVG],
+    DRBD_DISK_CUSTOM: _DRBD_DEFAULTS[LDP_DISK_CUSTOM],
+    DRBD_NET_CUSTOM: _DRBD_DEFAULTS[LDP_NET_CUSTOM],
+    DRBD_DYNAMIC_RESYNC: _DRBD_DEFAULTS[LDP_DYNAMIC_RESYNC],
+    DRBD_PLAN_AHEAD: _DRBD_DEFAULTS[LDP_PLAN_AHEAD],
+    DRBD_FILL_TARGET: _DRBD_DEFAULTS[LDP_FILL_TARGET],
+    DRBD_DELAY_TARGET: _DRBD_DEFAULTS[LDP_DELAY_TARGET],
+    DRBD_MAX_RATE: _DRBD_DEFAULTS[LDP_MAX_RATE],
+    DRBD_MIN_RATE: _DRBD_DEFAULTS[LDP_MIN_RATE],
+    },
+  DT_DISKLESS: {
+    },
+  DT_FILE: {
+    },
+  DT_SHARED_FILE: {
+    },
+  DT_BLOCK: {
+    },
+  DT_RBD: {
+    RBD_POOL: DISK_LD_DEFAULTS[LD_RBD][LDP_POOL]
+    },
+  }
+
+# we don't want to export the shortcuts
+del _LV_DEFAULTS, _DRBD_DEFAULTS
+
 NICC_DEFAULTS = {
   NIC_MODE: NIC_MODE_BRIDGED,
   NIC_LINK: DEFAULT_BRIDGE,
   }
 
+# All of the following values are quite arbitrarily - there are no
+# "good" defaults, these must be customised per-site
+IPOLICY_DEFAULTS = {
+  ISPECS_MIN: {
+    ISPEC_MEM_SIZE: 128,
+    ISPEC_CPU_COUNT: 1,
+    ISPEC_DISK_COUNT: 1,
+    ISPEC_DISK_SIZE: 1024,
+    ISPEC_NIC_COUNT: 1,
+    ISPEC_SPINDLE_USE: 1,
+    },
+  ISPECS_MAX: {
+    ISPEC_MEM_SIZE: 32768,
+    ISPEC_CPU_COUNT: 8,
+    ISPEC_DISK_COUNT: MAX_DISKS,
+    ISPEC_DISK_SIZE: 1024 * 1024,
+    ISPEC_NIC_COUNT: MAX_NICS,
+    ISPEC_SPINDLE_USE: 12,
+    },
+  ISPECS_STD: {
+    ISPEC_MEM_SIZE: 128,
+    ISPEC_CPU_COUNT: 1,
+    ISPEC_DISK_COUNT: 1,
+    ISPEC_DISK_SIZE: 1024,
+    ISPEC_NIC_COUNT: 1,
+    ISPEC_SPINDLE_USE: 1,
+    },
+  IPOLICY_DTS: DISK_TEMPLATES,
+  IPOLICY_VCPU_RATIO: 4.0,
+  IPOLICY_SPINDLE_RATIO: 32.0,
+  }
+
 MASTER_POOL_SIZE_DEFAULT = 10
 
 CONFD_PROTOCOL_VERSION = 1
@@ -1361,6 +1961,7 @@ CONFD_REQ_CLUSTER_MASTER = 3
 CONFD_REQ_NODE_PIP_LIST = 4
 CONFD_REQ_MC_PIP_LIST = 5
 CONFD_REQ_INSTANCES_IPS_LIST = 6
+CONFD_REQ_NODE_DRBD = 7
 
 # Confd request query fields. These are used to narrow down queries.
 # These must be strings rather than integers, because json-encoding
@@ -1382,6 +1983,7 @@ CONFD_REQS = frozenset([
   CONFD_REQ_NODE_PIP_LIST,
   CONFD_REQ_MC_PIP_LIST,
   CONFD_REQ_INSTANCES_IPS_LIST,
+  CONFD_REQ_NODE_DRBD,
   ])
 
 CONFD_REPL_STATUS_OK = 0
@@ -1466,7 +2068,18 @@ VALID_ALLOC_POLICIES = [
 # Temporary external/shared storage parameters
 BLOCKDEV_DRIVER_MANUAL = "manual"
 
+# qemu-img path, required for ovfconverter
+QEMUIMG_PATH = _autoconf.QEMUIMG_PATH
+
 # Whether htools was enabled at compilation time
 HTOOLS = _autoconf.HTOOLS
 # The hail iallocator
 IALLOC_HAIL = "hail"
+
+# Fake opcodes for functions that have hooks attached to them via
+# backend.RunLocalHooks
+FAKE_OP_MASTER_TURNUP = "OP_CLUSTER_IP_TURNUP"
+FAKE_OP_MASTER_TURNDOWN = "OP_CLUSTER_IP_TURNDOWN"
+
+# Do not re-export imported modules
+del re, _vcsversion, _autoconf, socket
index f5e18a2..1685e84 100644 (file)
@@ -75,7 +75,43 @@ class AsyncoreScheduler(sched.scheduler):
 
   """
   def __init__(self, timefunc):
-    sched.scheduler.__init__(self, timefunc, AsyncoreDelayFunction)
+    """Initializes this class.
+
+    """
+    sched.scheduler.__init__(self, timefunc, self._LimitedDelay)
+    self._max_delay = None
+
+  def run(self, max_delay=None): # pylint: disable=W0221
+    """Run any pending events.
+
+    @type max_delay: None or number
+    @param max_delay: Maximum delay (useful if caller has timeouts running)
+
+    """
+    assert self._max_delay is None
+
+    # The delay function used by the scheduler can't be different on each run,
+    # hence an instance variable must be used.
+    if max_delay is None:
+      self._max_delay = None
+    else:
+      self._max_delay = utils.RunningTimeout(max_delay, False)
+
+    try:
+      return sched.scheduler.run(self)
+    finally:
+      self._max_delay = None
+
+  def _LimitedDelay(self, duration):
+    """Custom delay function for C{sched.scheduler}.
+
+    """
+    if self._max_delay is None:
+      timeout = duration
+    else:
+      timeout = min(duration, self._max_delay.Remaining())
+
+    return AsyncoreDelayFunction(timeout)
 
 
 class GanetiBaseAsyncoreDispatcher(asyncore.dispatcher):
@@ -346,7 +382,7 @@ class AsyncUDPSocket(GanetiBaseAsyncoreDispatcher):
 
     """
     if len(payload) > constants.MAX_UDP_DATA_SIZE:
-      raise errors.UdpDataSizeError('Packet too big: %s > %s' % (len(payload),
+      raise errors.UdpDataSizeError("Packet too big: %s > %s" % (len(payload),
                                     constants.MAX_UDP_DATA_SIZE))
     self._out_queue.append((ip, port, payload))
 
@@ -421,6 +457,47 @@ class AsyncAwaker(GanetiBaseAsyncoreDispatcher):
       self.out_socket.send("\0")
 
 
+class _ShutdownCheck:
+  """Logic for L{Mainloop} shutdown.
+
+  """
+  def __init__(self, fn):
+    """Initializes this class.
+
+    @type fn: callable
+    @param fn: Function returning C{None} if mainloop can be stopped or a
+      duration in seconds after which the function should be called again
+    @see: L{Mainloop.Run}
+
+    """
+    assert callable(fn)
+
+    self._fn = fn
+    self._defer = None
+
+  def CanShutdown(self):
+    """Checks whether mainloop can be stopped.
+
+    @rtype: bool
+
+    """
+    if self._defer and self._defer.Remaining() > 0:
+      # A deferred check has already been scheduled
+      return False
+
+    # Ask mainloop driver whether we can stop or should check again
+    timeout = self._fn()
+
+    if timeout is None:
+      # Yes, can stop mainloop
+      return True
+
+    # Schedule another check in the future
+    self._defer = utils.RunningTimeout(timeout, True)
+
+    return False
+
+
 class Mainloop(object):
   """Generic mainloop for daemons
 
@@ -428,6 +505,8 @@ class Mainloop(object):
     timed events
 
   """
+  _SHUTDOWN_TIMEOUT_PRIORITY = -(sys.maxint - 1)
+
   def __init__(self):
     """Constructs a new Mainloop instance.
 
@@ -441,9 +520,13 @@ class Mainloop(object):
   @utils.SignalHandled([signal.SIGCHLD])
   @utils.SignalHandled([signal.SIGTERM])
   @utils.SignalHandled([signal.SIGINT])
-  def Run(self, signal_handlers=None):
+  def Run(self, shutdown_wait_fn=None, signal_handlers=None):
     """Runs the mainloop.
 
+    @type shutdown_wait_fn: callable
+    @param shutdown_wait_fn: Function to check whether loop can be terminated;
+      B{important}: function must be idempotent and must return either None
+      for shutting down or a timeout for another call
     @type signal_handlers: dict
     @param signal_handlers: signal->L{utils.SignalHandler} passed by decorator
 
@@ -451,24 +534,50 @@ class Mainloop(object):
     assert isinstance(signal_handlers, dict) and \
            len(signal_handlers) > 0, \
            "Broken SignalHandled decorator"
-    running = True
+
+    # Counter for received signals
+    shutdown_signals = 0
+
+    # Logic to wait for shutdown
+    shutdown_waiter = None
 
     # Start actual main loop
-    while running:
-      if not self.scheduler.empty():
+    while True:
+      if shutdown_signals == 1 and shutdown_wait_fn is not None:
+        if shutdown_waiter is None:
+          shutdown_waiter = _ShutdownCheck(shutdown_wait_fn)
+
+        # Let mainloop driver decide if we can already abort
+        if shutdown_waiter.CanShutdown():
+          break
+
+        # Re-evaluate in a second
+        timeout = 1.0
+
+      elif shutdown_signals >= 1:
+        # Abort loop if more than one signal has been sent or no callback has
+        # been given
+        break
+
+      else:
+        # Wait forever on I/O events
+        timeout = None
+
+      if self.scheduler.empty():
+        asyncore.loop(count=1, timeout=timeout, use_poll=True)
+      else:
         try:
-          self.scheduler.run()
+          self.scheduler.run(max_delay=timeout)
         except SchedulerBreakout:
           pass
-      else:
-        asyncore.loop(count=1, use_poll=True)
 
       # Check whether a signal was raised
-      for sig in signal_handlers:
-        handler = signal_handlers[sig]
+      for (sig, handler) in signal_handlers.items():
         if handler.called:
           self._CallSignalWaiters(sig)
-          running = sig not in (signal.SIGTERM, signal.SIGINT)
+          if sig in (signal.SIGTERM, signal.SIGINT):
+            logging.info("Received signal %s asking for shutdown", sig)
+            shutdown_signals += 1
           handler.Clear()
 
   def _CallSignalWaiters(self, signum):
@@ -512,6 +621,7 @@ def _VerifyDaemonUser(daemon_name):
     constants.NODED: getents.noded_uid,
     constants.CONFD: getents.confd_uid,
     }
+  assert daemon_name in daemon_uids, "Invalid daemon %s" % daemon_name
 
   return (daemon_uids[daemon_name] == running_uid, running_uid,
           daemon_uids[daemon_name])
@@ -687,7 +797,12 @@ def GenericMain(daemon_name, optionparser,
   signal.signal(signal.SIGHUP,
                 compat.partial(_HandleSigHup, [log_reopen_fn, stdio_reopen_fn]))
 
-  utils.WritePidFile(utils.DaemonPidFileName(daemon_name))
+  try:
+    utils.WritePidFile(utils.DaemonPidFileName(daemon_name))
+  except errors.PidFileLockError, err:
+    print >> sys.stderr, "Error while locking PID file:\n%s" % err
+    sys.exit(constants.EXIT_FAILURE)
+
   try:
     try:
       logging.info("%s daemon startup", daemon_name)
index 948920c..ca3e4d8 100644 (file)
@@ -82,6 +82,12 @@ class LockError(GenericError):
   pass
 
 
+class PidFileLockError(LockError):
+  """PID file is already locked by another process.
+
+  """
+
+
 class HypervisorError(GenericError):
   """Hypervisor-related exception.
 
@@ -277,6 +283,14 @@ class SshKeyError(GenericError):
   """
 
 
+class X509CertError(GenericError):
+  """Invalid X509 certificate.
+
+  This error has two arguments: the certificate filename and the error cause.
+
+  """
+
+
 class TagError(GenericError):
   """Generic tag error.
 
@@ -415,6 +429,12 @@ class QueryFilterParseError(ParseError):
             str(inner)]
 
 
+class RapiTestResult(GenericError):
+  """Exception containing results from RAPI test utilities.
+
+  """
+
+
 # errors should be added above
 
 
index 4a511dc..06ea165 100644 (file)
--- a/lib/ht.py
+++ b/lib/ht.py
@@ -322,10 +322,16 @@ TMaybeDict = TOr(TDict, TNone)
 TPositiveInt = \
   TAnd(TInt, WithDesc("EqualGreaterZero")(lambda v: v >= 0))
 
+#: a maybe positive integer (positive integer or None)
+TMaybePositiveInt = TOr(TPositiveInt, TNone)
+
 #: a strictly positive integer
 TStrictPositiveInt = \
   TAnd(TInt, WithDesc("GreaterThanZero")(lambda v: v > 0))
 
+#: a maybe strictly positive integer (strictly positive integer or None)
+TMaybeStrictPositiveInt = TOr(TStrictPositiveInt, TNone)
+
 #: a strictly negative integer (0 > value)
 TStrictNegativeInt = \
   TAnd(TInt, WithDesc("LessThanZero")(compat.partial(operator.gt, 0)))
@@ -354,6 +360,9 @@ def TListOf(my_type):
   return desc(TAnd(TList, lambda lst: compat.all(my_type(v) for v in lst)))
 
 
+TMaybeListOf = lambda item_type: TOr(TNone, TListOf(item_type))
+
+
 def TDictOf(key_type, val_type):
   """Checks a dict type for the type of its key/values.
 
index 84cd0b7..849a195 100644 (file)
 
 import logging
 import pycurl
+import threading
 from cStringIO import StringIO
 
 from ganeti import http
 from ganeti import compat
 from ganeti import netutils
+from ganeti import locking
 
 
 class HttpClientRequest(object):
   def __init__(self, host, port, method, path, headers=None, post_data=None,
-               read_timeout=None, curl_config_fn=None):
+               read_timeout=None, curl_config_fn=None, nicename=None,
+               completion_cb=None):
     """Describes an HTTP request.
 
     @type host: string
@@ -53,14 +56,17 @@ class HttpClientRequest(object):
         timeout while reading the response from the server
     @type curl_config_fn: callable
     @param curl_config_fn: Function to configure cURL object before request
-                           (Note: if the function configures the connection in
-                           a way where it wouldn't be efficient to reuse them,
-                           a "identity" property should be defined, see
-                           L{HttpClientRequest.identity})
+    @type nicename: string
+    @param nicename: Name, presentable to a user, to describe this request (no
+                     whitespace)
+    @type completion_cb: callable accepting this request object as a single
+                         parameter
+    @param completion_cb: Callback for request completion
 
     """
     assert path.startswith("/"), "Path must start with slash (/)"
     assert curl_config_fn is None or callable(curl_config_fn)
+    assert completion_cb is None or callable(completion_cb)
 
     # Request attributes
     self.host = host
@@ -69,6 +75,8 @@ class HttpClientRequest(object):
     self.path = path
     self.read_timeout = read_timeout
     self.curl_config_fn = curl_config_fn
+    self.nicename = nicename
+    self.completion_cb = completion_cb
 
     if post_data is None:
       self.post_data = ""
@@ -112,58 +120,77 @@ class HttpClientRequest(object):
     # TODO: Support for non-SSL requests
     return "https://%s%s" % (address, self.path)
 
-  @property
-  def identity(self):
-    """Returns identifier for retrieving a pooled connection for this request.
 
-    This allows cURL client objects to be re-used and to cache information
-    (e.g. SSL session IDs or connections).
+def _StartRequest(curl, req):
+  """Starts a request on a cURL object.
 
-    """
-    parts = [self.host, self.port]
+  @type curl: pycurl.Curl
+  @param curl: cURL object
+  @type req: L{HttpClientRequest}
+  @param req: HTTP request
 
-    if self.curl_config_fn:
-      try:
-        parts.append(self.curl_config_fn.identity)
-      except AttributeError:
-        pass
+  """
+  logging.debug("Starting request %r", req)
 
-    return "/".join(str(i) for i in parts)
+  url = req.url
+  method = req.method
+  post_data = req.post_data
+  headers = req.headers
 
+  # PycURL requires strings to be non-unicode
+  assert isinstance(method, str)
+  assert isinstance(url, str)
+  assert isinstance(post_data, str)
+  assert compat.all(isinstance(i, str) for i in headers)
 
-class _HttpClient(object):
-  def __init__(self, curl_config_fn):
-    """Initializes this class.
+  # Buffer for response
+  resp_buffer = StringIO()
 
-    @type curl_config_fn: callable
-    @param curl_config_fn: Function to configure cURL object after
-                           initialization
+  # Configure client for request
+  curl.setopt(pycurl.VERBOSE, False)
+  curl.setopt(pycurl.NOSIGNAL, True)
+  curl.setopt(pycurl.USERAGENT, http.HTTP_GANETI_VERSION)
+  curl.setopt(pycurl.PROXY, "")
+  curl.setopt(pycurl.CUSTOMREQUEST, str(method))
+  curl.setopt(pycurl.URL, url)
+  curl.setopt(pycurl.POSTFIELDS, post_data)
+  curl.setopt(pycurl.HTTPHEADER, headers)
 
-    """
-    self._req = None
+  if req.read_timeout is None:
+    curl.setopt(pycurl.TIMEOUT, 0)
+  else:
+    curl.setopt(pycurl.TIMEOUT, int(req.read_timeout))
 
-    curl = self._CreateCurlHandle()
-    curl.setopt(pycurl.VERBOSE, False)
-    curl.setopt(pycurl.NOSIGNAL, True)
-    curl.setopt(pycurl.USERAGENT, http.HTTP_GANETI_VERSION)
-    curl.setopt(pycurl.PROXY, "")
+  # Disable SSL session ID caching (pycurl >= 7.16.0)
+  if hasattr(pycurl, "SSL_SESSIONID_CACHE"):
+    curl.setopt(pycurl.SSL_SESSIONID_CACHE, False)
 
-    # Disable SSL session ID caching (pycurl >= 7.16.0)
-    if hasattr(pycurl, "SSL_SESSIONID_CACHE"):
-      curl.setopt(pycurl.SSL_SESSIONID_CACHE, False)
+  curl.setopt(pycurl.WRITEFUNCTION, resp_buffer.write)
 
-    # Pass cURL object to external config function
-    if curl_config_fn:
-      curl_config_fn(curl)
+  # Pass cURL object to external config function
+  if req.curl_config_fn:
+    req.curl_config_fn(curl)
 
-    self._curl = curl
+  return _PendingRequest(curl, req, resp_buffer.getvalue)
 
-  @staticmethod
-  def _CreateCurlHandle():
-    """Returns a new cURL object.
+
+class _PendingRequest:
+  def __init__(self, curl, req, resp_buffer_read):
+    """Initializes this class.
+
+    @type curl: pycurl.Curl
+    @param curl: cURL object
+    @type req: L{HttpClientRequest}
+    @param req: HTTP request
+    @type resp_buffer_read: callable
+    @param resp_buffer_read: Function to read response body
 
     """
-    return pycurl.Curl()
+    assert req.success is None
+
+    self._curl = curl
+    self._req = req
+    self._resp_buffer_read = resp_buffer_read
 
   def GetCurlHandle(self):
     """Returns the cURL object.
@@ -174,53 +201,9 @@ class _HttpClient(object):
   def GetCurrentRequest(self):
     """Returns the current request.
 
-    @rtype: L{HttpClientRequest} or None
-
     """
     return self._req
 
-  def StartRequest(self, req):
-    """Starts a request on this client.
-
-    @type req: L{HttpClientRequest}
-    @param req: HTTP request
-
-    """
-    assert not self._req, "Another request is already started"
-
-    logging.debug("Starting request %r", req)
-
-    self._req = req
-    self._resp_buffer = StringIO()
-
-    url = req.url
-    method = req.method
-    post_data = req.post_data
-    headers = req.headers
-
-    # PycURL requires strings to be non-unicode
-    assert isinstance(method, str)
-    assert isinstance(url, str)
-    assert isinstance(post_data, str)
-    assert compat.all(isinstance(i, str) for i in headers)
-
-    # Configure cURL object for request
-    curl = self._curl
-    curl.setopt(pycurl.CUSTOMREQUEST, str(method))
-    curl.setopt(pycurl.URL, url)
-    curl.setopt(pycurl.POSTFIELDS, post_data)
-    curl.setopt(pycurl.WRITEFUNCTION, self._resp_buffer.write)
-    curl.setopt(pycurl.HTTPHEADER, headers)
-
-    if req.read_timeout is None:
-      curl.setopt(pycurl.TIMEOUT, 0)
-    else:
-      curl.setopt(pycurl.TIMEOUT, int(req.read_timeout))
-
-    # Pass cURL object to external config function
-    if req.curl_config_fn:
-      req.curl_config_fn(curl)
-
   def Done(self, errmsg):
     """Finishes a request.
 
@@ -228,236 +211,183 @@ class _HttpClient(object):
     @param errmsg: Error message if request failed
 
     """
+    curl = self._curl
     req = self._req
-    assert req, "No request"
 
-    logging.debug("Request %s finished, errmsg=%s", req, errmsg)
+    assert req.success is None, "Request has already been finalized"
 
-    curl = self._curl
+    logging.debug("Request %s finished, errmsg=%s", req, errmsg)
 
     req.success = not bool(errmsg)
     req.error = errmsg
 
     # Get HTTP response code
     req.resp_status_code = curl.getinfo(pycurl.RESPONSE_CODE)
-    req.resp_body = self._resp_buffer.getvalue()
-
-    # Reset client object
-    self._req = None
-    self._resp_buffer = None
+    req.resp_body = self._resp_buffer_read()
 
     # Ensure no potentially large variables are referenced
     curl.setopt(pycurl.POSTFIELDS, "")
     curl.setopt(pycurl.WRITEFUNCTION, lambda _: None)
 
+    if req.completion_cb:
+      req.completion_cb(req)
 
-class _PooledHttpClient:
-  """Data structure for HTTP client pool.
-
-  """
-  def __init__(self, identity, client):
-    """Initializes this class.
-
-    @type identity: string
-    @param identity: Client identifier for pool
-    @type client: L{_HttpClient}
-    @param client: HTTP client
-
-    """
-    self.identity = identity
-    self.client = client
-    self.lastused = 0
-
-  def __repr__(self):
-    status = ["%s.%s" % (self.__class__.__module__, self.__class__.__name__),
-              "id=%s" % self.identity,
-              "lastuse=%s" % self.lastused,
-              repr(self.client)]
 
-    return "<%s at %#x>" % (" ".join(status), id(self))
+class _NoOpRequestMonitor: # pylint: disable=W0232
+  """No-op request monitor.
 
+  """
+  @staticmethod
+  def acquire(*args, **kwargs):
+    pass
 
-class HttpClientPool:
-  """A simple HTTP client pool.
+  release = acquire
+  Disable = acquire
 
-  Supports one pooled connection per identity (see
-  L{HttpClientRequest.identity}).
 
-  """
-  #: After how many generations to drop unused clients
-  _MAX_GENERATIONS_DROP = 25
+class _PendingRequestMonitor:
+  _LOCK = "_lock"
 
-  def __init__(self, curl_config_fn):
+  def __init__(self, owner, pending_fn):
     """Initializes this class.
 
-    @type curl_config_fn: callable
-    @param curl_config_fn: Function to configure cURL object after
-                           initialization
-
     """
-    self._curl_config_fn = curl_config_fn
-    self._generation = 0
-    self._pool = {}
+    self._owner = owner
+    self._pending_fn = pending_fn
 
-    # Create custom logger for HTTP client pool. Change logging level to
-    # C{logging.NOTSET} to get more details.
-    self._logger = logging.getLogger(self.__class__.__name__)
-    self._logger.setLevel(logging.INFO)
+    # The lock monitor runs in another thread, hence locking is necessary
+    self._lock = locking.SharedLock("PendingHttpRequests")
+    self.acquire = self._lock.acquire
+    self.release = self._lock.release
 
-  @staticmethod
-  def _GetHttpClientCreator():
-    """Returns callable to create HTTP client.
+  @locking.ssynchronized(_LOCK)
+  def Disable(self):
+    """Disable monitor.
 
     """
-    return _HttpClient
+    self._pending_fn = None
 
-  def _Get(self, identity):
-    """Gets an HTTP client from the pool.
+  @locking.ssynchronized(_LOCK, shared=1)
+  def GetLockInfo(self, requested): # pylint: disable=W0613
+    """Retrieves information about pending requests.
 
-    @type identity: string
-    @param identity: Client identifier
+    @type requested: set
+    @param requested: Requested information, see C{query.LQ_*}
 
     """
-    try:
-      pclient = self._pool.pop(identity)
-    except KeyError:
-      # Need to create new client
-      client = self._GetHttpClientCreator()(self._curl_config_fn)
-      pclient = _PooledHttpClient(identity, client)
-      self._logger.debug("Created new client %s", pclient)
-    else:
-      self._logger.debug("Reusing client %s", pclient)
+    # No need to sort here, that's being done by the lock manager and query
+    # library. There are no priorities for requests, hence all show up as
+    # one item under "pending".
+    result = []
 
-    assert pclient.identity == identity
+    if self._pending_fn:
+      owner_name = self._owner.getName()
 
-    return pclient
+      for client in self._pending_fn():
+        req = client.GetCurrentRequest()
+        if req:
+          if req.nicename is None:
+            name = "%s%s" % (req.host, req.path)
+          else:
+            name = req.nicename
+          result.append(("rpc/%s" % name, None, [owner_name], None))
 
-  def _StartRequest(self, req):
-    """Starts a request.
+    return result
 
-    @type req: L{HttpClientRequest}
-    @param req: HTTP request
 
-    """
-    pclient = self._Get(req.identity)
+def _ProcessCurlRequests(multi, requests):
+  """cURL request processor.
 
-    assert req.identity not in self._pool
+  This generator yields a tuple once for every completed request, successful or
+  not. The first value in the tuple is the handle, the second an error message
+  or C{None} for successful requests.
 
-    pclient.client.StartRequest(req)
-    pclient.lastused = self._generation
+  @type multi: C{pycurl.CurlMulti}
+  @param multi: cURL multi object
+  @type requests: sequence
+  @param requests: cURL request handles
 
-    return pclient
-
-  def _Return(self, pclients):
-    """Returns HTTP clients to the pool.
+  """
+  for curl in requests:
+    multi.add_handle(curl)
 
-    """
-    for pc in pclients:
-      self._logger.debug("Returning client %s to pool", pc)
-      assert pc.identity not in self._pool
-      assert pc not in self._pool.values()
-      self._pool[pc.identity] = pc
-
-    # Check for unused clients
-    for pc in self._pool.values():
-      if (pc.lastused + self._MAX_GENERATIONS_DROP) < self._generation:
-        self._logger.debug("Removing client %s which hasn't been used"
-                           " for %s generations",
-                           pc, self._MAX_GENERATIONS_DROP)
-        self._pool.pop(pc.identity, None)
-
-    assert compat.all(pc.lastused >= (self._generation -
-                                      self._MAX_GENERATIONS_DROP)
-                      for pc in self._pool.values())
+  while True:
+    (ret, active) = multi.perform()
+    assert ret in (pycurl.E_MULTI_OK, pycurl.E_CALL_MULTI_PERFORM)
 
-  @staticmethod
-  def _CreateCurlMultiHandle():
-    """Creates new cURL multi handle.
+    if ret == pycurl.E_CALL_MULTI_PERFORM:
+      # cURL wants to be called again
+      continue
 
-    """
-    return pycurl.CurlMulti()
+    while True:
+      (remaining_messages, successful, failed) = multi.info_read()
 
-  def ProcessRequests(self, requests):
-    """Processes any number of HTTP client requests using pooled objects.
+      for curl in successful:
+        multi.remove_handle(curl)
+        yield (curl, None)
 
-    @type requests: list of L{HttpClientRequest}
-    @param requests: List of all requests
+      for curl, errnum, errmsg in failed:
+        multi.remove_handle(curl)
+        yield (curl, "Error %s: %s" % (errnum, errmsg))
 
-    """
-    multi = self._CreateCurlMultiHandle()
+      if remaining_messages == 0:
+        break
 
-    # For client cleanup
-    self._generation += 1
+    if active == 0:
+      # No active handles anymore
+      break
 
-    assert compat.all((req.error is None and
-                       req.success is None and
-                       req.resp_status_code is None and
-                       req.resp_body is None)
-                      for req in requests)
+    # Wait for I/O. The I/O timeout shouldn't be too long so that HTTP
+    # timeouts, which are only evaluated in multi.perform, aren't
+    # unnecessarily delayed.
+    multi.select(1.0)
 
-    curl_to_pclient = {}
-    for req in requests:
-      pclient = self._StartRequest(req)
-      curl = pclient.client.GetCurlHandle()
-      curl_to_pclient[curl] = pclient
-      multi.add_handle(curl)
-      assert pclient.client.GetCurrentRequest() == req
-      assert pclient.lastused >= 0
 
-    assert len(curl_to_pclient) == len(requests)
+def ProcessRequests(requests, lock_monitor_cb=None, _curl=pycurl.Curl,
+                    _curl_multi=pycurl.CurlMulti,
+                    _curl_process=_ProcessCurlRequests):
+  """Processes any number of HTTP client requests.
 
-    done_count = 0
-    while True:
-      (ret, _) = multi.perform()
-      assert ret in (pycurl.E_MULTI_OK, pycurl.E_CALL_MULTI_PERFORM)
-
-      if ret == pycurl.E_CALL_MULTI_PERFORM:
-        # cURL wants to be called again
-        continue
-
-      while True:
-        (remaining_messages, successful, failed) = multi.info_read()
-
-        for curl in successful:
-          multi.remove_handle(curl)
-          done_count += 1
-          pclient = curl_to_pclient[curl]
-          req = pclient.client.GetCurrentRequest()
-          pclient.client.Done(None)
-          assert req.success
-          assert not pclient.client.GetCurrentRequest()
-
-        for curl, errnum, errmsg in failed:
-          multi.remove_handle(curl)
-          done_count += 1
-          pclient = curl_to_pclient[curl]
-          req = pclient.client.GetCurrentRequest()
-          pclient.client.Done("Error %s: %s" % (errnum, errmsg))
-          assert req.error
-          assert not pclient.client.GetCurrentRequest()
-
-        if remaining_messages == 0:
-          break
-
-      assert done_count <= len(requests)
-
-      if done_count == len(requests):
-        break
+  @type requests: list of L{HttpClientRequest}
+  @param requests: List of all requests
+  @param lock_monitor_cb: Callable for registering with lock monitor
 
-      # Wait for I/O. The I/O timeout shouldn't be too long so that HTTP
-      # timeouts, which are only evaluated in multi.perform, aren't
-      # unnecessarily delayed.
-      multi.select(1.0)
+  """
+  assert compat.all((req.error is None and
+                     req.success is None and
+                     req.resp_status_code is None and
+                     req.resp_body is None)
+                    for req in requests)
+
+  # Prepare all requests
+  curl_to_client = \
+    dict((client.GetCurlHandle(), client)
+         for client in map(lambda req: _StartRequest(_curl(), req), requests))
+
+  assert len(curl_to_client) == len(requests)
+
+  if lock_monitor_cb:
+    monitor = _PendingRequestMonitor(threading.currentThread(),
+                                     curl_to_client.values)
+    lock_monitor_cb(monitor)
+  else:
+    monitor = _NoOpRequestMonitor
+
+  # Process all requests and act based on the returned values
+  for (curl, msg) in _curl_process(_curl_multi(), curl_to_client.keys()):
+    monitor.acquire(shared=0)
+    try:
+      curl_to_client.pop(curl).Done(msg)
+    finally:
+      monitor.release()
 
-    assert compat.all(pclient.client.GetCurrentRequest() is None
-                      for pclient in curl_to_pclient.values())
+  assert not curl_to_client, "Not all requests were processed"
 
-    # Return clients to pool
-    self._Return(curl_to_pclient.values())
+  # Don't try to read information anymore as all requests have been processed
+  monitor.Disable()
 
-    assert done_count == len(requests)
-    assert compat.all(req.error is not None or
-                      (req.success and
-                       req.resp_status_code is not None and
-                       req.resp_body is not None)
-                      for req in requests)
+  assert compat.all(req.error is not None or
+                    (req.success and
+                     req.resp_status_code is not None and
+                     req.resp_body is not None)
+                    for req in requests)
index cb76185..a529496 100644 (file)
@@ -34,6 +34,8 @@ import asyncore
 from ganeti import http
 from ganeti import utils
 from ganeti import netutils
+from ganeti import compat
+from ganeti import errors
 
 
 WEEKDAYNAME = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
@@ -214,161 +216,144 @@ class _HttpClientToServerMessageReader(http.HttpMessageReader):
     return http.HttpClientToServerStartLine(method, path, version)
 
 
-class HttpServerRequestExecutor(object):
-  """Implements server side of HTTP.
-
-  This class implements the server side of HTTP. It's based on code of
-  Python's BaseHTTPServer, from both version 2.4 and 3k. It does not
-  support non-ASCII character encodings. Keep-alive connections are
-  not supported.
+def _HandleServerRequestInner(handler, req_msg):
+  """Calls the handler function for the current request.
 
   """
+  handler_context = _HttpServerRequest(req_msg.start_line.method,
+                                       req_msg.start_line.path,
+                                       req_msg.headers,
+                                       req_msg.body)
+
+  logging.debug("Handling request %r", handler_context)
+
+  try:
+    try:
+      # Authentication, etc.
+      handler.PreHandleRequest(handler_context)
+
+      # Call actual request handler
+      result = handler.HandleRequest(handler_context)
+    except (http.HttpException, errors.RapiTestResult,
+            KeyboardInterrupt, SystemExit):
+      raise
+    except Exception, err:
+      logging.exception("Caught exception")
+      raise http.HttpInternalServerError(message=str(err))
+    except:
+      logging.exception("Unknown exception")
+      raise http.HttpInternalServerError(message="Unknown error")
+
+    if not isinstance(result, basestring):
+      raise http.HttpError("Handler function didn't return string type")
+
+    return (http.HTTP_OK, handler_context.resp_headers, result)
+  finally:
+    # No reason to keep this any longer, even for exceptions
+    handler_context.private = None
+
+
+class HttpResponder(object):
   # The default request version.  This only affects responses up until
   # the point where the request line is parsed, so it mainly decides what
   # the client gets back when sending a malformed request line.
   # Most web servers default to HTTP 0.9, i.e. don't send a status line.
   default_request_version = http.HTTP_0_9
 
-  # Error message settings
-  error_message_format = DEFAULT_ERROR_MESSAGE
-  error_content_type = DEFAULT_ERROR_CONTENT_TYPE
-
   responses = BaseHTTPServer.BaseHTTPRequestHandler.responses
 
-  # Timeouts in seconds for socket layer
-  WRITE_TIMEOUT = 10
-  READ_TIMEOUT = 10
-  CLOSE_TIMEOUT = 1
-
-  def __init__(self, server, sock, client_addr):
+  def __init__(self, handler):
     """Initializes this class.
 
     """
-    self.server = server
-    self.sock = sock
-    self.client_addr = client_addr
+    self._handler = handler
+
+  def __call__(self, fn):
+    """Handles a request.
 
-    self.request_msg = http.HttpMessage()
-    self.response_msg = http.HttpMessage()
+    @type fn: callable
+    @param fn: Callback for retrieving HTTP request, must return a tuple
+      containing request message (L{http.HttpMessage}) and C{None} or the
+      message reader (L{_HttpClientToServerMessageReader})
 
-    self.response_msg.start_line = \
+    """
+    response_msg = http.HttpMessage()
+    response_msg.start_line = \
       http.HttpServerToClientStartLine(version=self.default_request_version,
                                        code=None, reason=None)
 
-    # Disable Python's timeout
-    self.sock.settimeout(None)
-
-    # Operate in non-blocking mode
-    self.sock.setblocking(0)
+    force_close = True
 
-    logging.debug("Connection from %s:%s", client_addr[0], client_addr[1])
     try:
-      request_msg_reader = None
-      force_close = True
-      try:
-        # Do the secret SSL handshake
-        if self.server.using_ssl:
-          self.sock.set_accept_state()
-          try:
-            http.Handshake(self.sock, self.WRITE_TIMEOUT)
-          except http.HttpSessionHandshakeUnexpectedEOF:
-            # Ignore rest
-            return
+      (request_msg, req_msg_reader) = fn()
+
+      response_msg.start_line.version = request_msg.start_line.version
+
+      # RFC2616, 14.23: All Internet-based HTTP/1.1 servers MUST respond
+      # with a 400 (Bad Request) status code to any HTTP/1.1 request
+      # message which lacks a Host header field.
+      if (request_msg.start_line.version == http.HTTP_1_1 and
+          not (request_msg.headers and
+               http.HTTP_HOST in request_msg.headers)):
+        raise http.HttpBadRequest(message="Missing Host header")
+
+      (response_msg.start_line.code, response_msg.headers,
+       response_msg.body) = \
+        _HandleServerRequestInner(self._handler, request_msg)
+    except http.HttpException, err:
+      self._SetError(self.responses, self._handler, response_msg, err)
+    else:
+      # Only wait for client to close if we didn't have any exception.
+      force_close = False
 
-        try:
-          try:
-            request_msg_reader = self._ReadRequest()
-
-            # RFC2616, 14.23: All Internet-based HTTP/1.1 servers MUST respond
-            # with a 400 (Bad Request) status code to any HTTP/1.1 request
-            # message which lacks a Host header field.
-            if (self.request_msg.start_line.version == http.HTTP_1_1 and
-                http.HTTP_HOST not in self.request_msg.headers):
-              raise http.HttpBadRequest(message="Missing Host header")
-
-            self._HandleRequest()
-
-            # Only wait for client to close if we didn't have any exception.
-            force_close = False
-          except http.HttpException, err:
-            self._SetErrorStatus(err)
-        finally:
-          # Try to send a response
-          self._SendResponse()
-      finally:
-        http.ShutdownConnection(sock, self.CLOSE_TIMEOUT, self.WRITE_TIMEOUT,
-                                request_msg_reader, force_close)
+    return (request_msg, req_msg_reader, force_close,
+            self._Finalize(self.responses, response_msg))
 
-      self.sock.close()
-      self.sock = None
-    finally:
-      logging.debug("Disconnected %s:%s", client_addr[0], client_addr[1])
+  @staticmethod
+  def _SetError(responses, handler, response_msg, err):
+    """Sets the response code and body from a HttpException.
 
-  def _ReadRequest(self):
-    """Reads a request sent by client.
+    @type err: HttpException
+    @param err: Exception instance
 
     """
     try:
-      request_msg_reader = \
-        _HttpClientToServerMessageReader(self.sock, self.request_msg,
-                                         self.READ_TIMEOUT)
-    except http.HttpSocketTimeout:
-      raise http.HttpError("Timeout while reading request")
-    except socket.error, err:
-      raise http.HttpError("Error reading request: %s" % err)
-
-    self.response_msg.start_line.version = self.request_msg.start_line.version
+      (shortmsg, longmsg) = responses[err.code]
+    except KeyError:
+      shortmsg = longmsg = "Unknown"
 
-    return request_msg_reader
+    if err.message:
+      message = err.message
+    else:
+      message = shortmsg
 
-  def _HandleRequest(self):
-    """Calls the handler function for the current request.
+    values = {
+      "code": err.code,
+      "message": cgi.escape(message),
+      "explain": longmsg,
+      }
 
-    """
-    handler_context = _HttpServerRequest(self.request_msg.start_line.method,
-                                         self.request_msg.start_line.path,
-                                         self.request_msg.headers,
-                                         self.request_msg.body)
+    (content_type, body) = handler.FormatErrorMessage(values)
 
-    logging.debug("Handling request %r", handler_context)
+    headers = {
+      http.HTTP_CONTENT_TYPE: content_type,
+      }
 
-    try:
-      try:
-        # Authentication, etc.
-        self.server.PreHandleRequest(handler_context)
-
-        # Call actual request handler
-        result = self.server.HandleRequest(handler_context)
-      except (http.HttpException, KeyboardInterrupt, SystemExit):
-        raise
-      except Exception, err:
-        logging.exception("Caught exception")
-        raise http.HttpInternalServerError(message=str(err))
-      except:
-        logging.exception("Unknown exception")
-        raise http.HttpInternalServerError(message="Unknown error")
-
-      if not isinstance(result, basestring):
-        raise http.HttpError("Handler function didn't return string type")
-
-      self.response_msg.start_line.code = http.HTTP_OK
-      self.response_msg.headers = handler_context.resp_headers
-      self.response_msg.body = result
-    finally:
-      # No reason to keep this any longer, even for exceptions
-      handler_context.private = None
+    if err.headers:
+      headers.update(err.headers)
 
-  def _SendResponse(self):
-    """Sends the response to the client.
+    response_msg.start_line.code = err.code
+    response_msg.headers = headers
+    response_msg.body = body
 
-    """
-    if self.response_msg.start_line.code is None:
-      return
+  @staticmethod
+  def _Finalize(responses, msg):
+    assert msg.start_line.reason is None
 
-    if not self.response_msg.headers:
-      self.response_msg.headers = {}
+    if not msg.headers:
+      msg.headers = {}
 
-    self.response_msg.headers.update({
+    msg.headers.update({
       # TODO: Keep-alive is not supported
       http.HTTP_CONNECTION: "close",
       http.HTTP_DATE: _DateTimeHeader(),
@@ -376,78 +361,113 @@ class HttpServerRequestExecutor(object):
       })
 
     # Get response reason based on code
-    response_code = self.response_msg.start_line.code
-    if response_code in self.responses:
-      response_reason = self.responses[response_code][0]
+    try:
+      code_desc = responses[msg.start_line.code]
+    except KeyError:
+      reason = ""
     else:
-      response_reason = ""
-    self.response_msg.start_line.reason = response_reason
+      (reason, _) = code_desc
 
-    logging.info("%s:%s %s %s", self.client_addr[0], self.client_addr[1],
-                 self.request_msg.start_line, response_code)
+    msg.start_line.reason = reason
 
-    try:
-      _HttpServerToClientMessageWriter(self.sock, self.request_msg,
-                                       self.response_msg, self.WRITE_TIMEOUT)
-    except http.HttpSocketTimeout:
-      raise http.HttpError("Timeout while sending response")
-    except socket.error, err:
-      raise http.HttpError("Error sending response: %s" % err)
+    return msg
 
-  def _SetErrorStatus(self, err):
-    """Sets the response code and body from a HttpException.
 
-    @type err: HttpException
-    @param err: Exception instance
+class HttpServerRequestExecutor(object):
+  """Implements server side of HTTP.
+
+  This class implements the server side of HTTP. It's based on code of
+  Python's BaseHTTPServer, from both version 2.4 and 3k. It does not
+  support non-ASCII character encodings. Keep-alive connections are
+  not supported.
+
+  """
+  # Timeouts in seconds for socket layer
+  WRITE_TIMEOUT = 10
+  READ_TIMEOUT = 10
+  CLOSE_TIMEOUT = 1
+
+  def __init__(self, server, handler, sock, client_addr):
+    """Initializes this class.
 
     """
+    responder = HttpResponder(handler)
+
+    # Disable Python's timeout
+    sock.settimeout(None)
+
+    # Operate in non-blocking mode
+    sock.setblocking(0)
+
+    request_msg_reader = None
+    force_close = True
+
+    logging.debug("Connection from %s:%s", client_addr[0], client_addr[1])
     try:
-      (shortmsg, longmsg) = self.responses[err.code]
-    except KeyError:
-      shortmsg = longmsg = "Unknown"
+      # Block for closing connection
+      try:
+        # Do the secret SSL handshake
+        if server.using_ssl:
+          sock.set_accept_state()
+          try:
+            http.Handshake(sock, self.WRITE_TIMEOUT)
+          except http.HttpSessionHandshakeUnexpectedEOF:
+            # Ignore rest
+            return
 
-    if err.message:
-      message = err.message
-    else:
-      message = shortmsg
+        (request_msg, request_msg_reader, force_close, response_msg) = \
+          responder(compat.partial(self._ReadRequest, sock, self.READ_TIMEOUT))
+        if response_msg:
+          # HttpMessage.start_line can be of different types
+          # pylint: disable=E1103
+          logging.info("%s:%s %s %s", client_addr[0], client_addr[1],
+                       request_msg.start_line, response_msg.start_line.code)
+          self._SendResponse(sock, request_msg, response_msg,
+                             self.WRITE_TIMEOUT)
+      finally:
+        http.ShutdownConnection(sock, self.CLOSE_TIMEOUT, self.WRITE_TIMEOUT,
+                                request_msg_reader, force_close)
 
-    values = {
-      "code": err.code,
-      "message": cgi.escape(message),
-      "explain": longmsg,
-      }
+      sock.close()
+    finally:
+      logging.debug("Disconnected %s:%s", client_addr[0], client_addr[1])
 
-    self.response_msg.start_line.code = err.code
+  @staticmethod
+  def _ReadRequest(sock, timeout):
+    """Reads a request sent by client.
 
-    headers = {}
-    if err.headers:
-      headers.update(err.headers)
-    headers[http.HTTP_CONTENT_TYPE] = self.error_content_type
-    self.response_msg.headers = headers
+    """
+    msg = http.HttpMessage()
 
-    self.response_msg.body = self._FormatErrorMessage(values)
+    try:
+      reader = _HttpClientToServerMessageReader(sock, msg, timeout)
+    except http.HttpSocketTimeout:
+      raise http.HttpError("Timeout while reading request")
+    except socket.error, err:
+      raise http.HttpError("Error reading request: %s" % err)
 
-  def _FormatErrorMessage(self, values):
-    """Formats the body of an error message.
+    return (msg, reader)
 
-    @type values: dict
-    @param values: dictionary with keys code, message and explain.
-    @rtype: string
-    @return: the body of the message
+  @staticmethod
+  def _SendResponse(sock, req_msg, msg, timeout):
+    """Sends the response to the client.
 
     """
-    return self.error_message_format % values
+    try:
+      _HttpServerToClientMessageWriter(sock, req_msg, msg, timeout)
+    except http.HttpSocketTimeout:
+      raise http.HttpError("Timeout while sending response")
+    except socket.error, err:
+      raise http.HttpError("Error sending response: %s" % err)
 
 
 class HttpServer(http.HttpBase, asyncore.dispatcher):
   """Generic HTTP server class
 
-  Users of this class must subclass it and override the HandleRequest function.
-
   """
   MAX_CHILDREN = 20
 
-  def __init__(self, mainloop, local_address, port,
+  def __init__(self, mainloop, local_address, port, handler,
                ssl_params=None, ssl_verify_peer=False,
                request_executor_class=None):
     """Initializes the HTTP server
@@ -479,6 +499,7 @@ class HttpServer(http.HttpBase, asyncore.dispatcher):
     self.mainloop = mainloop
     self.local_address = local_address
     self.port = port
+    self.handler = handler
     family = netutils.IPAddress.GetAddressFamily(local_address)
     self.socket = self._CreateSocket(ssl_params, ssl_verify_peer, family)
 
@@ -559,7 +580,7 @@ class HttpServer(http.HttpBase, asyncore.dispatcher):
         # In case the handler code uses temporary files
         utils.ResetTempfileModule()
 
-        self.request_executor(self, connection, client_addr)
+        self.request_executor(self, self.handler, connection, client_addr)
       except Exception: # pylint: disable=W0703
         logging.exception("Error while handling request from %s:%s",
                           client_addr[0], client_addr[1])
@@ -568,6 +589,14 @@ class HttpServer(http.HttpBase, asyncore.dispatcher):
     else:
       self._children.append(pid)
 
+
+class HttpServerHandler(object):
+  """Base class for handling HTTP server requests.
+
+  Users of this class must subclass it and override the L{HandleRequest}
+  function.
+
+  """
   def PreHandleRequest(self, req):
     """Called before handling a request.
 
@@ -582,3 +611,15 @@ class HttpServer(http.HttpBase, asyncore.dispatcher):
 
     """
     raise NotImplementedError()
+
+  @staticmethod
+  def FormatErrorMessage(values):
+    """Formats the body of an error message.
+
+    @type values: dict
+    @param values: dictionary with keys C{code}, C{message} and C{explain}.
+    @rtype: tuple; (string, string)
+    @return: Content-type and response body
+
+    """
+    return (DEFAULT_ERROR_CONTENT_TYPE, DEFAULT_ERROR_MESSAGE % values)
index 7c4b1b5..ceb062c 100644 (file)
@@ -48,6 +48,12 @@ from ganeti import constants
 
 
 def _IsCpuMaskWellFormed(cpu_mask):
+  """Verifies if the given single CPU mask is valid
+
+  The single CPU mask should be in the form "a,b,c,d", where each
+  letter is a positive number or range.
+
+  """
   try:
     cpu_list = utils.ParseCpuMask(cpu_mask)
   except errors.ParseError, _:
@@ -55,6 +61,21 @@ def _IsCpuMaskWellFormed(cpu_mask):
   return isinstance(cpu_list, list) and len(cpu_list) > 0
 
 
+def _IsMultiCpuMaskWellFormed(cpu_mask):
+  """Verifies if the given multiple CPU mask is valid
+
+  A valid multiple CPU mask is in the form "a:b:c:d", where each
+  letter is a single CPU mask.
+
+  """
+  try:
+    utils.ParseMultiCpuMask(cpu_mask)
+  except errors.ParseError, _:
+    return False
+
+  return True
+
+
 # Read the BaseHypervisor.PARAMETERS docstring for the syntax of the
 # _CHECK values
 
@@ -72,6 +93,11 @@ _CPU_MASK_CHECK = (_IsCpuMaskWellFormed,
                    "CPU mask definition is not well-formed",
                    None, None)
 
+# Multiple CPU mask must be well-formed
+_MULTI_CPU_MASK_CHECK = (_IsMultiCpuMaskWellFormed,
+                         "Multiple CPU mask definition is not well-formed",
+                         None, None)
+
 # Check for validity of port number
 _NET_PORT_CHECK = (lambda x: 0 < x < 65535, "invalid port number",
                    None, None)
@@ -85,6 +111,8 @@ REQ_NET_PORT_CHECK = (True, ) + _NET_PORT_CHECK
 OPT_NET_PORT_CHECK = (False, ) + _NET_PORT_CHECK
 REQ_CPU_MASK_CHECK = (True, ) + _CPU_MASK_CHECK
 OPT_CPU_MASK_CHECK = (False, ) + _CPU_MASK_CHECK
+REQ_MULTI_CPU_MASK_CHECK = (True, ) + _MULTI_CPU_MASK_CHECK
+OPT_MULTI_CPU_MASK_CHECK = (False, ) + _MULTI_CPU_MASK_CHECK
 
 # no checks at all
 NO_CHECK = (False, None, None, None, None)
@@ -133,6 +161,7 @@ class BaseHypervisor(object):
   """
   PARAMETERS = {}
   ANCILLARY_FILES = []
+  ANCILLARY_FILES_OPT = []
   CAN_MIGRATE = False
 
   def __init__(self):
@@ -221,13 +250,16 @@ class BaseHypervisor(object):
     """Return a list of ancillary files to be copied to all nodes as ancillary
     configuration files.
 
-    @rtype: list of strings
-    @return: list of absolute paths of files to ship cluster-wide
+    @rtype: (list of absolute paths, list of absolute paths)
+    @return: (all files, optional files)
 
     """
     # By default we return a member variable, so that if an hypervisor has just
     # a static list of files it doesn't have to override this function.
-    return cls.ANCILLARY_FILES
+    assert set(cls.ANCILLARY_FILES).issuperset(cls.ANCILLARY_FILES_OPT), \
+      "Optional ancillary files must be a subset of ancillary files"
+
+    return (cls.ANCILLARY_FILES, cls.ANCILLARY_FILES_OPT)
 
   def Verify(self):
     """Verify the hypervisor.
@@ -263,8 +295,19 @@ class BaseHypervisor(object):
     """
     pass
 
-  def FinalizeMigration(self, instance, info, success):
-    """Finalized an instance migration.
+  def BalloonInstanceMemory(self, instance, mem):
+    """Balloon an instance memory to a certain value.
+
+    @type instance: L{objects.Instance}
+    @param instance: instance to be accepted
+    @type mem: int
+    @param mem: actual memory size to use for instance runtime
+
+    """
+    raise NotImplementedError
+
+  def FinalizeMigrationDst(self, instance, info, success):
+    """Finalize the instance migration on the target node.
 
     Should finalize or revert any preparation done to accept the instance.
     Since by default we do no preparation, we also don't have anything to do
@@ -292,6 +335,50 @@ class BaseHypervisor(object):
     """
     raise NotImplementedError
 
+  def FinalizeMigrationSource(self, instance, success, live):
+    """Finalize the instance migration on the source node.
+
+    @type instance: L{objects.Instance}
+    @param instance: the instance that was migrated
+    @type success: bool
+    @param success: whether the migration succeeded or not
+    @type live: bool
+    @param live: whether the user requested a live migration or not
+
+    """
+    pass
+
+  def GetMigrationStatus(self, instance):
+    """Get the migration status
+
+    @type instance: L{objects.Instance}
+    @param instance: the instance that is being migrated
+    @rtype: L{objects.MigrationStatus}
+    @return: the status of the current migration (one of
+             L{constants.HV_MIGRATION_VALID_STATUSES}), plus any additional
+             progress info that can be retrieved from the hypervisor
+
+    """
+    raise NotImplementedError
+
+  def _InstanceStartupMemory(self, instance):
+    """Get the correct startup memory for an instance
+
+    This function calculates how much memory an instance should be started
+    with, making sure it's a value between the minimum and the maximum memory,
+    but also trying to use no more than the current free memory on the node.
+
+    @type instance: L{objects.Instance}
+    @param instance: the instance that is being started
+    @rtype: integer
+    @return: memory the instance should be started with
+
+    """
+    free_memory = self.GetNodeInfo()["memory_free"]
+    max_start_mem = min(instance.beparams[constants.BE_MAXMEM], free_memory)
+    start_mem = max(instance.beparams[constants.BE_MINMEM], max_start_mem)
+    return start_mem
+
   @classmethod
   def CheckParameterSyntax(cls, hvparams):
     """Check the given parameters for validity.
index 3d3caeb..3830f5f 100644 (file)
@@ -233,6 +233,18 @@ class ChrootManager(hv_base.BaseHypervisor):
     raise HypervisorError("The chroot manager doesn't implement the"
                           " reboot functionality")
 
+  def BalloonInstanceMemory(self, instance, mem):
+    """Balloon an instance memory to a certain value.
+
+    @type instance: L{objects.Instance}
+    @param instance: instance to be accepted
+    @type mem: int
+    @param mem: actual memory size to use for instance runtime
+
+    """
+    # Currently chroots don't have memory limits
+    pass
+
   def GetNodeInfo(self):
     """Return information about the node.
 
@@ -291,3 +303,16 @@ class ChrootManager(hv_base.BaseHypervisor):
 
     """
     raise HypervisorError("Migration not supported by the chroot hypervisor")
+
+  def GetMigrationStatus(self, instance):
+    """Get the migration status
+
+    @type instance: L{objects.Instance}
+    @param instance: the instance that is being migrated
+    @rtype: L{objects.MigrationStatus}
+    @return: the status of the current migration (one of
+             L{constants.HV_MIGRATION_VALID_STATUSES}), plus any additional
+             progress info that can be retrieved from the hypervisor
+
+    """
+    raise HypervisorError("Migration not supported by the chroot hypervisor")
index 441e44b..d2cd9a5 100644 (file)
@@ -123,7 +123,7 @@ class FakeHypervisor(hv_base.BaseHypervisor):
     file_name = self._InstanceFile(instance_name)
     return os.path.exists(file_name)
 
-  def _MarkUp(self, instance):
+  def _MarkUp(self, instance, memory):
     """Mark the instance as running.
 
     This does no checks, which should be done by its callers.
@@ -133,7 +133,7 @@ class FakeHypervisor(hv_base.BaseHypervisor):
     fh = file(file_name, "w")
     try:
       fh.write("0\n%d\n%d\n" %
-               (instance.beparams[constants.BE_MEMORY],
+               (memory,
                 instance.beparams[constants.BE_VCPUS]))
     finally:
       fh.close()
@@ -159,7 +159,7 @@ class FakeHypervisor(hv_base.BaseHypervisor):
       raise errors.HypervisorError("Failed to start instance %s: %s" %
                                    (instance.name, "already running"))
     try:
-      self._MarkUp(instance)
+      self._MarkUp(instance, self._InstanceStartupMemory(instance))
     except IOError, err:
       raise errors.HypervisorError("Failed to start instance %s: %s" %
                                    (instance.name, err))
@@ -186,6 +186,24 @@ class FakeHypervisor(hv_base.BaseHypervisor):
     """
     return
 
+  def BalloonInstanceMemory(self, instance, mem):
+    """Balloon an instance memory to a certain value.
+
+    @type instance: L{objects.Instance}
+    @param instance: instance to be accepted
+    @type mem: int
+    @param mem: actual memory size to use for instance runtime
+
+    """
+    if not self._IsAlive(instance.name):
+      raise errors.HypervisorError("Failed to balloon memory for %s: %s" %
+                                   (instance.name, "not running"))
+    try:
+      self._MarkUp(instance, mem)
+    except EnvironmentError, err:
+      raise errors.HypervisorError("Failed to balloon memory for %s: %s" %
+                                   (instance.name, utils.ErrnoOrStr(err)))
+
   def GetNodeInfo(self):
     """Return information about the node.
 
@@ -259,19 +277,62 @@ class FakeHypervisor(hv_base.BaseHypervisor):
     logging.debug("Fake hypervisor migrating %s to %s (live=%s)",
                   instance, target, live)
 
-    self._MarkDown(instance.name)
-
-  def FinalizeMigration(self, instance, info, success):
-    """Finalize an instance migration.
+  def FinalizeMigrationDst(self, instance, info, success):
+    """Finalize the instance migration on the target node.
 
     For the fake hv, this just marks the instance up.
 
     @type instance: L{objects.Instance}
     @param instance: instance whose migration is being finalized
+    @type info: string/data (opaque)
+    @param info: migration information, from the source node
+    @type success: boolean
+    @param success: whether the migration was a success or a failure
 
     """
     if success:
-      self._MarkUp(instance)
+      self._MarkUp(instance, self._InstanceStartupMemory(instance))
     else:
       # ensure it's down
       self._MarkDown(instance.name)
+
+  def PostMigrationCleanup(self, instance):
+    """Clean-up after a migration.
+
+    To be executed on the source node.
+
+    @type instance: L{objects.Instance}
+    @param instance: the instance that was migrated
+
+    """
+    pass
+
+  def FinalizeMigrationSource(self, instance, success, live):
+    """Finalize the instance migration on the source node.
+
+    @type instance: L{objects.Instance}
+    @param instance: the instance that was migrated
+    @type success: bool
+    @param success: whether the migration succeeded or not
+    @type live: bool
+    @param live: whether the user requested a live migration or not
+
+    """
+    # pylint: disable=W0613
+    if success:
+      self._MarkDown(instance.name)
+
+  def GetMigrationStatus(self, instance):
+    """Get the migration status
+
+    The fake hypervisor migration always succeeds.
+
+    @type instance: L{objects.Instance}
+    @param instance: the instance that is being migrated
+    @rtype: L{objects.MigrationStatus}
+    @return: the status of the current migration (one of
+             L{constants.HV_MIGRATION_VALID_STATUSES}), plus any additional
+             progress info that can be retrieved from the hypervisor
+
+    """
+    return objects.MigrationStatus(status=constants.HV_MIGRATION_COMPLETED)
index a481997..5ef9cc5 100644 (file)
@@ -34,6 +34,13 @@ import pwd
 import struct
 import fcntl
 import shutil
+import socket
+import stat
+import StringIO
+try:
+  import affinity   # pylint: disable=F0401
+except ImportError:
+  affinity = None
 
 from ganeti import utils
 from ganeti import constants
@@ -48,6 +55,7 @@ from ganeti.utils import wrapper as utils_wrapper
 
 
 _KVM_NETWORK_SCRIPT = constants.SYSCONFDIR + "/ganeti/kvm-vif-bridge"
+_KVM_START_PAUSED_FLAG = "-S"
 
 # TUN/TAP driver constants, taken from <linux/if_tun.h>
 # They are architecture-independent and already hardcoded in qemu-kvm source,
@@ -127,8 +135,273 @@ def _OpenTap(vnet_hdr=True):
   return (ifname, tapfd)
 
 
+class QmpMessage:
+  """QEMU Messaging Protocol (QMP) message.
+
+  """
+  def __init__(self, data):
+    """Creates a new QMP message based on the passed data.
+
+    """
+    if not isinstance(data, dict):
+      raise TypeError("QmpMessage must be initialized with a dict")
+
+    self.data = data
+
+  def __getitem__(self, field_name):
+    """Get the value of the required field if present, or None.
+
+    Overrides the [] operator to provide access to the message data,
+    returning None if the required item is not in the message
+    @return: the value of the field_name field, or None if field_name
+             is not contained in the message
+
+    """
+    return self.data.get(field_name, None)
+
+  def __setitem__(self, field_name, field_value):
+    """Set the value of the required field_name to field_value.
+
+    """
+    self.data[field_name] = field_value
+
+  @staticmethod
+  def BuildFromJsonString(json_string):
+    """Build a QmpMessage from a JSON encoded string.
+
+    @type json_string: str
+    @param json_string: JSON string representing the message
+    @rtype: L{QmpMessage}
+    @return: a L{QmpMessage} built from json_string
+
+    """
+    # Parse the string
+    data = serializer.LoadJson(json_string)
+    return QmpMessage(data)
+
+  def __str__(self):
+    # The protocol expects the JSON object to be sent as a single line.
+    return serializer.DumpJson(self.data)
+
+  def __eq__(self, other):
+    # When comparing two QmpMessages, we are interested in comparing
+    # their internal representation of the message data
+    return self.data == other.data
+
+
+class QmpConnection:
+  """Connection to the QEMU Monitor using the QEMU Monitor Protocol (QMP).
+
+  """
+  _FIRST_MESSAGE_KEY = "QMP"
+  _EVENT_KEY = "event"
+  _ERROR_KEY = "error"
+  _RETURN_KEY = RETURN_KEY = "return"
+  _ACTUAL_KEY = ACTUAL_KEY = "actual"
+  _ERROR_CLASS_KEY = "class"
+  _ERROR_DATA_KEY = "data"
+  _ERROR_DESC_KEY = "desc"
+  _EXECUTE_KEY = "execute"
+  _ARGUMENTS_KEY = "arguments"
+  _CAPABILITIES_COMMAND = "qmp_capabilities"
+  _MESSAGE_END_TOKEN = "\r\n"
+  _SOCKET_TIMEOUT = 5
+
+  def __init__(self, monitor_filename):
+    """Instantiates the QmpConnection object.
+
+    @type monitor_filename: string
+    @param monitor_filename: the filename of the UNIX raw socket on which the
+                             QMP monitor is listening
+
+    """
+    self.monitor_filename = monitor_filename
+    self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+    # We want to fail if the server doesn't send a complete message
+    # in a reasonable amount of time
+    self.sock.settimeout(self._SOCKET_TIMEOUT)
+    self._connected = False
+    self._buf = ""
+
+  def _check_socket(self):
+    sock_stat = None
+    try:
+      sock_stat = os.stat(self.monitor_filename)
+    except EnvironmentError, err:
+      if err.errno == errno.ENOENT:
+        raise errors.HypervisorError("No qmp socket found")
+      else:
+        raise errors.HypervisorError("Error checking qmp socket: %s",
+                                     utils.ErrnoOrStr(err))
+    if not stat.S_ISSOCK(sock_stat.st_mode):
+      raise errors.HypervisorError("Qmp socket is not a socket")
+
+  def _check_connection(self):
+    """Make sure that the connection is established.
+
+    """
+    if not self._connected:
+      raise errors.ProgrammerError("To use a QmpConnection you need to first"
+                                   " invoke connect() on it")
+
+  def connect(self):
+    """Connects to the QMP monitor.
+
+    Connects to the UNIX socket and makes sure that we can actually send and
+    receive data to the kvm instance via QMP.
+
+    @raise errors.HypervisorError: when there are communication errors
+    @raise errors.ProgrammerError: when there are data serialization errors
+
+    """
+    if self._connected:
+      raise errors.ProgrammerError("Cannot connect twice")
+
+    self._check_socket()
+
+    # Check file existance/stuff
+    try:
+      self.sock.connect(self.monitor_filename)
+    except EnvironmentError:
+      raise errors.HypervisorError("Can't connect to qmp socket")
+    self._connected = True
+
+    # Check if we receive a correct greeting message from the server
+    # (As per the QEMU Protocol Specification 0.1 - section 2.2)
+    greeting = self._Recv()
+    if not greeting[self._FIRST_MESSAGE_KEY]:
+      self._connected = False
+      raise errors.HypervisorError("kvm: qmp communication error (wrong"
+                                   " server greeting")
+
+    # Let's put the monitor in command mode using the qmp_capabilities
+    # command, or else no command will be executable.
+    # (As per the QEMU Protocol Specification 0.1 - section 4)
+    self.Execute(self._CAPABILITIES_COMMAND)
+
+  def _ParseMessage(self, buf):
+    """Extract and parse a QMP message from the given buffer.
+
+    Seeks for a QMP message in the given buf. If found, it parses it and
+    returns it together with the rest of the characters in the buf.
+    If no message is found, returns None and the whole buffer.
+
+    @raise errors.ProgrammerError: when there are data serialization errors
+
+    """
+    message = None
+    # Check if we got the message end token (CRLF, as per the QEMU Protocol
+    # Specification 0.1 - Section 2.1.1)
+    pos = buf.find(self._MESSAGE_END_TOKEN)
+    if pos >= 0:
+      try:
+        message = QmpMessage.BuildFromJsonString(buf[:pos + 1])
+      except Exception, err:
+        raise errors.ProgrammerError("QMP data serialization error: %s" % err)
+      buf = buf[pos + 1:]
+
+    return (message, buf)
+
+  def _Recv(self):
+    """Receives a message from QMP and decodes the received JSON object.
+
+    @rtype: QmpMessage
+    @return: the received message
+    @raise errors.HypervisorError: when there are communication errors
+    @raise errors.ProgrammerError: when there are data serialization errors
+
+    """
+    self._check_connection()
+
+    # Check if there is already a message in the buffer
+    (message, self._buf) = self._ParseMessage(self._buf)
+    if message:
+      return message
+
+    recv_buffer = StringIO.StringIO(self._buf)
+    recv_buffer.seek(len(self._buf))
+    try:
+      while True:
+        data = self.sock.recv(4096)
+        if not data:
+          break
+        recv_buffer.write(data)
+
+        (message, self._buf) = self._ParseMessage(recv_buffer.getvalue())
+        if message:
+          return message
+
+    except socket.timeout, err:
+      raise errors.HypervisorError("Timeout while receiving a QMP message: "
+                                   "%s" % (err))
+    except socket.error, err:
+      raise errors.HypervisorError("Unable to receive data from KVM using the"
+                                   " QMP protocol: %s" % err)
+
+  def _Send(self, message):
+    """Encodes and sends a message to KVM using QMP.
+
+    @type message: QmpMessage
+    @param message: message to send to KVM
+    @raise errors.HypervisorError: when there are communication errors
+    @raise errors.ProgrammerError: when there are data serialization errors
+
+    """
+    self._check_connection()
+    try:
+      message_str = str(message)
+    except Exception, err:
+      raise errors.ProgrammerError("QMP data deserialization error: %s" % err)
+
+    try:
+      self.sock.sendall(message_str)
+    except socket.timeout, err:
+      raise errors.HypervisorError("Timeout while sending a QMP message: "
+                                   "%s (%s)" % (err.string, err.errno))
+    except socket.error, err:
+      raise errors.HypervisorError("Unable to send data from KVM using the"
+                                   " QMP protocol: %s" % err)
+
+  def Execute(self, command, arguments=None):
+    """Executes a QMP command and returns the response of the server.
+
+    @type command: str
+    @param command: the command to execute
+    @type arguments: dict
+    @param arguments: dictionary of arguments to be passed to the command
+    @rtype: dict
+    @return: dictionary representing the received JSON object
+    @raise errors.HypervisorError: when there are communication errors
+    @raise errors.ProgrammerError: when there are data serialization errors
+
+    """
+    self._check_connection()
+    message = QmpMessage({self._EXECUTE_KEY: command})
+    if arguments:
+      message[self._ARGUMENTS_KEY] = arguments
+    self._Send(message)
+
+    # Events can occur between the sending of the command and the reception
+    # of the response, so we need to filter out messages with the event key.
+    while True:
+      response = self._Recv()
+      err = response[self._ERROR_KEY]
+      if err:
+        raise errors.HypervisorError("kvm: error executing the %s"
+                                     " command: %s (%s, %s):" %
+                                     (command,
+                                      err[self._ERROR_DESC_KEY],
+                                      err[self._ERROR_CLASS_KEY],
+                                      err[self._ERROR_DATA_KEY]))
+
+      elif not response[self._EVENT_KEY]:
+        return response
+
+
 class KVMHypervisor(hv_base.BaseHypervisor):
-  """KVM hypervisor interface"""
+  """KVM hypervisor interface
+
+  """
   CAN_MIGRATE = True
 
   _ROOT_DIR = constants.RUN_GANETI_DIR + "/kvm-hypervisor"
@@ -171,6 +444,23 @@ class KVMHypervisor(hv_base.BaseHypervisor):
                          x in constants.VALID_IP_VERSIONS),
        "the SPICE IP version should be 4 or 6",
        None, None),
+    constants.HV_KVM_SPICE_PASSWORD_FILE: hv_base.OPT_FILE_CHECK,
+    constants.HV_KVM_SPICE_LOSSLESS_IMG_COMPR:
+      hv_base.ParamInSet(False,
+        constants.HT_KVM_SPICE_VALID_LOSSLESS_IMG_COMPR_OPTIONS),
+    constants.HV_KVM_SPICE_JPEG_IMG_COMPR:
+      hv_base.ParamInSet(False,
+        constants.HT_KVM_SPICE_VALID_LOSSY_IMG_COMPR_OPTIONS),
+    constants.HV_KVM_SPICE_ZLIB_GLZ_IMG_COMPR:
+      hv_base.ParamInSet(False,
+        constants.HT_KVM_SPICE_VALID_LOSSY_IMG_COMPR_OPTIONS),
+    constants.HV_KVM_SPICE_STREAMING_VIDEO_DETECTION:
+      hv_base.ParamInSet(False,
+        constants.HT_KVM_SPICE_VALID_VIDEO_STREAM_DETECTION_OPTIONS),
+    constants.HV_KVM_SPICE_AUDIO_COMPR: hv_base.NO_CHECK,
+    constants.HV_KVM_SPICE_USE_TLS: hv_base.NO_CHECK,
+    constants.HV_KVM_SPICE_TLS_CIPHERS: hv_base.NO_CHECK,
+    constants.HV_KVM_SPICE_USE_VDAGENT: hv_base.NO_CHECK,
     constants.HV_KVM_FLOPPY_IMAGE_PATH: hv_base.OPT_FILE_CHECK,
     constants.HV_CDROM_IMAGE_PATH: hv_base.OPT_FILE_CHECK,
     constants.HV_KVM_CDROM2_IMAGE_PATH: hv_base.OPT_FILE_CHECK,
@@ -201,19 +491,32 @@ class KVMHypervisor(hv_base.BaseHypervisor):
     constants.HV_KVM_USE_CHROOT: hv_base.NO_CHECK,
     constants.HV_MEM_PATH: hv_base.OPT_DIR_CHECK,
     constants.HV_REBOOT_BEHAVIOR:
-      hv_base.ParamInSet(True, constants.REBOOT_BEHAVIORS)
+      hv_base.ParamInSet(True, constants.REBOOT_BEHAVIORS),
+    constants.HV_CPU_MASK: hv_base.OPT_MULTI_CPU_MASK_CHECK,
     }
 
   _MIGRATION_STATUS_RE = re.compile("Migration\s+status:\s+(\w+)",
                                     re.M | re.I)
+  _MIGRATION_PROGRESS_RE = \
+    re.compile(r"\s*transferred\s+ram:\s+(?P<transferred>\d+)\s+kbytes\s*\n"
+               r"\s*remaining\s+ram:\s+(?P<remaining>\d+)\s+kbytes\s*\n"
+               r"\s*total\s+ram:\s+(?P<total>\d+)\s+kbytes\s*\n", re.I)
+
   _MIGRATION_INFO_MAX_BAD_ANSWERS = 5
   _MIGRATION_INFO_RETRY_DELAY = 2
 
   _VERSION_RE = re.compile(r"\b(\d+)\.(\d+)(\.(\d+))?\b")
 
+  _CPU_INFO_RE = re.compile(r"cpu\s+\#(\d+).*thread_id\s*=\s*(\d+)", re.I)
+  _CPU_INFO_CMD = "info cpus"
+  _CONT_CMD = "cont"
+
   ANCILLARY_FILES = [
     _KVM_NETWORK_SCRIPT,
     ]
+  ANCILLARY_FILES_OPT = [
+    _KVM_NETWORK_SCRIPT,
+    ]
 
   def __init__(self):
     hv_base.BaseHypervisor.__init__(self)
@@ -325,6 +628,13 @@ class KVMHypervisor(hv_base.BaseHypervisor):
     """
     return utils.PathJoin(cls._CTRL_DIR, "%s.serial" % instance_name)
 
+  @classmethod
+  def _InstanceQmpMonitor(cls, instance_name):
+    """Returns the instance serial QMP socket name
+
+    """
+    return utils.PathJoin(cls._CTRL_DIR, "%s.qmp" % instance_name)
+
   @staticmethod
   def _SocatUnixConsoleParams():
     """Returns the correct parameters for socat
@@ -396,6 +706,7 @@ class KVMHypervisor(hv_base.BaseHypervisor):
     utils.RemoveFile(pidfile)
     utils.RemoveFile(cls._InstanceMonitor(instance_name))
     utils.RemoveFile(cls._InstanceSerial(instance_name))
+    utils.RemoveFile(cls._InstanceQmpMonitor(instance_name))
     utils.RemoveFile(cls._InstanceKVMRuntime(instance_name))
     utils.RemoveFile(cls._InstanceKeymapFile(instance_name))
     uid_file = cls._InstanceUidFile(instance_name)
@@ -441,7 +752,6 @@ class KVMHypervisor(hv_base.BaseHypervisor):
     @type tap: str
 
     """
-
     if instance.tags:
       tags = " ".join(instance.tags)
     else:
@@ -472,6 +782,106 @@ class KVMHypervisor(hv_base.BaseHypervisor):
                                    " Network configuration script output: %s" %
                                    (tap, result.fail_reason, result.output))
 
+  @staticmethod
+  def _VerifyAffinityPackage():
+    if affinity is None:
+      raise errors.HypervisorError("affinity Python package not"
+        " found; cannot use CPU pinning under KVM")
+
+  @staticmethod
+  def _BuildAffinityCpuMask(cpu_list):
+    """Create a CPU mask suitable for sched_setaffinity from a list of
+    CPUs.
+
+    See man taskset for more info on sched_setaffinity masks.
+    For example: [ 0, 2, 5, 6 ] will return 101 (0x65, 0..01100101).
+
+    @type cpu_list: list of int
+    @param cpu_list: list of physical CPU numbers to map to vCPUs in order
+    @rtype: int
+    @return: a bit mask of CPU affinities
+
+    """
+    if cpu_list == constants.CPU_PINNING_OFF:
+      return constants.CPU_PINNING_ALL_KVM
+    else:
+      return sum(2 ** cpu for cpu in cpu_list)
+
+  @classmethod
+  def _AssignCpuAffinity(cls, cpu_mask, process_id, thread_dict):
+    """Change CPU affinity for running VM according to given CPU mask.
+
+    @param cpu_mask: CPU mask as given by the user. e.g. "0-2,4:all:1,3"
+    @type cpu_mask: string
+    @param process_id: process ID of KVM process. Used to pin entire VM
+                       to physical CPUs.
+    @type process_id: int
+    @param thread_dict: map of virtual CPUs to KVM thread IDs
+    @type thread_dict: dict int:int
+
+    """
+    # Convert the string CPU mask to a list of list of int's
+    cpu_list = utils.ParseMultiCpuMask(cpu_mask)
+
+    if len(cpu_list) == 1:
+      all_cpu_mapping = cpu_list[0]
+      if all_cpu_mapping == constants.CPU_PINNING_OFF:
+        # If CPU pinning has 1 entry that's "all", then do nothing
+        pass
+      else:
+        # If CPU pinning has one non-all entry, map the entire VM to
+        # one set of physical CPUs
+        cls._VerifyAffinityPackage()
+        affinity.set_process_affinity_mask(process_id,
+          cls._BuildAffinityCpuMask(all_cpu_mapping))
+    else:
+      # The number of vCPUs mapped should match the number of vCPUs
+      # reported by KVM. This was already verified earlier, so
+      # here only as a sanity check.
+      assert len(thread_dict) == len(cpu_list)
+      cls._VerifyAffinityPackage()
+
+      # For each vCPU, map it to the proper list of physical CPUs
+      for vcpu, i in zip(cpu_list, range(len(cpu_list))):
+        affinity.set_process_affinity_mask(thread_dict[i],
+          cls._BuildAffinityCpuMask(vcpu))
+
+  def _GetVcpuThreadIds(self, instance_name):
+    """Get a mapping of vCPU no. to thread IDs for the instance
+
+    @type instance_name: string
+    @param instance_name: instance in question
+    @rtype: dictionary of int:int
+    @return: a dictionary mapping vCPU numbers to thread IDs
+
+    """
+    result = {}
+    output = self._CallMonitorCommand(instance_name, self._CPU_INFO_CMD)
+    for line in output.stdout.splitlines():
+      match = self._CPU_INFO_RE.search(line)
+      if not match:
+        continue
+      grp = map(int, match.groups())
+      result[grp[0]] = grp[1]
+
+    return result
+
+  def _ExecuteCpuAffinity(self, instance_name, cpu_mask):
+    """Complete CPU pinning.
+
+    @type instance_name: string
+    @param instance_name: name of instance
+    @type cpu_mask: string
+    @param cpu_mask: CPU pinning mask as entered by user
+
+    """
+    # Get KVM process ID, to be used if need to pin entire VM
+    _, pid, _ = self._InstancePidAlive(instance_name)
+    # Get vCPU thread IDs, to be used if need to pin vCPUs separately
+    thread_dict = self._GetVcpuThreadIds(instance_name)
+    # Run CPU pinning, based on configured mask
+    self._AssignCpuAffinity(cpu_mask, pid, thread_dict)
+
   def ListInstances(self):
     """Get the list of running instances.
 
@@ -499,10 +909,21 @@ class KVMHypervisor(hv_base.BaseHypervisor):
       return None
 
     _, memory, vcpus = self._InstancePidInfo(pid)
-    stat = "---b-"
+    istat = "---b-"
     times = "0"
 
-    return (instance_name, pid, memory, vcpus, stat, times)
+    try:
+      qmp = QmpConnection(self._InstanceQmpMonitor(instance_name))
+      qmp.connect()
+      vcpus = len(qmp.Execute("query-cpus")[qmp.RETURN_KEY])
+      # Will fail if ballooning is not enabled, but we can then just resort to
+      # the value above.
+      mem_bytes = qmp.Execute("query-balloon")[qmp.RETURN_KEY][qmp.ACTUAL_KEY]
+      memory = mem_bytes / 1048576
+    except errors.HypervisorError:
+      pass
+
+    return (instance_name, pid, memory, vcpus, istat, times)
 
   def GetAllInstancesInfo(self):
     """Get properties of all instances.
@@ -515,6 +936,7 @@ class KVMHypervisor(hv_base.BaseHypervisor):
       try:
         info = self.GetInstanceInfo(name)
       except errors.HypervisorError:
+        # Ignore exceptions due to instances being shut down
         continue
       if info:
         data.append(info)
@@ -531,6 +953,7 @@ class KVMHypervisor(hv_base.BaseHypervisor):
         done in L{_ExecuteKVMRuntime}
 
     """
+    # pylint: disable=R0914,R0915
     _, v_major, v_min, _ = self._GetKVMVersion()
 
     pidfile = self._InstancePidFile(instance.name)
@@ -538,14 +961,13 @@ class KVMHypervisor(hv_base.BaseHypervisor):
     kvm_cmd = [kvm]
     # used just by the vnc server, if enabled
     kvm_cmd.extend(["-name", instance.name])
-    kvm_cmd.extend(["-m", instance.beparams[constants.BE_MEMORY]])
+    kvm_cmd.extend(["-m", instance.beparams[constants.BE_MAXMEM]])
     kvm_cmd.extend(["-smp", instance.beparams[constants.BE_VCPUS]])
     kvm_cmd.extend(["-pidfile", pidfile])
+    kvm_cmd.extend(["-balloon", "virtio"])
     kvm_cmd.extend(["-daemonize"])
     if not instance.hvparams[constants.HV_ACPI]:
       kvm_cmd.extend(["-no-acpi"])
-    if startup_paused:
-      kvm_cmd.extend(["-S"])
     if instance.hvparams[constants.HV_REBOOT_BEHAVIOR] == \
         constants.INSTANCE_REBOOT_EXIT:
       kvm_cmd.extend(["-no-reboot"])
@@ -562,6 +984,9 @@ class KVMHypervisor(hv_base.BaseHypervisor):
 
     self.ValidateParameters(hvp)
 
+    if startup_paused:
+      kvm_cmd.extend([_KVM_START_PAUSED_FLAG])
+
     if hvp[constants.HV_KVM_FLAG] == constants.HT_KVM_ENABLED:
       kvm_cmd.extend(["-enable-kvm"])
     elif hvp[constants.HV_KVM_FLAG] == constants.HT_KVM_DISABLED:
@@ -748,8 +1173,8 @@ class KVMHypervisor(hv_base.BaseHypervisor):
           # we have both ipv4 and ipv6, let's use the cluster default IP
           # version
           cluster_family = ssconf.SimpleStore().GetPrimaryIPFamily()
-          spice_ip_version = netutils.IPAddress.GetVersionFromAddressFamily(
-              cluster_family)
+          spice_ip_version = \
+            netutils.IPAddress.GetVersionFromAddressFamily(cluster_family)
         elif addresses[constants.IP4_VERSION]:
           spice_ip_version = constants.IP4_VERSION
         elif addresses[constants.IP6_VERSION]:
@@ -765,14 +1190,59 @@ class KVMHypervisor(hv_base.BaseHypervisor):
         # ValidateParameters checked it.
         spice_address = spice_bind
 
-      spice_arg = "addr=%s,port=%s,disable-ticketing" % (spice_address,
-                                                         instance.network_port)
+      spice_arg = "addr=%s" % spice_address
+      if hvp[constants.HV_KVM_SPICE_USE_TLS]:
+        spice_arg = "%s,tls-port=%s,x509-cacert-file=%s" % (spice_arg,
+            instance.network_port, constants.SPICE_CACERT_FILE)
+        spice_arg = "%s,x509-key-file=%s,x509-cert-file=%s" % (spice_arg,
+            constants.SPICE_CERT_FILE, constants.SPICE_CERT_FILE)
+        tls_ciphers = hvp[constants.HV_KVM_SPICE_TLS_CIPHERS]
+        if tls_ciphers:
+          spice_arg = "%s,tls-ciphers=%s" % (spice_arg, tls_ciphers)
+      else:
+        spice_arg = "%s,port=%s" % (spice_arg, instance.network_port)
+
+      if not hvp[constants.HV_KVM_SPICE_PASSWORD_FILE]:
+        spice_arg = "%s,disable-ticketing" % spice_arg
+
       if spice_ip_version:
         spice_arg = "%s,ipv%s" % (spice_arg, spice_ip_version)
 
+      # Image compression options
+      img_lossless = hvp[constants.HV_KVM_SPICE_LOSSLESS_IMG_COMPR]
+      img_jpeg = hvp[constants.HV_KVM_SPICE_JPEG_IMG_COMPR]
+      img_zlib_glz = hvp[constants.HV_KVM_SPICE_ZLIB_GLZ_IMG_COMPR]
+      if img_lossless:
+        spice_arg = "%s,image-compression=%s" % (spice_arg, img_lossless)
+      if img_jpeg:
+        spice_arg = "%s,jpeg-wan-compression=%s" % (spice_arg, img_jpeg)
+      if img_zlib_glz:
+        spice_arg = "%s,zlib-glz-wan-compression=%s" % (spice_arg, img_zlib_glz)
+
+      # Video stream detection
+      video_streaming = hvp[constants.HV_KVM_SPICE_STREAMING_VIDEO_DETECTION]
+      if video_streaming:
+        spice_arg = "%s,streaming-video=%s" % (spice_arg, video_streaming)
+
+      # Audio compression, by default in qemu-kvm it is on
+      if not hvp[constants.HV_KVM_SPICE_AUDIO_COMPR]:
+        spice_arg = "%s,playback-compression=off" % spice_arg
+      if not hvp[constants.HV_KVM_SPICE_USE_VDAGENT]:
+        spice_arg = "%s,agent-mouse=off" % spice_arg
+      else:
+        # Enable the spice agent communication channel between the host and the
+        # agent.
+        kvm_cmd.extend(["-device", "virtio-serial-pci"])
+        kvm_cmd.extend(["-device", "virtserialport,chardev=spicechannel0,"
+                                                   "name=com.redhat.spice.0"])
+        kvm_cmd.extend(["-chardev", "spicevmc,id=spicechannel0,name=vdagent"])
+
       logging.info("KVM: SPICE will listen on port %s", instance.network_port)
       kvm_cmd.extend(["-spice", spice_arg])
 
+      # Tell kvm to use the paravirtualized graphic card, optimized for SPICE
+      kvm_cmd.extend(["-vga", "qxl"])
+
     else:
       kvm_cmd.extend(["-nographic"])
 
@@ -957,6 +1427,12 @@ class KVMHypervisor(hv_base.BaseHypervisor):
       utils.EnsureDirs([(self._InstanceChrootDir(name),
                          constants.SECURE_DIR_MODE)])
 
+    # Automatically enable QMP if version is >= 0.14
+    if (v_major, v_min) >= (0, 14):
+      logging.debug("Enabling QMP")
+      kvm_cmd.extend(["-qmp", "unix:%s,server,nowait" %
+                    self._InstanceQmpMonitor(instance.name)])
+
     # Configure the network now for starting instances and bridged interfaces,
     # during FinalizeMigration for incoming instances' routed interfaces
     for nic_seq, nic in enumerate(kvm_nics):
@@ -965,6 +1441,20 @@ class KVMHypervisor(hv_base.BaseHypervisor):
         continue
       self._ConfigureNIC(instance, nic_seq, nic, taps[nic_seq])
 
+    # CPU affinity requires kvm to start paused, so we set this flag if the
+    # instance is not already paused and if we are not going to accept a
+    # migrating instance. In the latter case, pausing is not needed.
+    start_kvm_paused = not (_KVM_START_PAUSED_FLAG in kvm_cmd) and not incoming
+    if start_kvm_paused:
+      kvm_cmd.extend([_KVM_START_PAUSED_FLAG])
+
+    # Note: CPU pinning is using up_hvp since changes take effect
+    # during instance startup anyway, and to avoid problems when soft
+    # rebooting the instance.
+    cpu_pinning = False
+    if up_hvp.get(constants.HV_CPU_MASK, None):
+      cpu_pinning = True
+
     if security_model == constants.HT_SM_POOL:
       ss = ssconf.SimpleStore()
       uid_pool = uidpool.ParseUidPool(ss.GetUidPool(), separator="\n")
@@ -993,9 +1483,45 @@ class KVMHypervisor(hv_base.BaseHypervisor):
       change_cmd = "change vnc password %s" % vnc_pwd
       self._CallMonitorCommand(instance.name, change_cmd)
 
+    # Setting SPICE password. We are not vulnerable to malicious passwordless
+    # connection attempts because SPICE by default does not allow connections
+    # if neither a password nor the "disable_ticketing" options are specified.
+    # As soon as we send the password via QMP, that password is a valid ticket
+    # for connection.
+    spice_password_file = conf_hvp[constants.HV_KVM_SPICE_PASSWORD_FILE]
+    if spice_password_file:
+      spice_pwd = ""
+      try:
+        spice_pwd = utils.ReadOneLineFile(spice_password_file, strict=True)
+      except EnvironmentError, err:
+        raise errors.HypervisorError("Failed to open SPICE password file %s: %s"
+                                     % (spice_password_file, err))
+
+      qmp = QmpConnection(self._InstanceQmpMonitor(instance.name))
+      qmp.connect()
+      arguments = {
+          "protocol": "spice",
+          "password": spice_pwd,
+      }
+      qmp.Execute("set_password", arguments)
+
     for filename in temp_files:
       utils.RemoveFile(filename)
 
+    # If requested, set CPU affinity and resume instance execution
+    if cpu_pinning:
+      self._ExecuteCpuAffinity(instance.name, up_hvp[constants.HV_CPU_MASK])
+
+    start_memory = self._InstanceStartupMemory(instance)
+    if start_memory < instance.beparams[constants.BE_MAXMEM]:
+      self.BalloonInstanceMemory(instance, start_memory)
+
+    if start_kvm_paused:
+      # To control CPU pinning, ballooning, and vnc/spice passwords the VM was
+      # started in a frozen state. If freezing was not explicitely requested
+      # resume the vm status.
+      self._CallMonitorCommand(instance.name, self._CONT_CMD)
+
   def StartInstance(self, instance, block_devices, startup_paused):
     """Start an instance.
 
@@ -1031,7 +1557,7 @@ class KVMHypervisor(hv_base.BaseHypervisor):
     @type text: string
     @param text: output of kvm --help
     @return: (version, v_maj, v_min, v_rev)
-    @raise L{errors.HypervisorError}: when the KVM version cannot be retrieved
+    @raise errors.HypervisorError: when the KVM version cannot be retrieved
 
     """
     match = cls._VERSION_RE.search(text.splitlines()[0])
@@ -1052,7 +1578,7 @@ class KVMHypervisor(hv_base.BaseHypervisor):
     """Return the installed KVM version.
 
     @return: (version, v_maj, v_min, v_rev)
-    @raise L{errors.HypervisorError}: when the KVM version cannot be retrieved
+    @raise errors.HypervisorError: when the KVM version cannot be retrieved
 
     """
     result = utils.RunCmd([constants.KVM_PATH, "--help"])
@@ -1134,8 +1660,8 @@ class KVMHypervisor(hv_base.BaseHypervisor):
     incoming_address = (target, instance.hvparams[constants.HV_MIGRATION_PORT])
     self._ExecuteKVMRuntime(instance, kvm_runtime, incoming=incoming_address)
 
-  def FinalizeMigration(self, instance, info, success):
-    """Finalize an instance migration.
+  def FinalizeMigrationDst(self, instance, info, success):
+    """Finalize the instance migration on the target node.
 
     Stop the incoming mode KVM.
 
@@ -1182,7 +1708,7 @@ class KVMHypervisor(hv_base.BaseHypervisor):
     """
     instance_name = instance.name
     port = instance.hvparams[constants.HV_MIGRATION_PORT]
-    pidfile, pid, alive = self._InstancePidAlive(instance_name)
+    _, _, alive = self._InstancePidAlive(instance_name)
     if not alive:
       raise errors.HypervisorError("Instance not running, cannot migrate")
 
@@ -1200,55 +1726,89 @@ class KVMHypervisor(hv_base.BaseHypervisor):
     migrate_command = "migrate -d tcp:%s:%s" % (target, port)
     self._CallMonitorCommand(instance_name, migrate_command)
 
+  def FinalizeMigrationSource(self, instance, success, live):
+    """Finalize the instance migration on the source node.
+
+    @type instance: L{objects.Instance}
+    @param instance: the instance that was migrated
+    @type success: bool
+    @param success: whether the migration succeeded or not
+    @type live: bool
+    @param live: whether the user requested a live migration or not
+
+    """
+    if success:
+      pidfile, pid, _ = self._InstancePidAlive(instance.name)
+      utils.KillProcess(pid)
+      self._RemoveInstanceRuntimeFiles(pidfile, instance.name)
+    elif live:
+      self._CallMonitorCommand(instance.name, self._CONT_CMD)
+
+  def GetMigrationStatus(self, instance):
+    """Get the migration status
+
+    @type instance: L{objects.Instance}
+    @param instance: the instance that is being migrated
+    @rtype: L{objects.MigrationStatus}
+    @return: the status of the current migration (one of
+             L{constants.HV_MIGRATION_VALID_STATUSES}), plus any additional
+             progress info that can be retrieved from the hypervisor
+
+    """
     info_command = "info migrate"
-    done = False
-    broken_answers = 0
-    while not done:
-      result = self._CallMonitorCommand(instance_name, info_command)
+    for _ in range(self._MIGRATION_INFO_MAX_BAD_ANSWERS):
+      result = self._CallMonitorCommand(instance.name, info_command)
       match = self._MIGRATION_STATUS_RE.search(result.stdout)
       if not match:
-        broken_answers += 1
         if not result.stdout:
           logging.info("KVM: empty 'info migrate' result")
         else:
           logging.warning("KVM: unknown 'info migrate' result: %s",
                           result.stdout)
-        time.sleep(self._MIGRATION_INFO_RETRY_DELAY)
       else:
         status = match.group(1)
-        if status == "completed":
-          done = True
-        elif status == "active":
-          # reset the broken answers count
-          broken_answers = 0
-          time.sleep(self._MIGRATION_INFO_RETRY_DELAY)
-        elif status == "failed" or status == "cancelled":
-          if not live:
-            self._CallMonitorCommand(instance_name, 'cont')
-          raise errors.HypervisorError("Migration %s at the kvm level" %
-                                       status)
-        else:
-          logging.warning("KVM: unknown migration status '%s'", status)
-          broken_answers += 1
-          time.sleep(self._MIGRATION_INFO_RETRY_DELAY)
-      if broken_answers >= self._MIGRATION_INFO_MAX_BAD_ANSWERS:
-        raise errors.HypervisorError("Too many 'info migrate' broken answers")
+        if status in constants.HV_KVM_MIGRATION_VALID_STATUSES:
+          migration_status = objects.MigrationStatus(status=status)
+          match = self._MIGRATION_PROGRESS_RE.search(result.stdout)
+          if match:
+            migration_status.transferred_ram = match.group("transferred")
+            migration_status.total_ram = match.group("total")
 
-    utils.KillProcess(pid)
-    self._RemoveInstanceRuntimeFiles(pidfile, instance_name)
+          return migration_status
+
+        logging.warning("KVM: unknown migration status '%s'", status)
+
+      time.sleep(self._MIGRATION_INFO_RETRY_DELAY)
+
+    return objects.MigrationStatus(status=constants.HV_MIGRATION_FAILED,
+                                  info="Too many 'info migrate' broken answers")
+
+  def BalloonInstanceMemory(self, instance, mem):
+    """Balloon an instance memory to a certain value.
+
+    @type instance: L{objects.Instance}
+    @param instance: instance to be accepted
+    @type mem: int
+    @param mem: actual memory size to use for instance runtime
+
+    """
+    self._CallMonitorCommand(instance.name, "balloon %d" % mem)
 
   def GetNodeInfo(self):
     """Return information about the node.
 
-    This is just a wrapper over the base GetLinuxNodeInfo method.
-
     @return: a dict with the following keys (values in MiB):
           - memory_total: the total memory size on the node
           - memory_free: the available memory on the node for instances
           - memory_dom0: the memory used by the node itself, if available
+          - hv_version: the hypervisor version in the form (major, minor,
+                        revision)
 
     """
-    return self.GetLinuxNodeInfo()
+    result = self.GetLinuxNodeInfo()
+    _, v_major, v_min, v_rev = self._GetKVMVersion()
+    result[constants.HV_NODEINFO_KEY_VERSION] = (v_major, v_min, v_rev)
+    return result
 
   @classmethod
   def GetInstanceConsole(cls, instance, hvparams, beparams):
@@ -1276,6 +1836,13 @@ class KVMHypervisor(hv_base.BaseHypervisor):
                                      port=instance.network_port,
                                      display=display)
 
+    spice_bind = hvparams[constants.HV_KVM_SPICE_BIND]
+    if spice_bind:
+      return objects.InstanceConsole(instance=instance.name,
+                                     kind=constants.CONS_SPICE,
+                                     host=spice_bind,
+                                     port=instance.network_port)
+
     return objects.InstanceConsole(instance=instance.name,
                                    kind=constants.CONS_MESSAGE,
                                    message=("No serial shell for instance %s" %
@@ -1333,8 +1900,8 @@ class KVMHypervisor(hv_base.BaseHypervisor):
                                      " security model is 'none' or 'pool'")
 
     spice_bind = hvparams[constants.HV_KVM_SPICE_BIND]
+    spice_ip_version = hvparams[constants.HV_KVM_SPICE_IP_VERSION]
     if spice_bind:
-      spice_ip_version = hvparams[constants.HV_KVM_SPICE_IP_VERSION]
       if spice_ip_version != constants.IFACE_NO_IP_VERSION_SPECIFIED:
         # if an IP version is specified, the spice_bind parameter must be an
         # IP of that family
@@ -1349,6 +1916,22 @@ class KVMHypervisor(hv_base.BaseHypervisor):
           raise errors.HypervisorError("spice: got an IPv6 address (%s), but"
                                        " the specified IP version is %s" %
                                        (spice_bind, spice_ip_version))
+    else:
+      # All the other SPICE parameters depend on spice_bind being set. Raise an
+      # error if any of them is set without it.
+      spice_additional_params = frozenset([
+        constants.HV_KVM_SPICE_IP_VERSION,
+        constants.HV_KVM_SPICE_PASSWORD_FILE,
+        constants.HV_KVM_SPICE_LOSSLESS_IMG_COMPR,
+        constants.HV_KVM_SPICE_JPEG_IMG_COMPR,
+        constants.HV_KVM_SPICE_ZLIB_GLZ_IMG_COMPR,
+        constants.HV_KVM_SPICE_STREAMING_VIDEO_DETECTION,
+        constants.HV_KVM_SPICE_USE_TLS,
+        ])
+      for param in spice_additional_params:
+        if hvparams[param]:
+          raise errors.HypervisorError("spice: %s requires %s to be set" %
+                                       (param, constants.HV_KVM_SPICE_BIND))
 
   @classmethod
   def ValidateParameters(cls, hvparams):
index 8fdc52f..70e7384 100644 (file)
@@ -361,6 +361,18 @@ class LXCHypervisor(hv_base.BaseHypervisor):
     raise HypervisorError("The LXC hypervisor doesn't implement the"
                           " reboot functionality")
 
+  def BalloonInstanceMemory(self, instance, mem):
+    """Balloon an instance memory to a certain value.
+
+    @type instance: L{objects.Instance}
+    @param instance: instance to be accepted
+    @type mem: int
+    @param mem: actual memory size to use for instance runtime
+
+    """
+    # Currently lxc instances don't have memory limits
+    pass
+
   def GetNodeInfo(self):
     """Return information about the node.
 
@@ -413,3 +425,16 @@ class LXCHypervisor(hv_base.BaseHypervisor):
 
     """
     raise HypervisorError("Migration is not supported by the LXC hypervisor")
+
+  def GetMigrationStatus(self, instance):
+    """Get the migration status
+
+    @type instance: L{objects.Instance}
+    @param instance: the instance that is being migrated
+    @rtype: L{objects.MigrationStatus}
+    @return: the status of the current migration (one of
+             L{constants.HV_MIGRATION_VALID_STATUSES}), plus any additional
+             progress info that can be retrieved from the hypervisor
+
+    """
+    raise HypervisorError("Migration is not supported by the LXC hypervisor")
index 5a772d0..a757617 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007, 2008, 2009, 2010 Google Inc.
+# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 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
@@ -34,6 +34,12 @@ from ganeti import netutils
 from ganeti import objects
 
 
+XEND_CONFIG_FILE = "/etc/xen/xend-config.sxp"
+XL_CONFIG_FILE = "/etc/xen/xl.conf"
+VIF_BRIDGE_SCRIPT = "/etc/xen/scripts/vif-bridge"
+_DOM0_NAME = "Domain-0"
+
+
 class XenHypervisor(hv_base.BaseHypervisor):
   """Xen generic hypervisor interface
 
@@ -46,12 +52,28 @@ class XenHypervisor(hv_base.BaseHypervisor):
   REBOOT_RETRY_INTERVAL = 10
 
   ANCILLARY_FILES = [
-    "/etc/xen/xend-config.sxp",
-    "/etc/xen/scripts/vif-bridge",
+    XEND_CONFIG_FILE,
+    XL_CONFIG_FILE,
+    VIF_BRIDGE_SCRIPT,
     ]
+  ANCILLARY_FILES_OPT = [
+    XL_CONFIG_FILE,
+    ]
+
+  @staticmethod
+  def _ConfigFileName(instance_name):
+    """Get the config file name for an instance.
+
+    @param instance_name: instance name
+    @type instance_name: str
+    @return: fully qualified path to instance config file
+    @rtype: str
+
+    """
+    return "/etc/xen/%s" % instance_name
 
   @classmethod
-  def _WriteConfigFile(cls, instance, block_devices):
+  def _WriteConfigFile(cls, instance, startup_memory, block_devices):
     """Write the Xen config file for the instance.
 
     """
@@ -64,7 +86,14 @@ class XenHypervisor(hv_base.BaseHypervisor):
     This version of the function just writes the config file from static data.
 
     """
-    utils.WriteFile("/etc/xen/%s" % instance_name, data=data)
+    # just in case it exists
+    utils.RemoveFile("/etc/xen/auto/%s" % instance_name)
+    cfg_file = XenHypervisor._ConfigFileName(instance_name)
+    try:
+      utils.WriteFile(cfg_file, data=data)
+    except EnvironmentError, err:
+      raise errors.HypervisorError("Cannot write Xen instance configuration"
+                                   " file %s: %s" % (cfg_file, err))
 
   @staticmethod
   def _ReadConfigFile(instance_name):
@@ -72,7 +101,8 @@ class XenHypervisor(hv_base.BaseHypervisor):
 
     """
     try:
-      file_content = utils.ReadFile("/etc/xen/%s" % instance_name)
+      file_content = utils.ReadFile(
+                       XenHypervisor._ConfigFileName(instance_name))
     except EnvironmentError, err:
       raise errors.HypervisorError("Failed to load Xen config file: %s" % err)
     return file_content
@@ -82,14 +112,46 @@ class XenHypervisor(hv_base.BaseHypervisor):
     """Remove the xen configuration file.
 
     """
-    utils.RemoveFile("/etc/xen/%s" % instance_name)
+    utils.RemoveFile(XenHypervisor._ConfigFileName(instance_name))
+
+  @classmethod
+  def _CreateConfigCpus(cls, cpu_mask):
+    """Create a CPU config string that's compatible with Xen's
+    configuration file.
+
+    """
+    # Convert the string CPU mask to a list of list of int's
+    cpu_list = utils.ParseMultiCpuMask(cpu_mask)
+
+    if len(cpu_list) == 1:
+      all_cpu_mapping = cpu_list[0]
+      if all_cpu_mapping == constants.CPU_PINNING_OFF:
+        # If CPU pinning has 1 entry that's "all", then remove the
+        # parameter from the config file
+        return None
+      else:
+        # If CPU pinning has one non-all entry, mapping all vCPUS (the entire
+        # VM) to one physical CPU, using format 'cpu = "C"'
+        return "cpu = \"%s\"" % ",".join(map(str, all_cpu_mapping))
+    else:
+      def _GetCPUMap(vcpu):
+        if vcpu[0] == constants.CPU_PINNING_ALL_VAL:
+          cpu_map = constants.CPU_PINNING_ALL_XEN
+        else:
+          cpu_map = ",".join(map(str, vcpu))
+        return "\"%s\"" % cpu_map
+
+      # build the result string in format 'cpus = [ "c", "c", "c" ]',
+      # where each c is a physical CPU number, a range, a list, or any
+      # combination
+      return "cpus = [ %s ]" % ", ".join(map(_GetCPUMap, cpu_list))
 
   @staticmethod
   def _RunXmList(xmlist_errors):
     """Helper function for L{_GetXMList} to run "xm list".
 
     """
-    result = utils.RunCmd(["xm", "list"])
+    result = utils.RunCmd([constants.XEN_CMD, "list"])
     if result.failed:
       logging.error("xm list failed (%s): %s", result.fail_reason,
                     result.output)
@@ -142,7 +204,7 @@ class XenHypervisor(hv_base.BaseHypervisor):
                                      " line: %s, error: %s" % (line, err))
 
       # skip the Domain-0 (optional)
-      if include_node or data[0] != "Domain-0":
+      if include_node or data[0] != _DOM0_NAME:
         result.append(data)
 
     return result
@@ -163,7 +225,7 @@ class XenHypervisor(hv_base.BaseHypervisor):
     @return: tuple (name, id, memory, vcpus, stat, times)
 
     """
-    xm_list = self._GetXMList(instance_name == "Domain-0")
+    xm_list = self._GetXMList(instance_name == _DOM0_NAME)
     result = None
     for data in xm_list:
       if data[0] == instance_name:
@@ -184,11 +246,12 @@ class XenHypervisor(hv_base.BaseHypervisor):
     """Start an instance.
 
     """
-    self._WriteConfigFile(instance, block_devices)
-    cmd = ["xm", "create"]
+    startup_memory = self._InstanceStartupMemory(instance)
+    self._WriteConfigFile(instance, startup_memory, block_devices)
+    cmd = [constants.XEN_CMD, "create"]
     if startup_paused:
-      cmd.extend(["--paused"])
-    cmd.extend([instance.name])
+      cmd.extend(["-p"])
+    cmd.extend([self._ConfigFileName(instance.name)])
     result = utils.RunCmd(cmd)
 
     if result.failed:
@@ -204,9 +267,9 @@ class XenHypervisor(hv_base.BaseHypervisor):
       name = instance.name
     self._RemoveConfigFile(name)
     if force:
-      command = ["xm", "destroy", name]
+      command = [constants.XEN_CMD, "destroy", name]
     else:
-      command = ["xm", "shutdown", name]
+      command = [constants.XEN_CMD, "shutdown", name]
     result = utils.RunCmd(command)
 
     if result.failed:
@@ -223,7 +286,7 @@ class XenHypervisor(hv_base.BaseHypervisor):
       raise errors.HypervisorError("Failed to reboot instance %s,"
                                    " not running" % instance.name)
 
-    result = utils.RunCmd(["xm", "reboot", instance.name])
+    result = utils.RunCmd([constants.XEN_CMD, "reboot", instance.name])
     if result.failed:
       raise errors.HypervisorError("Failed to reboot instance %s: %s, %s" %
                                    (instance.name, result.fail_reason,
@@ -247,6 +310,29 @@ class XenHypervisor(hv_base.BaseHypervisor):
                                    " did not reboot in the expected interval" %
                                    (instance.name, ))
 
+  def BalloonInstanceMemory(self, instance, mem):
+    """Balloon an instance memory to a certain value.
+
+    @type instance: L{objects.Instance}
+    @param instance: instance to be accepted
+    @type mem: int
+    @param mem: actual memory size to use for instance runtime
+
+    """
+    cmd = [constants.XEN_CMD, "mem-set", instance.name, mem]
+    result = utils.RunCmd(cmd)
+    if result.failed:
+      raise errors.HypervisorError("Failed to balloon instance %s: %s (%s)" %
+                                   (instance.name, result.fail_reason,
+                                    result.output))
+    cmd = ["sed", "-ie", "s/^memory.*$/memory = %s/" % mem]
+    cmd.append(XenHypervisor._ConfigFileName(instance.name))
+    result = utils.RunCmd(cmd)
+    if result.failed:
+      raise errors.HypervisorError("Failed to update memory for %s: %s (%s)" %
+                                   (instance.name, result.fail_reason,
+                                    result.output))
+
   def GetNodeInfo(self):
     """Return information about the node.
 
@@ -257,10 +343,10 @@ class XenHypervisor(hv_base.BaseHypervisor):
           - nr_cpus: total number of CPUs
           - nr_nodes: in a NUMA system, the number of domains
           - nr_sockets: the number of physical CPU sockets in the node
+          - hv_version: the hypervisor version in the form (major, minor)
 
     """
-    # note: in xen 3, memory has changed to total_memory
-    result = utils.RunCmd(["xm", "info"])
+    result = utils.RunCmd([constants.XEN_CMD, "info"])
     if result.failed:
       logging.error("Can't run 'xm info' (%s): %s", result.fail_reason,
                     result.output)
@@ -269,16 +355,22 @@ class XenHypervisor(hv_base.BaseHypervisor):
     xmoutput = result.stdout.splitlines()
     result = {}
     cores_per_socket = threads_per_core = nr_cpus = None
+    xen_major, xen_minor = None, None
+    memory_total = None
+    memory_free = None
+
     for line in xmoutput:
       splitfields = line.split(":", 1)
 
       if len(splitfields) > 1:
         key = splitfields[0].strip()
         val = splitfields[1].strip()
+
+        # note: in xen 3, memory has changed to total_memory
         if key == "memory" or key == "total_memory":
-          result["memory_total"] = int(val)
+          memory_total = int(val)
         elif key == "free_memory":
-          result["memory_free"] = int(val)
+          memory_free = int(val)
         elif key == "nr_cpus":
           nr_cpus = result["cpu_total"] = int(val)
         elif key == "nr_nodes":
@@ -287,14 +379,35 @@ class XenHypervisor(hv_base.BaseHypervisor):
           cores_per_socket = int(val)
         elif key == "threads_per_core":
           threads_per_core = int(val)
+        elif key == "xen_major":
+          xen_major = int(val)
+        elif key == "xen_minor":
+          xen_minor = int(val)
 
-    if (cores_per_socket is not None and
-        threads_per_core is not None and nr_cpus is not None):
+    if None not in [cores_per_socket, threads_per_core, nr_cpus]:
       result["cpu_sockets"] = nr_cpus / (cores_per_socket * threads_per_core)
 
-    dom0_info = self.GetInstanceInfo("Domain-0")
-    if dom0_info is not None:
-      result["memory_dom0"] = dom0_info[2]
+    total_instmem = 0
+    for (name, _, mem, vcpus, _, _) in self._GetXMList(True):
+      if name == _DOM0_NAME:
+        result["memory_dom0"] = mem
+        result["dom0_cpus"] = vcpus
+
+      # Include Dom0 in total memory usage
+      total_instmem += mem
+
+    if memory_free is not None:
+      result["memory_free"] = memory_free
+
+    if memory_total is not None:
+      result["memory_total"] = memory_total
+
+    # Calculate memory used by hypervisor
+    if None not in [memory_total, memory_free, total_instmem]:
+      result["memory_hv"] = memory_total - memory_free - total_instmem
+
+    if not (xen_major is None or xen_minor is None):
+      result[constants.HV_NODEINFO_KEY_VERSION] = (xen_major, xen_minor)
 
     return result
 
@@ -316,7 +429,7 @@ class XenHypervisor(hv_base.BaseHypervisor):
     For Xen, this verifies that the xend process is running.
 
     """
-    result = utils.RunCmd(["xm", "info"])
+    result = utils.RunCmd([constants.XEN_CMD, "info"])
     if result.failed:
       return "'xm info' failed: %s, %s" % (result.fail_reason, result.output)
 
@@ -383,7 +496,7 @@ class XenHypervisor(hv_base.BaseHypervisor):
     """
     pass
 
-  def FinalizeMigration(self, instance, info, success):
+  def FinalizeMigrationDst(self, instance, info, success):
     """Finalize an instance migration.
 
     After a successful migration we write the xen config file.
@@ -423,19 +536,61 @@ class XenHypervisor(hv_base.BaseHypervisor):
       raise errors.HypervisorError("Remote host %s not listening on port"
                                    " %s, cannot migrate" % (target, port))
 
-    args = ["xm", "migrate", "-p", "%d" % port]
-    if live:
-      args.append("-l")
+    # FIXME: migrate must be upgraded for transitioning to "xl" (xen 4.1).
+    #        This should be reworked in Ganeti 2.7
+    #  ssh must recognize the key of the target host for the migration
+    args = [constants.XEN_CMD, "migrate"]
+    if constants.XEN_CMD == constants.XEN_CMD_XM:
+      args.extend(["-p", "%d" % port])
+      if live:
+        args.append("-l")
+    elif constants.XEN_CMD == constants.XEN_CMD_XL:
+      args.extend(["-C", self._ConfigFileName(instance.name)])
+    else:
+      raise errors.HypervisorError("Unsupported xen command: %s" %
+                                   constants.XEN_CMD)
+
     args.extend([instance.name, target])
     result = utils.RunCmd(args)
     if result.failed:
       raise errors.HypervisorError("Failed to migrate instance %s: %s" %
                                    (instance.name, result.output))
-    # remove old xen file after migration succeeded
-    try:
-      self._RemoveConfigFile(instance.name)
-    except EnvironmentError:
-      logging.exception("Failure while removing instance config file")
+
+  def FinalizeMigrationSource(self, instance, success, live):
+    """Finalize the instance migration on the source node.
+
+    @type instance: L{objects.Instance}
+    @param instance: the instance that was migrated
+    @type success: bool
+    @param success: whether the migration succeeded or not
+    @type live: bool
+    @param live: whether the user requested a live migration or not
+
+    """
+    # pylint: disable=W0613
+    if success:
+      # remove old xen file after migration succeeded
+      try:
+        self._RemoveConfigFile(instance.name)
+      except EnvironmentError:
+        logging.exception("Failure while removing instance config file")
+
+  def GetMigrationStatus(self, instance):
+    """Get the migration status
+
+    As MigrateInstance for Xen is still blocking, if this method is called it
+    means that MigrateInstance has completed successfully. So we can safely
+    assume that the migration was successful and notify this fact to the client.
+
+    @type instance: L{objects.Instance}
+    @param instance: the instance that is being migrated
+    @rtype: L{objects.MigrationStatus}
+    @return: the status of the current migration (one of
+             L{constants.HV_MIGRATION_VALID_STATUSES}), plus any additional
+             progress info that can be retrieved from the hypervisor
+
+    """
+    return objects.MigrationStatus(status=constants.HV_MIGRATION_COMPLETED)
 
   @classmethod
   def PowercycleNode(cls):
@@ -452,7 +607,7 @@ class XenHypervisor(hv_base.BaseHypervisor):
     try:
       cls.LinuxPowercycle()
     finally:
-      utils.RunCmd(["xm", "debug", "R"])
+      utils.RunCmd([constants.XEN_CMD, "debug", "R"])
 
 
 class XenPvmHypervisor(XenHypervisor):
@@ -471,11 +626,12 @@ class XenPvmHypervisor(XenHypervisor):
     # TODO: Add a check for the blockdev prefix (matching [a-z:] or similar).
     constants.HV_BLOCKDEV_PREFIX: hv_base.NO_CHECK,
     constants.HV_REBOOT_BEHAVIOR:
-      hv_base.ParamInSet(True, constants.REBOOT_BEHAVIORS)
+      hv_base.ParamInSet(True, constants.REBOOT_BEHAVIORS),
+    constants.HV_CPU_MASK: hv_base.OPT_MULTI_CPU_MASK_CHECK,
     }
 
   @classmethod
-  def _WriteConfigFile(cls, instance, block_devices):
+  def _WriteConfigFile(cls, instance, startup_memory, block_devices):
     """Write the Xen config file for the instance.
 
     """
@@ -508,8 +664,13 @@ class XenPvmHypervisor(XenHypervisor):
         config.write("ramdisk = '%s'\n" % initrd_path)
 
     # rest of the settings
-    config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY])
+    config.write("memory = %d\n" % startup_memory)
+    config.write("maxmem = %d\n" % instance.beparams[constants.BE_MAXMEM])
     config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS])
+    cpu_pinning = cls._CreateConfigCpus(hvp[constants.HV_CPU_MASK])
+    if cpu_pinning:
+      config.write("%s\n" % cpu_pinning)
+
     config.write("name = '%s'\n" % instance.name)
 
     vif_data = []
@@ -537,14 +698,7 @@ class XenPvmHypervisor(XenHypervisor):
       config.write("on_reboot = 'destroy'\n")
     config.write("on_crash = 'restart'\n")
     config.write("extra = '%s'\n" % hvp[constants.HV_KERNEL_ARGS])
-    # just in case it exists
-    utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
-    try:
-      utils.WriteFile("/etc/xen/%s" % instance.name, data=config.getvalue())
-    except EnvironmentError, err:
-      raise errors.HypervisorError("Cannot write Xen instance confile"
-                                   " file /etc/xen/%s: %s" %
-                                   (instance.name, err))
+    cls._WriteConfigFileStatic(instance.name, config.getvalue())
 
     return True
 
@@ -555,6 +709,9 @@ class XenHvmHypervisor(XenHypervisor):
   ANCILLARY_FILES = XenHypervisor.ANCILLARY_FILES + [
     constants.VNC_PASSWORD_FILE,
     ]
+  ANCILLARY_FILES_OPT = XenHypervisor.ANCILLARY_FILES_OPT + [
+    constants.VNC_PASSWORD_FILE,
+    ]
 
   PARAMETERS = {
     constants.HV_ACPI: hv_base.NO_CHECK,
@@ -580,11 +737,12 @@ class XenHvmHypervisor(XenHypervisor):
     # TODO: Add a check for the blockdev prefix (matching [a-z:] or similar).
     constants.HV_BLOCKDEV_PREFIX: hv_base.NO_CHECK,
     constants.HV_REBOOT_BEHAVIOR:
-      hv_base.ParamInSet(True, constants.REBOOT_BEHAVIORS)
+      hv_base.ParamInSet(True, constants.REBOOT_BEHAVIORS),
+    constants.HV_CPU_MASK: hv_base.OPT_MULTI_CPU_MASK_CHECK,
     }
 
   @classmethod
-  def _WriteConfigFile(cls, instance, block_devices):
+  def _WriteConfigFile(cls, instance, startup_memory, block_devices):
     """Create a Xen 3.1 HVM config file.
 
     """
@@ -598,8 +756,13 @@ class XenHvmHypervisor(XenHypervisor):
     config.write("kernel = '%s'\n" % kpath)
 
     config.write("builder = 'hvm'\n")
-    config.write("memory = %d\n" % instance.beparams[constants.BE_MEMORY])
+    config.write("memory = %d\n" % startup_memory)
+    config.write("maxmem = %d\n" % instance.beparams[constants.BE_MAXMEM])
     config.write("vcpus = %d\n" % instance.beparams[constants.BE_VCPUS])
+    cpu_pinning = cls._CreateConfigCpus(hvp[constants.HV_CPU_MASK])
+    if cpu_pinning:
+      config.write("%s\n" % cpu_pinning)
+
     config.write("name = '%s'\n" % instance.name)
     if hvp[constants.HV_PAE]:
       config.write("pae = 1\n")
@@ -678,14 +841,6 @@ class XenHvmHypervisor(XenHypervisor):
     else:
       config.write("on_reboot = 'destroy'\n")
     config.write("on_crash = 'restart'\n")
-    # just in case it exists
-    utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
-    try:
-      utils.WriteFile("/etc/xen/%s" % instance.name,
-                      data=config.getvalue())
-    except EnvironmentError, err:
-      raise errors.HypervisorError("Cannot write Xen instance confile"
-                                   " file /etc/xen/%s: %s" %
-                                   (instance.name, err))
+    cls._WriteConfigFileStatic(instance.name, config.getvalue())
 
     return True
index 1391f2a..d91da45 100644 (file)
@@ -57,6 +57,8 @@ from ganeti import runtime
 from ganeti import netutils
 from ganeti import compat
 from ganeti import ht
+from ganeti import query
+from ganeti import qlang
 
 
 JOBQUEUE_THREADS = 25
@@ -83,6 +85,25 @@ def TimeStampNow():
   return utils.SplitTime(time.time())
 
 
+class _SimpleJobQuery:
+  """Wrapper for job queries.
+
+  Instance keeps list of fields cached, useful e.g. in L{_JobChangesChecker}.
+
+  """
+  def __init__(self, fields):
+    """Initializes this class.
+
+    """
+    self._query = query.Query(query.JOB_FIELDS, fields)
+
+  def __call__(self, job):
+    """Executes a job query using cached field list.
+
+    """
+    return self._query.OldStyleQuery([(job.id, job)], sort_by_name=False)[0]
+
+
 class _QueuedOpCode(object):
   """Encapsulates an opcode object.
 
@@ -101,7 +122,7 @@ class _QueuedOpCode(object):
                "__weakref__"]
 
   def __init__(self, op):
-    """Constructor for the _QuededOpCode.
+    """Initializes instances of this class.
 
     @type op: L{opcodes.OpCode}
     @param op: the opcode we encapsulate
@@ -383,41 +404,7 @@ class _QueuedJob(object):
         has been passed
 
     """
-    row = []
-    for fname in fields:
-      if fname == "id":
-        row.append(self.id)
-      elif fname == "status":
-        row.append(self.CalcStatus())
-      elif fname == "priority":
-        row.append(self.CalcPriority())
-      elif fname == "ops":
-        row.append([op.input.__getstate__() for op in self.ops])
-      elif fname == "opresult":
-        row.append([op.result for op in self.ops])
-      elif fname == "opstatus":
-        row.append([op.status for op in self.ops])
-      elif fname == "oplog":
-        row.append([op.log for op in self.ops])
-      elif fname == "opstart":
-        row.append([op.start_timestamp for op in self.ops])
-      elif fname == "opexec":
-        row.append([op.exec_timestamp for op in self.ops])
-      elif fname == "opend":
-        row.append([op.end_timestamp for op in self.ops])
-      elif fname == "oppriority":
-        row.append([op.priority for op in self.ops])
-      elif fname == "received_ts":
-        row.append(self.received_timestamp)
-      elif fname == "start_ts":
-        row.append(self.start_timestamp)
-      elif fname == "end_ts":
-        row.append(self.end_timestamp)
-      elif fname == "summary":
-        row.append([op.input.Summary() for op in self.ops])
-      else:
-        raise errors.OpExecError("Invalid self query field '%s'" % fname)
-    return row
+    return _SimpleJobQuery(fields)(self)
 
   def MarkUnfinishedOps(self, status, result):
     """Mark unfinished opcodes with a given status and result.
@@ -583,7 +570,7 @@ class _JobChangesChecker(object):
     @param prev_log_serial: previous job serial, as passed by the LUXI client
 
     """
-    self._fields = fields
+    self._squery = _SimpleJobQuery(fields)
     self._prev_job_info = prev_job_info
     self._prev_log_serial = prev_log_serial
 
@@ -597,7 +584,7 @@ class _JobChangesChecker(object):
     assert not job.writable, "Expected read-only job"
 
     status = job.CalcStatus()
-    job_info = job.GetInfo(self._fields)
+    job_info = self._squery(job)
     log_entries = job.GetLogEntries(self._prev_log_serial)
 
     # Serializing and deserializing data can cause type changes (e.g. from
@@ -1237,6 +1224,29 @@ class _JobProcessor(object):
       queue.release()
 
 
+def _EvaluateJobProcessorResult(depmgr, job, result):
+  """Looks at a result from L{_JobProcessor} for a job.
+
+  To be used in a L{_JobQueueWorker}.
+
+  """
+  if result == _JobProcessor.FINISHED:
+    # Notify waiting jobs
+    depmgr.NotifyWaiters(job.id)
+
+  elif result == _JobProcessor.DEFER:
+    # Schedule again
+    raise workerpool.DeferTask(priority=job.CalcPriority())
+
+  elif result == _JobProcessor.WAITDEP:
+    # No-op, dependency manager will re-schedule
+    pass
+
+  else:
+    raise errors.ProgrammerError("Job processor returned unknown status %s" %
+                                 (result, ))
+
+
 class _JobQueueWorker(workerpool.BaseWorker):
   """The actual job workers.
 
@@ -1277,23 +1287,8 @@ class _JobQueueWorker(workerpool.BaseWorker):
     wrap_execop_fn = compat.partial(self._WrapExecOpCode, setname_fn,
                                     proc.ExecOpCode)
 
-    result = _JobProcessor(queue, wrap_execop_fn, job)()
-
-    if result == _JobProcessor.FINISHED:
-      # Notify waiting jobs
-      queue.depmgr.NotifyWaiters(job.id)
-
-    elif result == _JobProcessor.DEFER:
-      # Schedule again
-      raise workerpool.DeferTask(priority=job.CalcPriority())
-
-    elif result == _JobProcessor.WAITDEP:
-      # No-op, dependency manager will re-schedule
-      pass
-
-    else:
-      raise errors.ProgrammerError("Job processor returned unknown status %s" %
-                                   (result, ))
+    _EvaluateJobProcessorResult(queue.depmgr, job,
+                                _JobProcessor(queue, wrap_execop_fn, job)())
 
   @staticmethod
   def _WrapExecOpCode(setname_fn, execop_fn, op, *args, **kwargs):
@@ -1498,6 +1493,31 @@ def _RequireOpenQueue(fn):
   return wrapper
 
 
+def _RequireNonDrainedQueue(fn):
+  """Decorator checking for a non-drained queue.
+
+  To be used with functions submitting new jobs.
+
+  """
+  def wrapper(self, *args, **kwargs):
+    """Wrapper function.
+
+    @raise errors.JobQueueDrainError: if the job queue is marked for draining
+
+    """
+    # Ok when sharing the big job queue lock, as the drain file is created when
+    # the lock is exclusive.
+    # Needs access to protected member, pylint: disable=W0212
+    if self._drained:
+      raise errors.JobQueueDrainError("Job queue is drained, refusing job")
+
+    if not self._accepting_jobs:
+      raise errors.JobQueueError("Job queue is shutting down, refusing job")
+
+    return fn(self, *args, **kwargs)
+  return wrapper
+
+
 class JobQueue(object):
   """Queue used to manage the jobs.
 
@@ -1529,6 +1549,9 @@ class JobQueue(object):
     self.acquire = self._lock.acquire
     self.release = self._lock.release
 
+    # Accept jobs by default
+    self._accepting_jobs = True
+
     # Initialize the queue, and acquire the filelock.
     # This ensures no other process is working on the job queue.
     self._queue_filelock = jstore.InitAndVerifyQueue(must_lock=True)
@@ -1548,8 +1571,9 @@ class JobQueue(object):
 
     # TODO: Check consistency across nodes
 
-    self._queue_size = 0
+    self._queue_size = None
     self._UpdateQueueSizeUnlocked()
+    assert ht.TInt(self._queue_size)
     self._drained = jstore.CheckDrainFlag()
 
     # Job dependencies
@@ -1622,6 +1646,12 @@ class JobQueue(object):
 
     logging.info("Job queue inspection finished")
 
+  def _GetRpc(self, address_list):
+    """Gets RPC runner with context.
+
+    """
+    return rpc.JobQueueRunner(self.context, address_list)
+
   @locking.ssynchronized(_LOCK)
   @_RequireOpenQueue
   def AddNode(self, node):
@@ -1635,7 +1665,7 @@ class JobQueue(object):
     assert node_name != self._my_hostname
 
     # Clean queue directory on added node
-    result = rpc.RpcRunner.call_jobqueue_purge(node_name)
+    result = self._GetRpc(None).call_jobqueue_purge(node_name)
     msg = result.fail_msg
     if msg:
       logging.warning("Cannot cleanup queue directory on node %s: %s",
@@ -1653,13 +1683,15 @@ class JobQueue(object):
     # Upload current serial file
     files.append(constants.JOB_QUEUE_SERIAL_FILE)
 
+    # Static address list
+    addrs = [node.primary_ip]
+
     for file_name in files:
       # Read file content
       content = utils.ReadFile(file_name)
 
-      result = rpc.RpcRunner.call_jobqueue_update([node_name],
-                                                  [node.primary_ip],
-                                                  file_name, content)
+      result = self._GetRpc(addrs).call_jobqueue_update([node_name], file_name,
+                                                        content)
       msg = result[node_name].fail_msg
       if msg:
         logging.error("Failed to upload file %s to node %s: %s",
@@ -1743,7 +1775,7 @@ class JobQueue(object):
 
     if replicate:
       names, addrs = self._GetNodeIp()
-      result = rpc.RpcRunner.call_jobqueue_update(names, addrs, file_name, data)
+      result = self._GetRpc(addrs).call_jobqueue_update(names, file_name, data)
       self._CheckRpcResult(result, self._nodes, "Updating %s" % file_name)
 
   def _RenameFilesUnlocked(self, rename):
@@ -1762,7 +1794,7 @@ class JobQueue(object):
 
     # ... and on all nodes
     names, addrs = self._GetNodeIp()
-    result = rpc.RpcRunner.call_jobqueue_rename(names, addrs, rename)
+    result = self._GetRpc(addrs).call_jobqueue_rename(names, rename)
     self._CheckRpcResult(result, self._nodes, "Renaming files (%r)" % rename)
 
   @staticmethod
@@ -2017,16 +2049,10 @@ class JobQueue(object):
     @param ops: The list of OpCodes that will become the new job.
     @rtype: L{_QueuedJob}
     @return: the job object to be queued
-    @raise errors.JobQueueDrainError: if the job queue is marked for draining
     @raise errors.JobQueueFull: if the job queue has too many jobs in it
     @raise errors.GenericError: If an opcode is not valid
 
     """
-    # Ok when sharing the big job queue lock, as the drain file is created when
-    # the lock is exclusive.
-    if self._drained:
-      raise errors.JobQueueDrainError("Job queue is drained, refusing job")
-
     if self._queue_size >= constants.JOB_QUEUE_SIZE_HARD_LIMIT:
       raise errors.JobQueueFull()
 
@@ -2058,6 +2084,7 @@ class JobQueue(object):
 
   @locking.ssynchronized(_LOCK)
   @_RequireOpenQueue
+  @_RequireNonDrainedQueue
   def SubmitJob(self, ops):
     """Create and store a new job.
 
@@ -2070,6 +2097,7 @@ class JobQueue(object):
 
   @locking.ssynchronized(_LOCK)
   @_RequireOpenQueue
+  @_RequireNonDrainedQueue
   def SubmitManyJobs(self, jobs):
     """Create and store multiple jobs.
 
@@ -2226,7 +2254,7 @@ class JobQueue(object):
       assert job.writable, "Can't update read-only job"
 
     filename = self._GetJobPath(job.id)
-    data = serializer.DumpJson(job.Serialize(), indent=False)
+    data = serializer.DumpJson(job.Serialize())
     logging.debug("Writing job %s to %s", job.id, filename)
     self._UpdateJobQueueFile(filename, data, replicate)
 
@@ -2407,7 +2435,42 @@ class JobQueue(object):
 
     return (archived_count, len(all_job_ids) - last_touched)
 
-  def QueryJobs(self, job_ids, fields):
+  def _Query(self, fields, qfilter):
+    qobj = query.Query(query.JOB_FIELDS, fields, qfilter=qfilter,
+                       namefield="id")
+
+    job_ids = qobj.RequestedNames()
+
+    list_all = (job_ids is None)
+
+    if list_all:
+      # Since files are added to/removed from the queue atomically, there's no
+      # risk of getting the job ids in an inconsistent state.
+      job_ids = self._GetJobIDsUnlocked()
+
+    jobs = []
+
+    for job_id in job_ids:
+      job = self.SafeLoadJobFromDisk(job_id, True, writable=False)
+      if job is not None or not list_all:
+        jobs.append((job_id, job))
+
+    return (qobj, jobs, list_all)
+
+  def QueryJobs(self, fields, qfilter):
+    """Returns a list of jobs in queue.
+
+    @type fields: sequence
+    @param fields: List of wanted fields
+    @type qfilter: None or query2 filter (list)
+    @param qfilter: Query filter
+
+    """
+    (qobj, ctx, sort_by_name) = self._Query(fields, qfilter)
+
+    return query.GetQueryResponse(qobj, ctx, sort_by_name=sort_by_name)
+
+  def OldStyleQueryJobs(self, job_ids, fields):
     """Returns a list of jobs in queue.
 
     @type job_ids: list
@@ -2419,22 +2482,36 @@ class JobQueue(object):
         the requested fields
 
     """
-    jobs = []
-    list_all = False
-    if not job_ids:
-      # Since files are added to/removed from the queue atomically, there's no
-      # risk of getting the job ids in an inconsistent state.
-      job_ids = self._GetJobIDsUnlocked()
-      list_all = True
+    qfilter = qlang.MakeSimpleFilter("id", job_ids)
 
-    for job_id in job_ids:
-      job = self.SafeLoadJobFromDisk(job_id, True)
-      if job is not None:
-        jobs.append(job.GetInfo(fields))
-      elif not list_all:
-        jobs.append(None)
+    (qobj, ctx, sort_by_name) = self._Query(fields, qfilter)
+
+    return qobj.OldStyleQuery(ctx, sort_by_name=sort_by_name)
+
+  @locking.ssynchronized(_LOCK)
+  def PrepareShutdown(self):
+    """Prepare to stop the job queue.
+
+    Disables execution of jobs in the workerpool and returns whether there are
+    any jobs currently running. If the latter is the case, the job queue is not
+    yet ready for shutdown. Once this function returns C{True} L{Shutdown} can
+    be called without interfering with any job. Queued and unfinished jobs will
+    be resumed next time.
+
+    Once this function has been called no new job submissions will be accepted
+    (see L{_RequireNonDrainedQueue}).
+
+    @rtype: bool
+    @return: Whether there are any running jobs
+
+    """
+    if self._accepting_jobs:
+      self._accepting_jobs = False
+
+      # Tell worker pool to stop processing pending tasks
+      self._wpool.SetActive(False)
 
-    return jobs
+    return self._wpool.HasRunningTasks()
 
   @locking.ssynchronized(_LOCK)
   @_RequireOpenQueue
index 2634563..ea044d5 100644 (file)
@@ -33,6 +33,7 @@ import weakref
 import logging
 import heapq
 import itertools
+import time
 
 from ganeti import errors
 from ganeti import utils
@@ -161,7 +162,7 @@ class _BaseCondition(object):
     except AttributeError:
       self._acquire_restore = self._base_acquire_restore
     try:
-      self._is_owned = lock._is_owned
+      self._is_owned = lock.is_owned
     except AttributeError:
       self._is_owned = self._base_is_owned
 
@@ -357,6 +358,11 @@ class PipeCondition(_BaseCondition):
 
     return bool(self._waiters)
 
+  def __repr__(self):
+    return ("<%s.%s waiters=%s at %#x>" %
+            (self.__class__.__module__, self.__class__.__name__,
+             self._waiters, id(self)))
+
 
 class _PipeConditionWithMode(PipeCondition):
   __slots__ = [
@@ -399,12 +405,13 @@ class SharedLock(object):
     "__pending_by_prio",
     "__pending_shared",
     "__shr",
+    "__time_fn",
     "name",
     ]
 
   __condition_class = _PipeConditionWithMode
 
-  def __init__(self, name, monitor=None):
+  def __init__(self, name, monitor=None, _time_fn=time.time):
     """Construct a new SharedLock.
 
     @param name: the name of the lock
@@ -416,6 +423,9 @@ class SharedLock(object):
 
     self.name = name
 
+    # Used for unittesting
+    self.__time_fn = _time_fn
+
     # Internal lock
     self.__lock = threading.Lock()
 
@@ -436,6 +446,11 @@ class SharedLock(object):
       logging.debug("Adding lock %s to monitor", name)
       monitor.RegisterLock(self)
 
+  def __repr__(self):
+    return ("<%s.%s name=%s at %#x>" %
+            (self.__class__.__module__, self.__class__.__name__,
+             self.name, id(self)))
+
   def GetLockInfo(self, requested):
     """Retrieves information for querying locks.
 
@@ -526,7 +541,7 @@ class SharedLock(object):
     else:
       return self.__is_exclusive()
 
-  def _is_owned(self, shared=-1):
+  def is_owned(self, shared=-1):
     """Is the current thread somehow owning the lock at this time?
 
     @param shared:
@@ -541,7 +556,9 @@ class SharedLock(object):
     finally:
       self.__lock.release()
 
-  is_owned = _is_owned
+  #: Necessary to remain compatible with threading.Condition, which tries to
+  #: retrieve a locks' "_is_owned" attribute
+  _is_owned = is_owned
 
   def _count_pending(self):
     """Returns the number of pending acquires.
@@ -672,24 +689,27 @@ class SharedLock(object):
         assert priority not in self.__pending_shared
         self.__pending_shared[priority] = wait_condition
 
+    wait_start = self.__time_fn()
+    acquired = False
+
     try:
       # Wait until we become the topmost acquire in the queue or the timeout
       # expires.
-      # TODO: Decrease timeout with spurious notifications
-      while not (self.__is_on_top(wait_condition) and
-                 self.__can_acquire(shared)):
-        # Wait for notification
-        wait_condition.wait(timeout)
-        self.__check_deleted()
+      while True:
+        if self.__is_on_top(wait_condition) and self.__can_acquire(shared):
+          self.__do_acquire(shared)
+          acquired = True
+          break
 
-        # A lot of code assumes blocking acquires always succeed. Loop
-        # internally for that case.
-        if timeout is not None:
+        # A lot of code assumes blocking acquires always succeed, therefore we
+        # can never return False for a blocking acquire
+        if (timeout is not None and
+            utils.TimeoutExpired(wait_start, timeout, _time_fn=self.__time_fn)):
           break
 
-      if self.__is_on_top(wait_condition) and self.__can_acquire(shared):
-        self.__do_acquire(shared)
-        return True
+        # Wait for notification
+        wait_condition.wait(timeout)
+        self.__check_deleted()
     finally:
       # Remove condition from queue if there are no more waiters
       if not wait_condition.has_waiting():
@@ -699,7 +719,7 @@ class SharedLock(object):
           # (e.g. on lock deletion)
           self.__pending_shared.pop(priority, None)
 
-    return False
+    return acquired
 
   def acquire(self, shared=0, timeout=None, priority=None,
               test_notify=None):
@@ -786,19 +806,38 @@ class SharedLock(object):
       # Autodetect release type
       if self.__is_exclusive():
         self.__exc = None
+        notify = True
       else:
         self.__shr.remove(threading.currentThread())
+        notify = not self.__shr
 
-      # Notify topmost condition in queue
-      (priority, prioqueue) = self.__find_first_pending_queue()
-      if prioqueue:
-        cond = prioqueue[0]
-        cond.notifyAll()
-        if cond.shared:
-          # Prevent further shared acquires from sneaking in while waiters are
-          # notified
-          self.__pending_shared.pop(priority, None)
+      # Notify topmost condition in queue if there are no owners left (for
+      # shared locks)
+      if notify:
+        self.__notify_topmost()
+    finally:
+      self.__lock.release()
 
+  def __notify_topmost(self):
+    """Notifies topmost condition in queue of pending acquires.
+
+    """
+    (priority, prioqueue) = self.__find_first_pending_queue()
+    if prioqueue:
+      cond = prioqueue[0]
+      cond.notifyAll()
+      if cond.shared:
+        # Prevent further shared acquires from sneaking in while waiters are
+        # notified
+        self.__pending_shared.pop(priority, None)
+
+  def _notify_topmost(self):
+    """Exported version of L{__notify_topmost}.
+
+    """
+    self.__lock.acquire()
+    try:
+      return self.__notify_topmost()
     finally:
       self.__lock.release()
 
@@ -830,10 +869,10 @@ class SharedLock(object):
       if not acquired:
         acquired = self.__acquire_unlocked(0, timeout, priority)
 
+      if acquired:
         assert self.__is_exclusive() and not self.__is_sharer(), \
           "Lock wasn't acquired in exclusive mode"
 
-      if acquired:
         self.__deleted = True
         self.__exc = None
 
@@ -940,17 +979,53 @@ class LockSet:
     """
     return self.__lockdict
 
-  def _is_owned(self):
-    """Is the current thread a current level owner?"""
+  def is_owned(self):
+    """Is the current thread a current level owner?
+
+    @note: Use L{check_owned} to check if a specific lock is held
+
+    """
     return threading.currentThread() in self.__owners
 
+  def check_owned(self, names, shared=-1):
+    """Check if locks are owned in a specific mode.
+
+    @type names: sequence or string
+    @param names: Lock names (or a single lock name)
+    @param shared: See L{SharedLock.is_owned}
+    @rtype: bool
+    @note: Use L{is_owned} to check if the current thread holds I{any} lock and
+      L{list_owned} to get the names of all owned locks
+
+    """
+    if isinstance(names, basestring):
+      names = [names]
+
+    # Avoid check if no locks are owned anyway
+    if names and self.is_owned():
+      candidates = []
+
+      # Gather references to all locks (in case they're deleted in the meantime)
+      for lname in names:
+        try:
+          lock = self.__lockdict[lname]
+        except KeyError:
+          raise errors.LockError("Non-existing lock '%s' in set '%s' (it may"
+                                 " have been removed)" % (lname, self.name))
+        else:
+          candidates.append(lock)
+
+      return compat.all(lock.is_owned(shared=shared) for lock in candidates)
+    else:
+      return False
+
   def _add_owned(self, name=None):
     """Note the current thread owns the given lock"""
     if name is None:
-      if not self._is_owned():
+      if not self.is_owned():
         self.__owners[threading.currentThread()] = set()
     else:
-      if self._is_owned():
+      if self.is_owned():
         self.__owners[threading.currentThread()].add(name)
       else:
         self.__owners[threading.currentThread()] = set([name])
@@ -958,29 +1033,29 @@ class LockSet:
   def _del_owned(self, name=None):
     """Note the current thread owns the given lock"""
 
-    assert not (name is None and self.__lock._is_owned()), \
+    assert not (name is None and self.__lock.is_owned()), \
            "Cannot hold internal lock when deleting owner status"
 
     if name is not None:
       self.__owners[threading.currentThread()].remove(name)
 
     # Only remove the key if we don't hold the set-lock as well
-    if (not self.__lock._is_owned() and
+    if (not self.__lock.is_owned() and
         not self.__owners[threading.currentThread()]):
       del self.__owners[threading.currentThread()]
 
-  def _list_owned(self):
+  def list_owned(self):
     """Get the set of resource names owned by the current thread"""
-    if self._is_owned():
+    if self.is_owned():
       return self.__owners[threading.currentThread()].copy()
     else:
       return set()
 
   def _release_and_delete_owned(self):
     """Release and delete all resources owned by the current thread"""
-    for lname in self._list_owned():
+    for lname in self.list_owned():
       lock = self.__lockdict[lname]
-      if lock._is_owned():
+      if lock.is_owned():
         lock.release()
       self._del_owned(name=lname)
 
@@ -1002,7 +1077,7 @@ class LockSet:
     # If we don't already own the set-level lock acquired
     # we'll get it and note we need to release it later.
     release_lock = False
-    if not self.__lock._is_owned():
+    if not self.__lock.is_owned():
       release_lock = True
       self.__lock.acquire(shared=1)
     try:
@@ -1039,8 +1114,8 @@ class LockSet:
     assert timeout is None or timeout >= 0.0
 
     # Check we don't already own locks at this level
-    assert not self._is_owned(), ("Cannot acquire locks in the same set twice"
-                                  " (lockset %s)" % self.name)
+    assert not self.is_owned(), ("Cannot acquire locks in the same set twice"
+                                 " (lockset %s)" % self.name)
 
     if priority is None:
       priority = _DEFAULT_PRIORITY
@@ -1170,7 +1245,7 @@ class LockSet:
           # We shouldn't have problems adding the lock to the owners list, but
           # if we did we'll try to release this lock and re-raise exception.
           # Of course something is going to be really wrong after this.
-          if lock._is_owned():
+          if lock.is_owned():
             lock.release()
           raise
 
@@ -1187,14 +1262,14 @@ class LockSet:
     The locks must have been acquired in exclusive mode.
 
     """
-    assert self._is_owned(), ("downgrade on lockset %s while not owning any"
-                              " lock" % self.name)
+    assert self.is_owned(), ("downgrade on lockset %s while not owning any"
+                             " lock" % self.name)
 
     # Support passing in a single resource to downgrade rather than many
     if isinstance(names, basestring):
       names = [names]
 
-    owned = self._list_owned()
+    owned = self.list_owned()
 
     if names is None:
       names = owned
@@ -1208,12 +1283,12 @@ class LockSet:
       self.__lockdict[lockname].downgrade()
 
     # Do we own the lockset in exclusive mode?
-    if self.__lock._is_owned(shared=0):
+    if self.__lock.is_owned(shared=0):
       # Have all locks been downgraded?
-      if not compat.any(lock._is_owned(shared=0)
+      if not compat.any(lock.is_owned(shared=0)
                         for lock in self.__lockdict.values()):
         self.__lock.downgrade()
-        assert self.__lock._is_owned(shared=1)
+        assert self.__lock.is_owned(shared=1)
 
     return True
 
@@ -1228,24 +1303,24 @@ class LockSet:
         (defaults to all the locks acquired at that level).
 
     """
-    assert self._is_owned(), ("release() on lock set %s while not owner" %
-                              self.name)
+    assert self.is_owned(), ("release() on lock set %s while not owner" %
+                             self.name)
 
     # Support passing in a single resource to release rather than many
     if isinstance(names, basestring):
       names = [names]
 
     if names is None:
-      names = self._list_owned()
+      names = self.list_owned()
     else:
       names = set(names)
-      assert self._list_owned().issuperset(names), (
+      assert self.list_owned().issuperset(names), (
                "release() on unheld resources %s (set %s)" %
-               (names.difference(self._list_owned()), self.name))
+               (names.difference(self.list_owned()), self.name))
 
     # First of all let's release the "all elements" lock, if set.
     # After this 'add' can work again
-    if self.__lock._is_owned():
+    if self.__lock.is_owned():
       self.__lock.release()
       self._del_owned()
 
@@ -1267,7 +1342,7 @@ class LockSet:
 
     """
     # Check we don't already own locks at this level
-    assert not self._is_owned() or self.__lock._is_owned(shared=0), \
+    assert not self.is_owned() or self.__lock.is_owned(shared=0), \
       ("Cannot add locks if the set %s is only partially owned, or shared" %
        self.name)
 
@@ -1278,7 +1353,7 @@ class LockSet:
     # If we don't already own the set-level lock acquired in an exclusive way
     # we'll get it and note we need to release it later.
     release_lock = False
-    if not self.__lock._is_owned():
+    if not self.__lock.is_owned():
       release_lock = True
       self.__lock.acquire()
 
@@ -1341,7 +1416,7 @@ class LockSet:
     # If we own any subset of this lock it must be a superset of what we want
     # to delete. The ownership must also be exclusive, but that will be checked
     # by the lock itself.
-    assert not self._is_owned() or self._list_owned().issuperset(names), (
+    assert not self.is_owned() or self.list_owned().issuperset(names), (
       "remove() on acquired lockset %s while not owning all elements" %
       self.name)
 
@@ -1358,8 +1433,8 @@ class LockSet:
         removed.append(lname)
       except (KeyError, errors.LockError):
         # This cannot happen if we were already holding it, verify:
-        assert not self._is_owned(), ("remove failed while holding lockset %s"
-                                      % self.name)
+        assert not self.is_owned(), ("remove failed while holding lockset %s" %
+                                     self.name)
       else:
         # If no LockError was raised we are the ones who deleted the lock.
         # This means we can safely remove it from lockdict, as any further or
@@ -1370,7 +1445,7 @@ class LockSet:
         # it's the job of the one who actually deleted it.
         del self.__lockdict[lname]
         # And let's remove it from our private list if we owned it.
-        if self._is_owned():
+        if self.is_owned():
           self._del_owned(name=lname)
 
     return removed
@@ -1389,24 +1464,35 @@ LEVEL_CLUSTER = 0
 LEVEL_INSTANCE = 1
 LEVEL_NODEGROUP = 2
 LEVEL_NODE = 3
+LEVEL_NODE_RES = 4
 
-LEVELS = [LEVEL_CLUSTER,
-          LEVEL_INSTANCE,
-          LEVEL_NODEGROUP,
-          LEVEL_NODE]
+LEVELS = [
+  LEVEL_CLUSTER,
+  LEVEL_INSTANCE,
+  LEVEL_NODEGROUP,
+  LEVEL_NODE,
+  LEVEL_NODE_RES,
+  ]
 
 # Lock levels which are modifiable
-LEVELS_MOD = [LEVEL_NODE, LEVEL_NODEGROUP, LEVEL_INSTANCE]
-
+LEVELS_MOD = frozenset([
+  LEVEL_NODE_RES,
+  LEVEL_NODE,
+  LEVEL_NODEGROUP,
+  LEVEL_INSTANCE,
+  ])
+
+#: Lock level names (make sure to use singular form)
 LEVEL_NAMES = {
   LEVEL_CLUSTER: "cluster",
   LEVEL_INSTANCE: "instance",
   LEVEL_NODEGROUP: "nodegroup",
   LEVEL_NODE: "node",
+  LEVEL_NODE_RES: "node-res",
   }
 
 # Constant for the big ganeti lock
-BGL = 'BGL'
+BGL = "BGL"
 
 
 class GanetiLockManager:
@@ -1441,13 +1527,17 @@ class GanetiLockManager:
     # The keyring contains all the locks, at their level and in the correct
     # locking order.
     self.__keyring = {
-      LEVEL_CLUSTER: LockSet([BGL], "BGL", monitor=self._monitor),
-      LEVEL_NODE: LockSet(nodes, "nodes", monitor=self._monitor),
-      LEVEL_NODEGROUP: LockSet(nodegroups, "nodegroups", monitor=self._monitor),
-      LEVEL_INSTANCE: LockSet(instances, "instances",
+      LEVEL_CLUSTER: LockSet([BGL], "cluster", monitor=self._monitor),
+      LEVEL_NODE: LockSet(nodes, "node", monitor=self._monitor),
+      LEVEL_NODE_RES: LockSet(nodes, "node-res", monitor=self._monitor),
+      LEVEL_NODEGROUP: LockSet(nodegroups, "nodegroup", monitor=self._monitor),
+      LEVEL_INSTANCE: LockSet(instances, "instance",
                               monitor=self._monitor),
       }
 
+    assert compat.all(ls.name == LEVEL_NAMES[level]
+                      for (level, ls) in self.__keyring.items())
+
   def AddToLockMonitor(self, provider):
     """Registers a new lock with the monitor.
 
@@ -1464,14 +1554,6 @@ class GanetiLockManager:
     """
     return self._monitor.QueryLocks(fields)
 
-  def OldStyleQueryLocks(self, fields):
-    """Queries information from all locks, returning old-style data.
-
-    See L{LockMonitor.OldStyleQueryLocks}.
-
-    """
-    return self._monitor.OldStyleQueryLocks(fields)
-
   def _names(self, level):
     """List the lock names at the given level.
 
@@ -1483,21 +1565,25 @@ class GanetiLockManager:
     assert level in LEVELS, "Invalid locking level %s" % level
     return self.__keyring[level]._names()
 
-  def _is_owned(self, level):
+  def is_owned(self, level):
     """Check whether we are owning locks at the given level
 
     """
-    return self.__keyring[level]._is_owned()
-
-  is_owned = _is_owned
+    return self.__keyring[level].is_owned()
 
-  def _list_owned(self, level):
+  def list_owned(self, level):
     """Get the set of owned locks at the given level
 
     """
-    return self.__keyring[level]._list_owned()
+    return self.__keyring[level].list_owned()
 
-  list_owned = _list_owned
+  def check_owned(self, level, names, shared=-1):
+    """Check if locks at a certain level are owned in a specific mode.
+
+    @see: L{LockSet.check_owned}
+
+    """
+    return self.__keyring[level].check_owned(names, shared=shared)
 
   def _upper_owned(self, level):
     """Check that we don't own any lock at a level greater than the given one.
@@ -1505,7 +1591,7 @@ class GanetiLockManager:
     """
     # This way of checking only works if LEVELS[i] = i, which we check for in
     # the test cases.
-    return compat.any((self._is_owned(l) for l in LEVELS[level + 1:]))
+    return compat.any((self.is_owned(l) for l in LEVELS[level + 1:]))
 
   def _BGL_owned(self): # pylint: disable=C0103
     """Check if the current thread owns the BGL.
@@ -1513,7 +1599,7 @@ class GanetiLockManager:
     Both an exclusive or a shared acquisition work.
 
     """
-    return BGL in self.__keyring[LEVEL_CLUSTER]._list_owned()
+    return BGL in self.__keyring[LEVEL_CLUSTER].list_owned()
 
   @staticmethod
   def _contains_BGL(level, names): # pylint: disable=C0103
@@ -1595,7 +1681,7 @@ class GanetiLockManager:
             not self._upper_owned(LEVEL_CLUSTER)), (
             "Cannot release the Big Ganeti Lock while holding something"
             " at upper levels (%r)" %
-            (utils.CommaJoin(["%s=%r" % (LEVEL_NAMES[i], self._list_owned(i))
+            (utils.CommaJoin(["%s=%r" % (LEVEL_NAMES[i], self.list_owned(i))
                               for i in self.__keyring.keys()]), ))
 
     # Release will complain if we don't own the locks already
@@ -1640,7 +1726,7 @@ class GanetiLockManager:
     # Check we either own the level or don't own anything from here
     # up. LockSet.remove() will check the case in which we don't own
     # all the needed resources, or we have a shared ownership.
-    assert self._is_owned(level) or not self._upper_owned(level), (
+    assert self.is_owned(level) or not self._upper_owned(level), (
            "Cannot remove locks at a level while not owning it or"
            " owning some at a greater one")
     return self.__keyring[level].remove(names)
@@ -1741,14 +1827,3 @@ class LockMonitor(object):
 
     # Prepare query response
     return query.GetQueryResponse(qobj, ctx)
-
-  def OldStyleQueryLocks(self, fields):
-    """Queries information from all locks, returning old-style data.
-
-    @type fields: list of strings
-    @param fields: List of fields to return
-
-    """
-    (qobj, ctx) = self._Query(fields)
-
-    return qobj.OldStyleQuery(ctx)
index 4371613..108b836 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007, 2011 Google Inc.
+# Copyright (C) 2006, 2007, 2011, 2012 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
@@ -34,7 +34,6 @@ import collections
 import time
 import errno
 import logging
-import warnings
 
 from ganeti import serializer
 from ganeti import constants
@@ -54,7 +53,7 @@ REQ_SUBMIT_MANY_JOBS = "SubmitManyJobs"
 REQ_WAIT_FOR_JOB_CHANGE = "WaitForJobChange"
 REQ_CANCEL_JOB = "CancelJob"
 REQ_ARCHIVE_JOB = "ArchiveJob"
-REQ_AUTOARCHIVE_JOBS = "AutoArchiveJobs"
+REQ_AUTO_ARCHIVE_JOBS = "AutoArchiveJobs"
 REQ_QUERY = "Query"
 REQ_QUERY_FIELDS = "QueryFields"
 REQ_QUERY_JOBS = "QueryJobs"
@@ -65,10 +64,31 @@ REQ_QUERY_EXPORTS = "QueryExports"
 REQ_QUERY_CONFIG_VALUES = "QueryConfigValues"
 REQ_QUERY_CLUSTER_INFO = "QueryClusterInfo"
 REQ_QUERY_TAGS = "QueryTags"
-REQ_QUERY_LOCKS = "QueryLocks"
-REQ_QUEUE_SET_DRAIN_FLAG = "SetDrainFlag"
+REQ_SET_DRAIN_FLAG = "SetDrainFlag"
 REQ_SET_WATCHER_PAUSE = "SetWatcherPause"
 
+#: List of all LUXI requests
+REQ_ALL = frozenset([
+  REQ_ARCHIVE_JOB,
+  REQ_AUTO_ARCHIVE_JOBS,
+  REQ_CANCEL_JOB,
+  REQ_QUERY,
+  REQ_QUERY_CLUSTER_INFO,
+  REQ_QUERY_CONFIG_VALUES,
+  REQ_QUERY_EXPORTS,
+  REQ_QUERY_FIELDS,
+  REQ_QUERY_GROUPS,
+  REQ_QUERY_INSTANCES,
+  REQ_QUERY_JOBS,
+  REQ_QUERY_NODES,
+  REQ_QUERY_TAGS,
+  REQ_SET_DRAIN_FLAG,
+  REQ_SET_WATCHER_PAUSE,
+  REQ_SUBMIT_JOB,
+  REQ_SUBMIT_MANY_JOBS,
+  REQ_WAIT_FOR_JOB_CHANGE,
+  ])
+
 DEF_CTMO = 10
 DEF_RWTO = 60
 
@@ -343,7 +363,7 @@ def FormatRequest(method, args, version=None):
     request[KEY_VERSION] = version
 
   # Serialize the request
-  return serializer.DumpJson(request, indent=False)
+  return serializer.DumpJson(request)
 
 
 def CallLuxiMethod(transport_cb, method, args, version=None):
@@ -439,34 +459,37 @@ class Client(object):
     """Send a generic request and return the response.
 
     """
+    if not isinstance(args, (list, tuple)):
+      raise errors.ProgrammerError("Invalid parameter passed to CallMethod:"
+                                   " expected list, got %s" % type(args))
     return CallLuxiMethod(self._SendMethodCall, method, args,
                           version=constants.LUXI_VERSION)
 
   def SetQueueDrainFlag(self, drain_flag):
-    return self.CallMethod(REQ_QUEUE_SET_DRAIN_FLAG, drain_flag)
+    return self.CallMethod(REQ_SET_DRAIN_FLAG, (drain_flag, ))
 
   def SetWatcherPause(self, until):
-    return self.CallMethod(REQ_SET_WATCHER_PAUSE, [until])
+    return self.CallMethod(REQ_SET_WATCHER_PAUSE, (until, ))
 
   def SubmitJob(self, ops):
     ops_state = map(lambda op: op.__getstate__(), ops)
-    return self.CallMethod(REQ_SUBMIT_JOB, ops_state)
+    return self.CallMethod(REQ_SUBMIT_JOB, (ops_state, ))
 
   def SubmitManyJobs(self, jobs):
     jobs_state = []
     for ops in jobs:
       jobs_state.append([op.__getstate__() for op in ops])
-    return self.CallMethod(REQ_SUBMIT_MANY_JOBS, jobs_state)
+    return self.CallMethod(REQ_SUBMIT_MANY_JOBS, (jobs_state, ))
 
   def CancelJob(self, job_id):
-    return self.CallMethod(REQ_CANCEL_JOB, job_id)
+    return self.CallMethod(REQ_CANCEL_JOB, (job_id, ))
 
   def ArchiveJob(self, job_id):
-    return self.CallMethod(REQ_ARCHIVE_JOB, job_id)
+    return self.CallMethod(REQ_ARCHIVE_JOB, (job_id, ))
 
   def AutoArchiveJobs(self, age):
     timeout = (DEF_RWTO - 1) / 2
-    return self.CallMethod(REQ_AUTOARCHIVE_JOBS, (age, timeout))
+    return self.CallMethod(REQ_AUTO_ARCHIVE_JOBS, (age, timeout))
 
   def WaitForJobChangeOnce(self, job_id, fields,
                            prev_job_info, prev_log_serial,
@@ -499,19 +522,18 @@ class Client(object):
         break
     return result
 
-  def Query(self, what, fields, filter_):
+  def Query(self, what, fields, qfilter):
     """Query for resources/items.
 
     @param what: One of L{constants.QR_VIA_LUXI}
     @type fields: List of strings
     @param fields: List of requested fields
-    @type filter_: None or list
-    @param filter_: Query filter
+    @type qfilter: None or list
+    @param qfilter: Query filter
     @rtype: L{objects.QueryResponse}
 
     """
-    req = objects.QueryRequest(what=what, fields=fields, filter=filter_)
-    result = self.CallMethod(REQ_QUERY, req.ToDict())
+    result = self.CallMethod(REQ_QUERY, (what, fields, qfilter))
     return objects.QueryResponse.FromDict(result)
 
   def QueryFields(self, what, fields):
@@ -523,8 +545,7 @@ class Client(object):
     @rtype: L{objects.QueryFieldsResponse}
 
     """
-    req = objects.QueryFieldsRequest(what=what, fields=fields)
-    result = self.CallMethod(REQ_QUERY_FIELDS, req.ToDict())
+    result = self.CallMethod(REQ_QUERY_FIELDS, (what, fields))
     return objects.QueryFieldsResponse.FromDict(result)
 
   def QueryJobs(self, job_ids, fields):
@@ -546,12 +567,7 @@ class Client(object):
     return self.CallMethod(REQ_QUERY_CLUSTER_INFO, ())
 
   def QueryConfigValues(self, fields):
-    return self.CallMethod(REQ_QUERY_CONFIG_VALUES, fields)
+    return self.CallMethod(REQ_QUERY_CONFIG_VALUES, (fields, ))
 
   def QueryTags(self, kind, name):
     return self.CallMethod(REQ_QUERY_TAGS, (kind, name))
-
-  def QueryLocks(self, fields, sync):
-    warnings.warn("This LUXI call is deprecated and will be removed, use"
-                  " Query(\"%s\", ...) instead" % constants.QR_LOCK)
-    return self.CallMethod(REQ_QUERY_LOCKS, (fields, sync))
index 2597887..e1fc908 100644 (file)
@@ -130,13 +130,6 @@ class ImportExportCbBase(object):
     """
 
 
-def _TimeoutExpired(epoch, timeout, _time_fn=time.time):
-  """Checks whether a timeout has expired.
-
-  """
-  return _time_fn() > (epoch + timeout)
-
-
 class _DiskImportExportBase(object):
   MODE_TEXT = None
 
@@ -319,7 +312,7 @@ class _DiskImportExportBase(object):
     assert self._ts_begin is not None
 
     if not data:
-      if _TimeoutExpired(self._ts_begin, self._timeouts.ready):
+      if utils.TimeoutExpired(self._ts_begin, self._timeouts.ready):
         raise _ImportExportError("Didn't become ready after %s seconds" %
                                  self._timeouts.ready)
 
@@ -342,7 +335,7 @@ class _DiskImportExportBase(object):
       if self._ts_last_error is None:
         self._ts_last_error = time.time()
 
-      elif _TimeoutExpired(self._ts_last_error, self._timeouts.error):
+      elif utils.TimeoutExpired(self._ts_last_error, self._timeouts.error):
         raise _ImportExportError("Too many errors while updating data")
 
       return False
@@ -386,7 +379,8 @@ class _DiskImportExportBase(object):
 
       return True
 
-    if _TimeoutExpired(self._GetConnectedCheckEpoch(), self._timeouts.connect):
+    if utils.TimeoutExpired(self._GetConnectedCheckEpoch(),
+                            self._timeouts.connect):
       raise _ImportExportError("Not connected after %s seconds" %
                                self._timeouts.connect)
 
@@ -397,7 +391,8 @@ class _DiskImportExportBase(object):
 
     """
     if ((self._ts_last_progress is None or
-         _TimeoutExpired(self._ts_last_progress, self._timeouts.progress)) and
+        utils.TimeoutExpired(self._ts_last_progress,
+                             self._timeouts.progress)) and
         self._daemon and
         self._daemon.progress_mbytes is not None and
         self._daemon.progress_throughput is not None):
@@ -535,7 +530,7 @@ class DiskImport(_DiskImportExportBase):
     """
     return self._lu.rpc.call_import_start(self.node_name, self._opts,
                                           self._instance, self._component,
-                                          self._dest, self._dest_args)
+                                          (self._dest, self._dest_args))
 
   def CheckListening(self):
     """Checks whether the daemon is listening.
@@ -560,7 +555,7 @@ class DiskImport(_DiskImportExportBase):
 
       return True
 
-    if _TimeoutExpired(self._ts_begin, self._timeouts.listen):
+    if utils.TimeoutExpired(self._ts_begin, self._timeouts.listen):
       raise _ImportExportError("Not listening after %s seconds" %
                                self._timeouts.listen)
 
@@ -621,7 +616,7 @@ class DiskExport(_DiskImportExportBase):
     return self._lu.rpc.call_export_start(self.node_name, self._opts,
                                           self._dest_host, self._dest_port,
                                           self._instance, self._component,
-                                          self._source, self._source_args)
+                                          (self._source, self._source_args))
 
   def CheckListening(self):
     """Checks whether the daemon is listening.
@@ -1169,7 +1164,7 @@ class ExportInstanceHelper:
 
       # result.payload will be a snapshot of an lvm leaf of the one we
       # passed
-      result = self._lu.rpc.call_blockdev_snapshot(src_node, disk)
+      result = self._lu.rpc.call_blockdev_snapshot(src_node, (disk, instance))
       new_dev = False
       msg = result.fail_msg
       if msg:
@@ -1181,9 +1176,11 @@ class ExportInstanceHelper:
                             " result '%s'", idx, src_node, result.payload)
       else:
         disk_id = tuple(result.payload)
+        disk_params = constants.DISK_LD_DEFAULTS[constants.LD_LV].copy()
         new_dev = objects.Disk(dev_type=constants.LD_LV, size=disk.size,
                                logical_id=disk_id, physical_id=disk_id,
-                               iv_name=disk.iv_name)
+                               iv_name=disk.iv_name,
+                               params=disk_params)
 
       self._snap_disks.append(new_dev)
 
index ba07b83..6e8be77 100644 (file)
@@ -31,11 +31,11 @@ are two kinds of classes defined:
 import logging
 import random
 import time
+import itertools
 
 from ganeti import opcodes
 from ganeti import constants
 from ganeti import errors
-from ganeti import rpc
 from ganeti import cmdlib
 from ganeti import locking
 from ganeti import utils
@@ -172,11 +172,81 @@ def _ComputeDispatchTable():
               if op.WITH_LU)
 
 
+def _SetBaseOpParams(src, defcomment, dst):
+  """Copies basic opcode parameters.
+
+  @type src: L{opcodes.OpCode}
+  @param src: Source opcode
+  @type defcomment: string
+  @param defcomment: Comment to specify if not already given
+  @type dst: L{opcodes.OpCode}
+  @param dst: Destination opcode
+
+  """
+  if hasattr(src, "debug_level"):
+    dst.debug_level = src.debug_level
+
+  if (getattr(dst, "priority", None) is None and
+      hasattr(src, "priority")):
+    dst.priority = src.priority
+
+  if not getattr(dst, opcodes.COMMENT_ATTR, None):
+    dst.comment = defcomment
+
+
+def _ProcessResult(submit_fn, op, result):
+  """Examines opcode result.
+
+  If necessary, additional processing on the result is done.
+
+  """
+  if isinstance(result, cmdlib.ResultWithJobs):
+    # Copy basic parameters (e.g. priority)
+    map(compat.partial(_SetBaseOpParams, op,
+                       "Submitted by %s" % op.OP_ID),
+        itertools.chain(*result.jobs))
+
+    # Submit jobs
+    job_submission = submit_fn(result.jobs)
+
+    # Build dictionary
+    result = result.other
+
+    assert constants.JOB_IDS_KEY not in result, \
+      "Key '%s' found in additional return values" % constants.JOB_IDS_KEY
+
+    result[constants.JOB_IDS_KEY] = job_submission
+
+  return result
+
+
+def _FailingSubmitManyJobs(_):
+  """Implementation of L{OpExecCbBase.SubmitManyJobs} to raise an exception.
+
+  """
+  raise errors.ProgrammerError("Opcodes processed without callbacks (e.g."
+                               " queries) can not submit jobs")
+
+
+def _RpcResultsToHooksResults(rpc_results):
+  """Function to convert RPC results to the format expected by HooksMaster.
+
+  @type rpc_results: dict(node: L{rpc.RpcResult})
+  @param rpc_results: RPC results
+  @rtype: dict(node: (fail_msg, offline, hooks_results))
+  @return: RPC results unpacked according to the format expected by
+    L({mcpu.HooksMaster}
+
+  """
+  return dict((node, (rpc_res.fail_msg, rpc_res.offline, rpc_res.payload))
+              for (node, rpc_res) in rpc_results.items())
+
+
 class Processor(object):
   """Object which runs OpCodes"""
   DISPATCH_TABLE = _ComputeDispatchTable()
 
-  def __init__(self, context, ec_id):
+  def __init__(self, context, ec_id, enable_locks=True):
     """Constructor for Processor
 
     @type context: GanetiContext
@@ -188,8 +258,18 @@ class Processor(object):
     self.context = context
     self._ec_id = ec_id
     self._cbs = None
-    self.rpc = rpc.RpcRunner(context.cfg)
+    self.rpc = context.rpc
     self.hmclass = HooksMaster
+    self._enable_locks = enable_locks
+
+  def _CheckLocksEnabled(self):
+    """Checks if locking is enabled.
+
+    @raise errors.ProgrammerError: In case locking is not enabled
+
+    """
+    if not self._enable_locks:
+      raise errors.ProgrammerError("Attempted to use disabled locks")
 
   def _AcquireLocks(self, level, names, shared, timeout, priority):
     """Acquires locks via the Ganeti lock manager.
@@ -206,6 +286,8 @@ class Processor(object):
         amount of time
 
     """
+    self._CheckLocksEnabled()
+
     if self._cbs:
       self._cbs.CheckCancel()
 
@@ -217,33 +299,14 @@ class Processor(object):
 
     return acquired
 
-  def _ProcessResult(self, result):
-    """Examines opcode result.
-
-    If necessary, additional processing on the result is done.
-
-    """
-    if isinstance(result, cmdlib.ResultWithJobs):
-      # Submit jobs
-      job_submission = self._cbs.SubmitManyJobs(result.jobs)
-
-      # Build dictionary
-      result = result.other
-
-      assert constants.JOB_IDS_KEY not in result, \
-        "Key '%s' found in additional return values" % constants.JOB_IDS_KEY
-
-      result[constants.JOB_IDS_KEY] = job_submission
-
-    return result
-
   def _ExecLU(self, lu):
     """Logical Unit execution sequence.
 
     """
     write_count = self.context.cfg.write_count
     lu.CheckPrereq()
-    hm = HooksMaster(self.rpc.call_hooks_runner, lu)
+
+    hm = self.BuildHooksManager(lu)
     h_results = hm.RunPhase(constants.HOOKS_PHASE_PRE)
     lu.HooksCallBack(constants.HOOKS_PHASE_PRE, h_results,
                      self.Log, None)
@@ -256,8 +319,13 @@ class Processor(object):
                    " the operation")
       return lu.dry_run_result
 
+    if self._cbs:
+      submit_mj_fn = self._cbs.SubmitManyJobs
+    else:
+      submit_mj_fn = _FailingSubmitManyJobs
+
     try:
-      result = self._ProcessResult(lu.Exec(self.Log))
+      result = _ProcessResult(submit_mj_fn, lu.op, lu.Exec(self.Log))
       h_results = hm.RunPhase(constants.HOOKS_PHASE_POST)
       result = lu.HooksCallBack(constants.HOOKS_PHASE_POST, h_results,
                                 self.Log, result)
@@ -268,6 +336,9 @@ class Processor(object):
 
     return result
 
+  def BuildHooksManager(self, lu):
+    return self.hmclass.BuildFromLu(lu.rpc.call_hooks_runner, lu)
+
   def _LockAndExecLU(self, lu, level, calc_timeout, priority):
     """Execute a Logical Unit, with the needed locks.
 
@@ -291,6 +362,8 @@ class Processor(object):
                                 " others")
 
     elif adding_locks or acquiring_locks:
+      self._CheckLocksEnabled()
+
       lu.DeclareLocks(level)
       share = lu.share_locks[level]
 
@@ -361,12 +434,17 @@ class Processor(object):
 
     self._cbs = cbs
     try:
-      # Acquire the Big Ganeti Lock exclusively if this LU requires it,
-      # and in a shared fashion otherwise (to prevent concurrent run with
-      # an exclusive LU.
-      self._AcquireLocks(locking.LEVEL_CLUSTER, locking.BGL,
-                          not lu_class.REQ_BGL, calc_timeout(),
-                          priority)
+      if self._enable_locks:
+        # Acquire the Big Ganeti Lock exclusively if this LU requires it,
+        # and in a shared fashion otherwise (to prevent concurrent run with
+        # an exclusive LU.
+        self._AcquireLocks(locking.LEVEL_CLUSTER, locking.BGL,
+                            not lu_class.REQ_BGL, calc_timeout(),
+                            priority)
+      elif lu_class.REQ_BGL:
+        raise errors.ProgrammerError("Opcode '%s' requires BGL, but locks are"
+                                     " disabled" % op.OP_ID)
+
       try:
         lu = lu_class(self, op, self.context, self.rpc)
         lu.ExpandNames()
@@ -379,7 +457,10 @@ class Processor(object):
           if self._ec_id:
             self.context.cfg.DropECReservations(self._ec_id)
       finally:
-        self.context.glm.release(locking.LEVEL_CLUSTER)
+        # Release BGL if owned
+        if self.context.glm.is_owned(locking.LEVEL_CLUSTER):
+          assert self._enable_locks
+          self.context.glm.release(locking.LEVEL_CLUSTER)
     finally:
       self._cbs = None
 
@@ -387,8 +468,8 @@ class Processor(object):
     if not (resultcheck_fn is None or resultcheck_fn(result)):
       logging.error("Expected opcode result matching %s, got %s",
                     resultcheck_fn, result)
-      raise errors.OpResultError("Opcode result does not match %s" %
-                                 resultcheck_fn)
+      raise errors.OpResultError("Opcode result does not match %s: %s" %
+                                 (resultcheck_fn, utils.Truncate(result, 80)))
 
     return result
 
@@ -445,28 +526,53 @@ class Processor(object):
 
 
 class HooksMaster(object):
-  """Hooks master.
+  def __init__(self, opcode, hooks_path, nodes, hooks_execution_fn,
+    hooks_results_adapt_fn, build_env_fn, log_fn, htype=None, cluster_name=None,
+    master_name=None):
+    """Base class for hooks masters.
+
+    This class invokes the execution of hooks according to the behaviour
+    specified by its parameters.
+
+    @type opcode: string
+    @param opcode: opcode of the operation to which the hooks are tied
+    @type hooks_path: string
+    @param hooks_path: prefix of the hooks directories
+    @type nodes: 2-tuple of lists
+    @param nodes: 2-tuple of lists containing nodes on which pre-hooks must be
+      run and nodes on which post-hooks must be run
+    @type hooks_execution_fn: function that accepts the following parameters:
+      (node_list, hooks_path, phase, environment)
+    @param hooks_execution_fn: function that will execute the hooks; can be
+      None, indicating that no conversion is necessary.
+    @type hooks_results_adapt_fn: function
+    @param hooks_results_adapt_fn: function that will adapt the return value of
+      hooks_execution_fn to the format expected by RunPhase
+    @type build_env_fn: function that returns a dictionary having strings as
+      keys
+    @param build_env_fn: function that builds the environment for the hooks
+    @type log_fn: function that accepts a string
+    @param log_fn: logging function
+    @type htype: string or None
+    @param htype: None or one of L{constants.HTYPE_CLUSTER},
+     L{constants.HTYPE_NODE}, L{constants.HTYPE_INSTANCE}
+    @type cluster_name: string
+    @param cluster_name: name of the cluster
+    @type master_name: string
+    @param master_name: name of the master
 
-  This class distributes the run commands to the nodes based on the
-  specific LU class.
-
-  In order to remove the direct dependency on the rpc module, the
-  constructor needs a function which actually does the remote
-  call. This will usually be rpc.call_hooks_runner, but any function
-  which behaves the same works.
+    """
+    self.opcode = opcode
+    self.hooks_path = hooks_path
+    self.hooks_execution_fn = hooks_execution_fn
+    self.hooks_results_adapt_fn = hooks_results_adapt_fn
+    self.build_env_fn = build_env_fn
+    self.log_fn = log_fn
+    self.htype = htype
+    self.cluster_name = cluster_name
+    self.master_name = master_name
 
-  """
-  def __init__(self, callfn, lu):
-    self.callfn = callfn
-    self.lu = lu
-    self.op = lu.op
     self.pre_env = self._BuildEnv(constants.HOOKS_PHASE_PRE)
-
-    if self.lu.HPATH is None:
-      nodes = (None, None)
-    else:
-      nodes = map(frozenset, self.lu.BuildHooksNodes())
-
     (self.pre_nodes, self.post_nodes) = nodes
 
   def _BuildEnv(self, phase):
@@ -485,12 +591,13 @@ class HooksMaster(object):
 
     env = {}
 
-    if self.lu.HPATH is not None:
-      lu_env = self.lu.BuildHooksEnv()
-      if lu_env:
-        assert not compat.any(key.upper().startswith(prefix) for key in lu_env)
+    if self.hooks_path is not None:
+      phase_env = self.build_env_fn()
+      if phase_env:
+        assert not compat.any(key.upper().startswith(prefix)
+                              for key in phase_env)
         env.update(("%s%s" % (prefix, key), value)
-                   for (key, value) in lu_env.items())
+                   for (key, value) in phase_env.items())
 
     if phase == constants.HOOKS_PHASE_PRE:
       assert compat.all((key.startswith("GANETI_") and
@@ -513,30 +620,29 @@ class HooksMaster(object):
   def _RunWrapper(self, node_list, hpath, phase, phase_env):
     """Simple wrapper over self.callfn.
 
-    This method fixes the environment before doing the rpc call.
+    This method fixes the environment before executing the hooks.
 
     """
-    cfg = self.lu.cfg
-
     env = {
       "PATH": constants.HOOKS_PATH,
       "GANETI_HOOKS_VERSION": constants.HOOKS_VERSION,
-      "GANETI_OP_CODE": self.op.OP_ID,
+      "GANETI_OP_CODE": self.opcode,
       "GANETI_DATA_DIR": constants.DATA_DIR,
       "GANETI_HOOKS_PHASE": phase,
       "GANETI_HOOKS_PATH": hpath,
       }
 
-    if self.lu.HTYPE:
-      env["GANETI_OBJECT_TYPE"] = self.lu.HTYPE
+    if self.htype:
+      env["GANETI_OBJECT_TYPE"] = self.htype
 
-    if cfg is not None:
-      env["GANETI_CLUSTER"] = cfg.GetClusterName()
-      env["GANETI_MASTER"] = cfg.GetMasterNode()
+    if self.cluster_name is not None:
+      env["GANETI_CLUSTER"] = self.cluster_name
+
+    if self.master_name is not None:
+      env["GANETI_MASTER"] = self.master_name
 
     if phase_env:
-      assert not (set(env) & set(phase_env)), "Environment variables conflict"
-      env.update(phase_env)
+      env = utils.algo.JoinDisjointDicts(env, phase_env)
 
     # Convert everything to strings
     env = dict([(str(key), str(val)) for key, val in env.iteritems()])
@@ -544,12 +650,15 @@ class HooksMaster(object):
     assert compat.all(key == "PATH" or key.startswith("GANETI_")
                       for key in env)
 
-    return self.callfn(node_list, hpath, phase, env)
+    return self.hooks_execution_fn(node_list, hpath, phase, env)
 
   def RunPhase(self, phase, nodes=None):
     """Run all the scripts for a phase.
 
     This is the main function of the HookMaster.
+    It executes self.hooks_execution_fn, and after running
+    self.hooks_results_adapt_fn on its results it expects them to be in the form
+    {node_name: (fail_msg, [(script, result, output), ...]}).
 
     @param phase: one of L{constants.HOOKS_PHASE_POST} or
         L{constants.HOOKS_PHASE_PRE}; it denotes the hooks phase
@@ -576,36 +685,37 @@ class HooksMaster(object):
       # even attempt to run, or this LU doesn't do hooks at all
       return
 
-    results = self._RunWrapper(nodes, self.lu.HPATH, phase, env)
+    results = self._RunWrapper(nodes, self.hooks_path, phase, env)
     if not results:
       msg = "Communication Failure"
       if phase == constants.HOOKS_PHASE_PRE:
         raise errors.HooksFailure(msg)
       else:
-        self.lu.LogWarning(msg)
+        self.log_fn(msg)
         return results
 
+    converted_res = results
+    if self.hooks_results_adapt_fn:
+      converted_res = self.hooks_results_adapt_fn(results)
+
     errs = []
-    for node_name in results:
-      res = results[node_name]
-      if res.offline:
+    for node_name, (fail_msg, offline, hooks_results) in converted_res.items():
+      if offline:
         continue
 
-      msg = res.fail_msg
-      if msg:
-        self.lu.LogWarning("Communication failure to node %s: %s",
-                           node_name, msg)
+      if fail_msg:
+        self.log_fn("Communication failure to node %s: %s", node_name, fail_msg)
         continue
 
-      for script, hkr, output in res.payload:
+      for script, hkr, output in hooks_results:
         if hkr == constants.HKR_FAIL:
           if phase == constants.HOOKS_PHASE_PRE:
             errs.append((node_name, script, output))
           else:
             if not output:
               output = "(no output)"
-            self.lu.LogWarning("On %s script %s failed, output: %s" %
-                               (node_name, script, output))
+            self.log_fn("On %s script %s failed, output: %s" %
+                        (node_name, script, output))
 
     if errs and phase == constants.HOOKS_PHASE_PRE:
       raise errors.HooksAbort(errs)
@@ -621,5 +731,21 @@ class HooksMaster(object):
     """
     phase = constants.HOOKS_PHASE_POST
     hpath = constants.HOOKS_NAME_CFGUPDATE
-    nodes = [self.lu.cfg.GetMasterNode()]
+    nodes = [self.master_name]
     self._RunWrapper(nodes, hpath, phase, self.pre_env)
+
+  @staticmethod
+  def BuildFromLu(hooks_execution_fn, lu):
+    if lu.HPATH is None:
+      nodes = (None, None)
+    else:
+      nodes = map(frozenset, lu.BuildHooksNodes())
+
+    master_name = cluster_name = None
+    if lu.cfg:
+      master_name = lu.cfg.GetMasterNode()
+      cluster_name = lu.cfg.GetClusterName()
+
+    return HooksMaster(lu.op.OP_ID, lu.HPATH, nodes, hooks_execution_fn,
+                       _RpcResultsToHooksResults, lu.BuildHooksEnv,
+                       lu.LogWarning, lu.HTYPE, cluster_name, master_name)
index 80c0219..ac87d4d 100644 (file)
@@ -362,6 +362,20 @@ class IPAddress(object):
       return False
 
   @classmethod
+  def ValidateNetmask(cls, netmask):
+    """Validate a netmask suffix in CIDR notation.
+
+    @type netmask: int
+    @param netmask: netmask suffix to validate
+    @rtype: bool
+    @return: True if valid, False otherwise
+
+    """
+    assert (isinstance(netmask, (int, long)))
+
+    return 0 < netmask <= cls.iplen
+
+  @classmethod
   def Own(cls, address):
     """Check if the current host has the the given IP address.
 
@@ -487,6 +501,36 @@ class IPAddress(object):
 
     raise errors.ProgrammerError("%s is not a valid IP version" % version)
 
+  @staticmethod
+  def GetClassFromIpVersion(version):
+    """Return the IPAddress subclass for the given IP version.
+
+    @type version: int
+    @param version: IP version, one of L{constants.IP4_VERSION} or
+                    L{constants.IP6_VERSION}
+    @return: a subclass of L{netutils.IPAddress}
+    @raise errors.ProgrammerError: for unknowo IP versions
+
+    """
+    if version == constants.IP4_VERSION:
+      return IP4Address
+    elif version == constants.IP6_VERSION:
+      return IP6Address
+
+    raise errors.ProgrammerError("%s is not a valid IP version" % version)
+
+  @staticmethod
+  def GetClassFromIpFamily(family):
+    """Return the IPAddress subclass for the given IP family.
+
+    @param family: IP family (one of C{socket.AF_INET} or C{socket.AF_INET6}
+    @return: a subclass of L{netutils.IPAddress}
+    @raise errors.ProgrammerError: for unknowo IP versions
+
+    """
+    return IPAddress.GetClassFromIpVersion(
+              IPAddress.GetVersionFromAddressFamily(family))
+
   @classmethod
   def IsLoopback(cls, address):
     """Determine whether it is a loopback address.
index ab55b3d..74526d3 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 Google Inc.
+# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 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
@@ -26,13 +26,15 @@ pass to and from external parties.
 
 """
 
-# pylint: disable=E0203,W0201
+# pylint: disable=E0203,W0201,R0902
 
 # E0203: Access to member %r before its definition, since we use
 # objects.py which doesn't explicitely initialise its members
 
 # W0201: Attribute '%s' defined outside __init__
 
+# R0902: Allow instances of these objects to have more than 20 attributes
+
 import ConfigParser
 import re
 import copy
@@ -41,6 +43,8 @@ from cStringIO import StringIO
 
 from ganeti import errors
 from ganeti import constants
+from ganeti import netutils
+from ganeti import utils
 
 from socket import AF_INET
 
@@ -76,6 +80,39 @@ def FillDict(defaults_dict, custom_dict, skip_keys=None):
   return ret_dict
 
 
+def FillIPolicy(default_ipolicy, custom_ipolicy, skip_keys=None):
+  """Fills an instance policy with defaults.
+
+  """
+  assert frozenset(default_ipolicy.keys()) == constants.IPOLICY_ALL_KEYS
+  ret_dict = {}
+  for key in constants.IPOLICY_ISPECS:
+    ret_dict[key] = FillDict(default_ipolicy[key],
+                             custom_ipolicy.get(key, {}),
+                             skip_keys=skip_keys)
+  # list items
+  for key in [constants.IPOLICY_DTS]:
+    ret_dict[key] = list(custom_ipolicy.get(key, default_ipolicy[key]))
+  # other items which we know we can directly copy (immutables)
+  for key in constants.IPOLICY_PARAMETERS:
+    ret_dict[key] = custom_ipolicy.get(key, default_ipolicy[key])
+
+  return ret_dict
+
+
+def FillDiskParams(default_dparams, custom_dparams, skip_keys=None):
+  """Fills the disk parameter defaults.
+
+  @see: L{FillDict} for parameters and return value
+
+  """
+  assert frozenset(default_dparams.keys()) == constants.DISK_TEMPLATES
+
+  return dict((dt, FillDict(default_dparams[dt], custom_dparams.get(dt, {}),
+                             skip_keys=skip_keys))
+              for dt in constants.DISK_TEMPLATES)
+
+
 def UpgradeGroupedParams(target, defaults):
   """Update all groups for the target parameter.
 
@@ -93,6 +130,63 @@ def UpgradeGroupedParams(target, defaults):
   return target
 
 
+def UpgradeBeParams(target):
+  """Update the be parameters dict to the new format.
+
+  @type target: dict
+  @param target: "be" parameters dict
+
+  """
+  if constants.BE_MEMORY in target:
+    memory = target[constants.BE_MEMORY]
+    target[constants.BE_MAXMEM] = memory
+    target[constants.BE_MINMEM] = memory
+    del target[constants.BE_MEMORY]
+
+
+def UpgradeDiskParams(diskparams):
+  """Upgrade the disk parameters.
+
+  @type diskparams: dict
+  @param diskparams: disk parameters to upgrade
+  @rtype: dict
+  @return: the upgraded disk parameters dict
+
+  """
+  if not diskparams:
+    result = {}
+  else:
+    result = FillDiskParams(constants.DISK_DT_DEFAULTS, diskparams)
+
+  return result
+
+
+def UpgradeNDParams(ndparams):
+  """Upgrade ndparams structure.
+
+  @type ndparams: dict
+  @param ndparams: disk parameters to upgrade
+  @rtype: dict
+  @return: the upgraded node parameters dict
+
+  """
+  if ndparams is None:
+    ndparams = {}
+
+  return FillDict(constants.NDC_DEFAULTS, ndparams)
+
+
+def MakeEmptyIPolicy():
+  """Create empty IPolicy dictionary.
+
+  """
+  return dict([
+    (constants.ISPECS_MIN, {}),
+    (constants.ISPECS_MAX, {}),
+    (constants.ISPECS_STD, {}),
+    ])
+
+
 class ConfigObject(object):
   """A generic config object.
 
@@ -134,6 +228,9 @@ class ConfigObject(object):
       slots.extend(getattr(parent, "__slots__", []))
     return slots
 
+  #: Public getter for the defined slots
+  GetAllSlots = _all_slots
+
   def ToDict(self):
     """Convert to a dict holding only standard python types.
 
@@ -315,6 +412,25 @@ class TaggableObject(ConfigObject):
     return obj
 
 
+class MasterNetworkParameters(ConfigObject):
+  """Network configuration parameters for the master
+
+  @ivar name: master name
+  @ivar ip: master IP
+  @ivar netmask: master netmask
+  @ivar netdev: master network device
+  @ivar ip_family: master IP family
+
+  """
+  __slots__ = [
+    "name",
+    "ip",
+    "netmask",
+    "netdev",
+    "ip_family"
+    ]
+
+
 class ConfigData(ConfigObject):
   """Top-level config object."""
   __slots__ = [
@@ -401,7 +517,8 @@ class NIC(ConfigObject):
     @raise errors.ConfigurationError: when a parameter is not valid
 
     """
-    if nicparams[constants.NIC_MODE] not in constants.NIC_VALID_MODES:
+    if (nicparams[constants.NIC_MODE] not in constants.NIC_VALID_MODES and
+        nicparams[constants.NIC_MODE] != constants.VALUE_AUTO):
       err = "Invalid nic mode: %s" % nicparams[constants.NIC_MODE]
       raise errors.ConfigurationError(err)
 
@@ -414,7 +531,7 @@ class NIC(ConfigObject):
 class Disk(ConfigObject):
   """Config object representing a block device."""
   __slots__ = ["dev_type", "logical_id", "physical_id",
-               "children", "iv_name", "size", "mode"]
+               "children", "iv_name", "size", "mode", "params"]
 
   def CreateOnSecondary(self):
     """Test if this device needs to be created on a secondary node."""
@@ -443,6 +560,8 @@ class Disk(ConfigObject):
       return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
     elif self.dev_type == constants.LD_BLOCKDEV:
       return self.logical_id[1]
+    elif self.dev_type == constants.LD_RBD:
+      return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
     return None
 
   def ChildrenNeeded(self):
@@ -486,7 +605,7 @@ class Disk(ConfigObject):
 
     """
     if self.dev_type in [constants.LD_LV, constants.LD_FILE,
-                         constants.LD_BLOCKDEV]:
+                         constants.LD_BLOCKDEV, constants.LD_RBD]:
       result = [node]
     elif self.dev_type in constants.LDS_DRBD:
       result = [self.logical_id[0], self.logical_id[1]]
@@ -561,7 +680,8 @@ class Disk(ConfigObject):
     actual algorithms from bdev.
 
     """
-    if self.dev_type in (constants.LD_LV, constants.LD_FILE):
+    if self.dev_type in (constants.LD_LV, constants.LD_FILE,
+                         constants.LD_RBD):
       self.size += amount
     elif self.dev_type == constants.LD_DRBD8:
       if self.children:
@@ -571,6 +691,21 @@ class Disk(ConfigObject):
       raise errors.ProgrammerError("Disk.RecordGrow called for unsupported"
                                    " disk type %s" % self.dev_type)
 
+  def Update(self, size=None, mode=None):
+    """Apply changes to size and mode.
+
+    """
+    if self.dev_type == constants.LD_DRBD8:
+      if self.children:
+        self.children[0].Update(size=size, mode=mode)
+    else:
+      assert not self.children
+
+    if size is not None:
+      self.size = size
+    if mode is not None:
+      self.mode = mode
+
   def UnsetSize(self):
     """Sets recursively the size to zero for the disk and its children.
 
@@ -708,8 +843,182 @@ class Disk(ConfigObject):
     if self.children:
       for child in self.children:
         child.UpgradeConfig()
+
+    # FIXME: Make this configurable in Ganeti 2.7
+    self.params = {}
     # add here config upgrade for this disk
 
+  @staticmethod
+  def ComputeLDParams(disk_template, disk_params):
+    """Computes Logical Disk parameters from Disk Template parameters.
+
+    @type disk_template: string
+    @param disk_template: disk template, one of L{constants.DISK_TEMPLATES}
+    @type disk_params: dict
+    @param disk_params: disk template parameters;
+                        dict(template_name -> parameters
+    @rtype: list(dict)
+    @return: a list of dicts, one for each node of the disk hierarchy. Each dict
+      contains the LD parameters of the node. The tree is flattened in-order.
+
+    """
+    if disk_template not in constants.DISK_TEMPLATES:
+      raise errors.ProgrammerError("Unknown disk template %s" % disk_template)
+
+    assert disk_template in disk_params
+
+    result = list()
+    dt_params = disk_params[disk_template]
+    if disk_template == constants.DT_DRBD8:
+      drbd_params = {
+        constants.LDP_RESYNC_RATE: dt_params[constants.DRBD_RESYNC_RATE],
+        constants.LDP_BARRIERS: dt_params[constants.DRBD_DISK_BARRIERS],
+        constants.LDP_NO_META_FLUSH: dt_params[constants.DRBD_META_BARRIERS],
+        constants.LDP_DEFAULT_METAVG: dt_params[constants.DRBD_DEFAULT_METAVG],
+        constants.LDP_DISK_CUSTOM: dt_params[constants.DRBD_DISK_CUSTOM],
+        constants.LDP_NET_CUSTOM: dt_params[constants.DRBD_NET_CUSTOM],
+        constants.LDP_DYNAMIC_RESYNC: dt_params[constants.DRBD_DYNAMIC_RESYNC],
+        constants.LDP_PLAN_AHEAD: dt_params[constants.DRBD_PLAN_AHEAD],
+        constants.LDP_FILL_TARGET: dt_params[constants.DRBD_FILL_TARGET],
+        constants.LDP_DELAY_TARGET: dt_params[constants.DRBD_DELAY_TARGET],
+        constants.LDP_MAX_RATE: dt_params[constants.DRBD_MAX_RATE],
+        constants.LDP_MIN_RATE: dt_params[constants.DRBD_MIN_RATE],
+        }
+
+      drbd_params = \
+        FillDict(constants.DISK_LD_DEFAULTS[constants.LD_DRBD8],
+                 drbd_params)
+
+      result.append(drbd_params)
+
+      # data LV
+      data_params = {
+        constants.LDP_STRIPES: dt_params[constants.DRBD_DATA_STRIPES],
+        }
+      data_params = \
+        FillDict(constants.DISK_LD_DEFAULTS[constants.LD_LV],
+                 data_params)
+      result.append(data_params)
+
+      # metadata LV
+      meta_params = {
+        constants.LDP_STRIPES: dt_params[constants.DRBD_META_STRIPES],
+        }
+      meta_params = \
+        FillDict(constants.DISK_LD_DEFAULTS[constants.LD_LV],
+                 meta_params)
+      result.append(meta_params)
+
+    elif (disk_template == constants.DT_FILE or
+          disk_template == constants.DT_SHARED_FILE):
+      result.append(constants.DISK_LD_DEFAULTS[constants.LD_FILE])
+
+    elif disk_template == constants.DT_PLAIN:
+      params = {
+        constants.LDP_STRIPES: dt_params[constants.LV_STRIPES],
+        }
+      params = \
+        FillDict(constants.DISK_LD_DEFAULTS[constants.LD_LV],
+                 params)
+      result.append(params)
+
+    elif disk_template == constants.DT_BLOCK:
+      result.append(constants.DISK_LD_DEFAULTS[constants.LD_BLOCKDEV])
+
+    elif disk_template == constants.DT_RBD:
+      params = {
+        constants.LDP_POOL: dt_params[constants.RBD_POOL]
+        }
+      params = \
+        FillDict(constants.DISK_LD_DEFAULTS[constants.LD_RBD],
+                 params)
+      result.append(params)
+
+    return result
+
+
+class InstancePolicy(ConfigObject):
+  """Config object representing instance policy limits dictionary.
+
+
+  Note that this object is not actually used in the config, it's just
+  used as a placeholder for a few functions.
+
+  """
+  @classmethod
+  def CheckParameterSyntax(cls, ipolicy, check_std):
+    """ Check the instance policy for validity.
+
+    """
+    for param in constants.ISPECS_PARAMETERS:
+      InstancePolicy.CheckISpecSyntax(ipolicy, param, check_std)
+    if constants.IPOLICY_DTS in ipolicy:
+      InstancePolicy.CheckDiskTemplates(ipolicy[constants.IPOLICY_DTS])
+    for key in constants.IPOLICY_PARAMETERS:
+      if key in ipolicy:
+        InstancePolicy.CheckParameter(key, ipolicy[key])
+    wrong_keys = frozenset(ipolicy.keys()) - constants.IPOLICY_ALL_KEYS
+    if wrong_keys:
+      raise errors.ConfigurationError("Invalid keys in ipolicy: %s" %
+                                      utils.CommaJoin(wrong_keys))
+
+  @classmethod
+  def CheckISpecSyntax(cls, ipolicy, name, check_std):
+    """Check the instance policy for validity on a given key.
+
+    We check if the instance policy makes sense for a given key, that is
+    if ipolicy[min][name] <= ipolicy[std][name] <= ipolicy[max][name].
+
+    @type ipolicy: dict
+    @param ipolicy: dictionary with min, max, std specs
+    @type name: string
+    @param name: what are the limits for
+    @type check_std: bool
+    @param check_std: Whether to check std value or just assume compliance
+    @raise errors.ConfigureError: when specs for given name are not valid
+
+    """
+    min_v = ipolicy[constants.ISPECS_MIN].get(name, 0)
+
+    if check_std:
+      std_v = ipolicy[constants.ISPECS_STD].get(name, min_v)
+      std_msg = std_v
+    else:
+      std_v = min_v
+      std_msg = "-"
+
+    max_v = ipolicy[constants.ISPECS_MAX].get(name, std_v)
+    err = ("Invalid specification of min/max/std values for %s: %s/%s/%s" %
+           (name,
+            ipolicy[constants.ISPECS_MIN].get(name, "-"),
+            ipolicy[constants.ISPECS_MAX].get(name, "-"),
+            std_msg))
+    if min_v > std_v or std_v > max_v:
+      raise errors.ConfigurationError(err)
+
+  @classmethod
+  def CheckDiskTemplates(cls, disk_templates):
+    """Checks the disk templates for validity.
+
+    """
+    wrong = frozenset(disk_templates).difference(constants.DISK_TEMPLATES)
+    if wrong:
+      raise errors.ConfigurationError("Invalid disk template(s) %s" %
+                                      utils.CommaJoin(wrong))
+
+  @classmethod
+  def CheckParameter(cls, key, value):
+    """Checks a parameter.
+
+    Currently we expect all parameters to be float values.
+
+    """
+    try:
+      float(value)
+    except (TypeError, ValueError), err:
+      raise errors.ConfigurationError("Invalid value for key" " '%s':"
+                                      " '%s', error: %s" % (key, value, err))
+
 
 class Instance(TaggableObject):
   """Config object representing an instance."""
@@ -721,7 +1030,7 @@ class Instance(TaggableObject):
     "hvparams",
     "beparams",
     "osparams",
-    "admin_up",
+    "admin_state",
     "nics",
     "disks",
     "disk_template",
@@ -861,6 +1170,13 @@ class Instance(TaggableObject):
     """Custom function for instances.
 
     """
+    if "admin_state" not in val:
+      if val.get("admin_up", False):
+        val["admin_state"] = constants.ADMINST_UP
+      else:
+        val["admin_state"] = constants.ADMINST_DOWN
+    if "admin_up" in val:
+      del val["admin_up"]
     obj = super(Instance, cls).FromDict(val)
     obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC)
     obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
@@ -882,6 +1198,7 @@ class Instance(TaggableObject):
           pass
     if self.osparams is None:
       self.osparams = {}
+    UpgradeBeParams(self.beparams)
 
 
 class OS(ConfigObject):
@@ -944,8 +1261,49 @@ class OS(ConfigObject):
     return cls.SplitNameVariant(name)[1]
 
 
+class NodeHvState(ConfigObject):
+  """Hypvervisor state on a node.
+
+  @ivar mem_total: Total amount of memory
+  @ivar mem_node: Memory used by, or reserved for, the node itself (not always
+    available)
+  @ivar mem_hv: Memory used by hypervisor or lost due to instance allocation
+    rounding
+  @ivar mem_inst: Memory used by instances living on node
+  @ivar cpu_total: Total node CPU core count
+  @ivar cpu_node: Number of CPU cores reserved for the node itself
+
+  """
+  __slots__ = [
+    "mem_total",
+    "mem_node",
+    "mem_hv",
+    "mem_inst",
+    "cpu_total",
+    "cpu_node",
+    ] + _TIMESTAMPS
+
+
+class NodeDiskState(ConfigObject):
+  """Disk state on a node.
+
+  """
+  __slots__ = [
+    "total",
+    "reserved",
+    "overhead",
+    ] + _TIMESTAMPS
+
+
 class Node(TaggableObject):
-  """Config object representing a node."""
+  """Config object representing a node.
+
+  @ivar hv_state: Hypervisor state (e.g. number of CPUs)
+  @ivar hv_state_static: Hypervisor state overriden by user
+  @ivar disk_state: Disk state (e.g. free space)
+  @ivar disk_state_static: Disk state overriden by user
+
+  """
   __slots__ = [
     "name",
     "primary_ip",
@@ -959,6 +1317,10 @@ class Node(TaggableObject):
     "vm_capable",
     "ndparams",
     "powered",
+    "hv_state",
+    "hv_state_static",
+    "disk_state",
+    "disk_state_static",
     ] + _TIMESTAMPS + _UUID
 
   def UpgradeConfig(self):
@@ -979,6 +1341,41 @@ class Node(TaggableObject):
     if self.powered is None:
       self.powered = True
 
+  def ToDict(self):
+    """Custom function for serializing.
+
+    """
+    data = super(Node, self).ToDict()
+
+    hv_state = data.get("hv_state", None)
+    if hv_state is not None:
+      data["hv_state"] = self._ContainerToDicts(hv_state)
+
+    disk_state = data.get("disk_state", None)
+    if disk_state is not None:
+      data["disk_state"] = \
+        dict((key, self._ContainerToDicts(value))
+             for (key, value) in disk_state.items())
+
+    return data
+
+  @classmethod
+  def FromDict(cls, val):
+    """Custom function for deserializing.
+
+    """
+    obj = super(Node, cls).FromDict(val)
+
+    if obj.hv_state is not None:
+      obj.hv_state = cls._ContainerFromDicts(obj.hv_state, dict, NodeHvState)
+
+    if obj.disk_state is not None:
+      obj.disk_state = \
+        dict((key, cls._ContainerFromDicts(value, dict, NodeDiskState))
+             for (key, value) in obj.disk_state.items())
+
+    return obj
+
 
 class NodeGroup(TaggableObject):
   """Config object representing a node group."""
@@ -986,7 +1383,11 @@ class NodeGroup(TaggableObject):
     "name",
     "members",
     "ndparams",
+    "diskparams",
+    "ipolicy",
     "serial_no",
+    "hv_state_static",
+    "disk_state_static",
     "alloc_policy",
     ] + _TIMESTAMPS + _UUID
 
@@ -1025,11 +1426,16 @@ class NodeGroup(TaggableObject):
     if self.alloc_policy is None:
       self.alloc_policy = constants.ALLOC_POLICY_PREFERRED
 
-    # We only update mtime, and not ctime, since we would not be able to provide
-    # a correct value for creation time.
+    # We only update mtime, and not ctime, since we would not be able
+    # to provide a correct value for creation time.
     if self.mtime is None:
       self.mtime = time.time()
 
+    if self.diskparams is None:
+      self.diskparams = {}
+    if self.ipolicy is None:
+      self.ipolicy = MakeEmptyIPolicy()
+
   def FillND(self, node):
     """Return filled out ndparams for L{objects.Node}
 
@@ -1069,16 +1475,20 @@ class Cluster(TaggableObject):
     "master_node",
     "master_ip",
     "master_netdev",
+    "master_netmask",
+    "use_external_mip_script",
     "cluster_name",
     "file_storage_dir",
     "shared_file_storage_dir",
     "enabled_hypervisors",
     "hvparams",
+    "ipolicy",
     "os_hvp",
     "beparams",
     "osparams",
     "nicparams",
     "ndparams",
+    "diskparams",
     "candidate_pool_size",
     "modify_etc_hosts",
     "modify_ssh_setup",
@@ -1089,6 +1499,8 @@ class Cluster(TaggableObject):
     "blacklisted_os",
     "primary_ip_family",
     "prealloc_wipe_disks",
+    "hv_state_static",
+    "disk_state_static",
     ] + _TIMESTAMPS + _UUID
 
   def UpgradeConfig(self):
@@ -1111,11 +1523,13 @@ class Cluster(TaggableObject):
     if self.osparams is None:
       self.osparams = {}
 
-    if self.ndparams is None:
-      self.ndparams = constants.NDC_DEFAULTS
+    self.ndparams = UpgradeNDParams(self.ndparams)
 
     self.beparams = UpgradeGroupedParams(self.beparams,
                                          constants.BEC_DEFAULTS)
+    for beparams_group in self.beparams:
+      UpgradeBeParams(self.beparams[beparams_group])
+
     migrate_default_bridge = not self.nicparams
     self.nicparams = UpgradeGroupedParams(self.nicparams,
                                           constants.NICC_DEFAULTS)
@@ -1168,6 +1582,10 @@ class Cluster(TaggableObject):
     if self.primary_ip_family is None:
       self.primary_ip_family = AF_INET
 
+    if self.master_netmask is None:
+      ipcls = netutils.IPAddress.GetClassFromIpFamily(self.primary_ip_family)
+      self.master_netmask = ipcls.iplen
+
     if self.prealloc_wipe_disks is None:
       self.prealloc_wipe_disks = False
 
@@ -1175,6 +1593,32 @@ class Cluster(TaggableObject):
     if self.shared_file_storage_dir is None:
       self.shared_file_storage_dir = ""
 
+    if self.use_external_mip_script is None:
+      self.use_external_mip_script = False
+
+    if self.diskparams:
+      self.diskparams = UpgradeDiskParams(self.diskparams)
+    else:
+      self.diskparams = constants.DISK_DT_DEFAULTS.copy()
+
+    # instance policy added before 2.6
+    if self.ipolicy is None:
+      self.ipolicy = FillIPolicy(constants.IPOLICY_DEFAULTS, {})
+    else:
+      # we can either make sure to upgrade the ipolicy always, or only
+      # do it in some corner cases (e.g. missing keys); note that this
+      # will break any removal of keys from the ipolicy dict
+      self.ipolicy = FillIPolicy(constants.IPOLICY_DEFAULTS, self.ipolicy)
+
+  @property
+  def primary_hypervisor(self):
+    """The first hypervisor is the primary.
+
+    Useful, for example, for L{Node}'s hv/disk state.
+
+    """
+    return self.enabled_hypervisors[0]
+
   def ToDict(self):
     """Custom function for cluster.
 
@@ -1193,6 +1637,15 @@ class Cluster(TaggableObject):
       obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
     return obj
 
+  def SimpleFillDP(self, diskparams):
+    """Fill a given diskparams dict with cluster defaults.
+
+    @param diskparams: The diskparams
+    @return: The defaults dict
+
+    """
+    return FillDiskParams(self.diskparams, diskparams)
+
   def GetHVDefaults(self, hypervisor, os_name=None, skip_keys=None):
     """Get the default hypervisor parameters for the cluster.
 
@@ -1311,6 +1764,20 @@ class Cluster(TaggableObject):
     # specified params
     return FillDict(result, os_params)
 
+  @staticmethod
+  def SimpleFillHvState(hv_state):
+    """Fill an hv_state sub dict with cluster defaults.
+
+    """
+    return FillDict(constants.HVST_DEFAULTS, hv_state)
+
+  @staticmethod
+  def SimpleFillDiskState(disk_state):
+    """Fill an disk_state sub dict with cluster defaults.
+
+    """
+    return FillDict(constants.DS_DEFAULTS, disk_state)
+
   def FillND(self, node, nodegroup):
     """Return filled out ndparams for L{objects.NodeGroup} and L{objects.Node}
 
@@ -1335,6 +1802,18 @@ class Cluster(TaggableObject):
     """
     return FillDict(self.ndparams, ndparams)
 
+  def SimpleFillIPolicy(self, ipolicy):
+    """ Fill instance policy dict with defaults.
+
+    @type ipolicy: dict
+    @param ipolicy: the dict to fill
+    @rtype: dict
+    @return: a copy of passed ipolicy with missing keys filled from
+      the cluster defaults
+
+    """
+    return FillIPolicy(self.ipolicy, ipolicy)
+
 
 class BlockDevStatus(ConfigObject):
   """Config object representing the status of a block device."""
@@ -1459,17 +1938,6 @@ class _QueryResponseBase(ConfigObject):
     return obj
 
 
-class QueryRequest(ConfigObject):
-  """Object holding a query request.
-
-  """
-  __slots__ = [
-    "what",
-    "fields",
-    "filter",
-    ]
-
-
 class QueryResponse(_QueryResponseBase):
   """Object holding the response to a query.
 
@@ -1502,6 +1970,17 @@ class QueryFieldsResponse(_QueryResponseBase):
     ]
 
 
+class MigrationStatus(ConfigObject):
+  """Object holding the status of a migration.
+
+  """
+  __slots__ = [
+    "status",
+    "transferred_ram",
+    "total_ram",
+    ]
+
+
 class InstanceConsole(ConfigObject):
   """Object describing how to access the console of an instance.
 
@@ -1523,15 +2002,20 @@ class InstanceConsole(ConfigObject):
     """
     assert self.kind in constants.CONS_ALL, "Unknown console type"
     assert self.instance, "Missing instance name"
-    assert self.message or self.kind in [constants.CONS_SSH, constants.CONS_VNC]
+    assert self.message or self.kind in [constants.CONS_SSH,
+                                         constants.CONS_SPICE,
+                                         constants.CONS_VNC]
     assert self.host or self.kind == constants.CONS_MESSAGE
     assert self.port or self.kind in [constants.CONS_MESSAGE,
                                       constants.CONS_SSH]
     assert self.user or self.kind in [constants.CONS_MESSAGE,
+                                      constants.CONS_SPICE,
                                       constants.CONS_VNC]
     assert self.command or self.kind in [constants.CONS_MESSAGE,
+                                         constants.CONS_SPICE,
                                          constants.CONS_VNC]
     assert self.display or self.kind in [constants.CONS_MESSAGE,
+                                         constants.CONS_SPICE,
                                          constants.CONS_SSH]
     return True
 
index daf3801..aa7cce1 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 Google Inc.
+# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 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
@@ -36,10 +36,10 @@ opcodes.
 import logging
 import re
 
-from ganeti import compat
 from ganeti import constants
 from ganeti import errors
 from ganeti import ht
+from ganeti import objects
 
 
 # Common opcode attributes
@@ -80,10 +80,12 @@ _PMigrationLive = ("live", None, ht.TMaybeBool,
                    "Legacy setting for live migration, do not use")
 
 #: Tag type
-_PTagKind = ("kind", ht.NoDefault, ht.TElemOf(constants.VALID_TAG_TYPES), None)
+_PTagKind = ("kind", ht.NoDefault, ht.TElemOf(constants.VALID_TAG_TYPES),
+             "Tag kind")
 
 #: List of tag strings
-_PTags = ("tags", ht.NoDefault, ht.TListOf(ht.TNonEmptyString), None)
+_PTags = ("tags", ht.NoDefault, ht.TListOf(ht.TNonEmptyString),
+          "List of tag names")
 
 _PForceVariant = ("force_variant", False, ht.TBool,
                   "Whether to force an unknown OS variant")
@@ -137,31 +139,75 @@ _PErrorCodes = ("error_codes", False, ht.TBool, "Error codes")
 _PSkipChecks = ("skip_checks", ht.EmptyList,
                 ht.TListOf(ht.TElemOf(constants.VERIFY_OPTIONAL_CHECKS)),
                 "Which checks to skip")
+_PIgnoreErrors = ("ignore_errors", ht.EmptyList,
+                  ht.TListOf(ht.TElemOf(constants.CV_ALL_ECODES_STRINGS)),
+                  "List of error codes that should be treated as warnings")
+
+# Disk parameters
+_PDiskParams = ("diskparams", None,
+                ht.TOr(
+                  ht.TDictOf(ht.TElemOf(constants.DISK_TEMPLATES), ht.TDict),
+                  ht.TNone),
+                "Disk templates' parameter defaults")
+
+# Parameters for node resource model
+_PHvState = ("hv_state", None, ht.TMaybeDict, "Set hypervisor states")
+_PDiskState = ("disk_state", None, ht.TMaybeDict, "Set disk states")
+
+
+_PIgnoreIpolicy = ("ignore_ipolicy", False, ht.TBool,
+                   "Whether to ignore ipolicy violations")
+
+# Allow runtime changes while migrating
+_PAllowRuntimeChgs = ("allow_runtime_changes", True, ht.TBool,
+                      "Allow runtime changes (eg. memory ballooning)")
+
 
 #: OP_ID conversion regular expression
 _OPID_RE = re.compile("([a-z])([A-Z])")
 
 #: Utility function for L{OpClusterSetParams}
-_TestClusterOsList = ht.TOr(ht.TNone,
-  ht.TListOf(ht.TAnd(ht.TList, ht.TIsLength(2),
-    ht.TMap(ht.WithDesc("GetFirstItem")(compat.fst),
-            ht.TElemOf(constants.DDMS_VALUES)))))
+_TestClusterOsListItem = \
+  ht.TAnd(ht.TIsLength(2), ht.TItems([
+    ht.TElemOf(constants.DDMS_VALUES),
+    ht.TNonEmptyString,
+    ]))
 
+_TestClusterOsList = ht.TMaybeListOf(_TestClusterOsListItem)
 
 # TODO: Generate check from constants.INIC_PARAMS_TYPES
 #: Utility function for testing NIC definitions
-_TestNicDef = ht.TDictOf(ht.TElemOf(constants.INIC_PARAMS),
-                         ht.TOr(ht.TNone, ht.TNonEmptyString))
+_TestNicDef = \
+  ht.Comment("NIC parameters")(ht.TDictOf(ht.TElemOf(constants.INIC_PARAMS),
+                                          ht.TOr(ht.TNone, ht.TNonEmptyString)))
 
 _TSetParamsResultItemItems = [
   ht.Comment("name of changed parameter")(ht.TNonEmptyString),
-  ht.TAny,
+  ht.Comment("new value")(ht.TAny),
   ]
 
 _TSetParamsResult = \
   ht.TListOf(ht.TAnd(ht.TIsLength(len(_TSetParamsResultItemItems)),
                      ht.TItems(_TSetParamsResultItemItems)))
 
+# TODO: Generate check from constants.IDISK_PARAMS_TYPES (however, not all users
+# of this check support all parameters)
+_TDiskParams = \
+  ht.Comment("Disk parameters")(ht.TDictOf(ht.TElemOf(constants.IDISK_PARAMS),
+                                           ht.TOr(ht.TNonEmptyString, ht.TInt)))
+
+_TQueryRow = \
+  ht.TListOf(ht.TAnd(ht.TIsLength(2),
+                     ht.TItems([ht.TElemOf(constants.RS_ALL),
+                                ht.TAny])))
+
+_TQueryResult = ht.TListOf(_TQueryRow)
+
+_TOldQueryRow = ht.TListOf(ht.TAny)
+
+_TOldQueryResult = ht.TListOf(_TOldQueryRow)
+
+
 _SUMMARY_PREFIX = {
   "CLUSTER_": "C_",
   "GROUP_": "G_",
@@ -197,6 +243,28 @@ def _NameToId(name):
   return "_".join(n.upper() for n in elems)
 
 
+def _GenerateObjectTypeCheck(obj, fields_types):
+  """Helper to generate type checks for objects.
+
+  @param obj: The object to generate type checks
+  @param fields_types: The fields and their types as a dict
+  @return: A ht type check function
+
+  """
+  assert set(obj.GetAllSlots()) == set(fields_types.keys()), \
+    "%s != %s" % (set(obj.GetAllSlots()), set(fields_types.keys()))
+  return ht.TStrictDict(True, True, fields_types)
+
+
+_TQueryFieldDef = \
+  _GenerateObjectTypeCheck(objects.QueryFieldDefinition, {
+    "name": ht.TNonEmptyString,
+    "title": ht.TNonEmptyString,
+    "kind": ht.TElemOf(constants.QFT_ALL),
+    "doc": ht.TNonEmptyString,
+    })
+
+
 def RequireFileStorage():
   """Checks that file storage is enabled.
 
@@ -237,8 +305,20 @@ def _CheckFileStorage(value):
   return True
 
 
-_CheckDiskTemplate = ht.TAnd(ht.TElemOf(constants.DISK_TEMPLATES),
-                             _CheckFileStorage)
+def _BuildDiskTemplateCheck(accept_none):
+  """Builds check for disk template.
+
+  @type accept_none: bool
+  @param accept_none: whether to accept None as a correct value
+  @rtype: callable
+
+  """
+  template_check = ht.TElemOf(constants.DISK_TEMPLATES)
+
+  if accept_none:
+    template_check = ht.TOr(template_check, ht.TNone)
+
+  return ht.TAnd(template_check, _CheckFileStorage)
 
 
 def _CheckStorageType(storage_type):
@@ -432,7 +512,7 @@ def _BuildJobDepCheck(relative):
             ht.TItems([job_id,
                        ht.TListOf(ht.TElemOf(constants.JOBS_FINALIZED))]))
 
-  return ht.TOr(ht.TNone, ht.TListOf(job_dep))
+  return ht.TMaybeListOf(job_dep)
 
 
 TNoRelativeJobDependencies = _BuildJobDepCheck(False)
@@ -483,7 +563,8 @@ class OpCode(BaseOpCode):
      ht.TElemOf(constants.OP_PRIO_SUBMIT_VALID), "Opcode priority"),
     (DEPEND_ATTR, None, _BuildJobDepCheck(True),
      "Job dependencies; if used through ``SubmitManyJobs`` relative (negative)"
-     " job IDs can be used"),
+     " job IDs can be used; see :doc:`design document <design-chained-jobs>`"
+     " for details"),
     (COMMENT_ATTR, None, ht.TMaybeString,
      "Comment describing the purpose of the opcode"),
     ]
@@ -578,6 +659,7 @@ class OpClusterPostInit(OpCode):
   after the cluster has been initialized.
 
   """
+  OP_RESULT = ht.TBool
 
 
 class OpClusterDestroy(OpCode):
@@ -587,10 +669,12 @@ class OpClusterDestroy(OpCode):
   lost after the execution of this opcode.
 
   """
+  OP_RESULT = ht.TNonEmptyString
 
 
 class OpClusterQuery(OpCode):
   """Query cluster information."""
+  OP_RESULT = ht.TDictOf(ht.TNonEmptyString, ht.TAny)
 
 
 class OpClusterVerify(OpCode):
@@ -601,6 +685,7 @@ class OpClusterVerify(OpCode):
     _PDebugSimulateErrors,
     _PErrorCodes,
     _PSkipChecks,
+    _PIgnoreErrors,
     _PVerbose,
     ("group_name", None, ht.TMaybeString, "Group to verify")
     ]
@@ -614,6 +699,7 @@ class OpClusterVerifyConfig(OpCode):
   OP_PARAMS = [
     _PDebugSimulateErrors,
     _PErrorCodes,
+    _PIgnoreErrors,
     _PVerbose,
     ]
   OP_RESULT = ht.TBool
@@ -635,6 +721,7 @@ class OpClusterVerifyGroup(OpCode):
     _PDebugSimulateErrors,
     _PErrorCodes,
     _PSkipChecks,
+    _PIgnoreErrors,
     _PVerbose,
     ]
   OP_RESULT = ht.TBool
@@ -696,6 +783,10 @@ class OpClusterRepairDiskSizes(OpCode):
   OP_PARAMS = [
     ("instances", ht.EmptyList, ht.TListOf(ht.TNonEmptyString), None),
     ]
+  OP_RESULT = ht.TListOf(ht.TAnd(ht.TIsLength(3),
+                                 ht.TItems([ht.TNonEmptyString,
+                                            ht.TPositiveInt,
+                                            ht.TPositiveInt])))
 
 
 class OpClusterConfigQuery(OpCode):
@@ -703,6 +794,7 @@ class OpClusterConfigQuery(OpCode):
   OP_PARAMS = [
     _POutputFields
     ]
+  OP_RESULT = ht.TListOf(ht.TAny)
 
 
 class OpClusterRename(OpCode):
@@ -718,6 +810,7 @@ class OpClusterRename(OpCode):
   OP_PARAMS = [
     ("name", ht.NoDefault, ht.TNonEmptyString, None),
     ]
+  OP_RESULT = ht.TNonEmptyString
 
 
 class OpClusterSetParams(OpCode):
@@ -728,6 +821,8 @@ class OpClusterSetParams(OpCode):
 
   """
   OP_PARAMS = [
+    _PHvState,
+    _PDiskState,
     ("vg_name", None, ht.TMaybeString, "Volume group name"),
     ("enabled_hypervisors", None,
      ht.TOr(ht.TAnd(ht.TListOf(ht.TElemOf(constants.HYPER_TYPES)), ht.TTrue),
@@ -744,6 +839,7 @@ class OpClusterSetParams(OpCode):
     ("osparams", None, ht.TOr(ht.TDictOf(ht.TNonEmptyString, ht.TDict),
                               ht.TNone),
      "Cluster-wide OS parameter defaults"),
+    _PDiskParams,
     ("candidate_pool_size", None, ht.TOr(ht.TStrictPositiveInt, ht.TNone),
      "Master candidate pool size"),
     ("uid_pool", None, ht.NoType,
@@ -761,28 +857,50 @@ class OpClusterSetParams(OpCode):
      "Whether to wipe disks before allocating them to instances"),
     ("nicparams", None, ht.TMaybeDict, "Cluster-wide NIC parameter defaults"),
     ("ndparams", None, ht.TMaybeDict, "Cluster-wide node parameter defaults"),
+    ("ipolicy", None, ht.TMaybeDict,
+     "Cluster-wide :ref:`instance policy <rapi-ipolicy>` specs"),
     ("drbd_helper", None, ht.TOr(ht.TString, ht.TNone), "DRBD helper program"),
     ("default_iallocator", None, ht.TOr(ht.TString, ht.TNone),
      "Default iallocator for cluster"),
     ("master_netdev", None, ht.TOr(ht.TString, ht.TNone),
      "Master network device"),
-    ("reserved_lvs", None, ht.TOr(ht.TListOf(ht.TNonEmptyString), ht.TNone),
+    ("master_netmask", None, ht.TOr(ht.TInt, ht.TNone),
+     "Netmask of the master IP"),
+    ("reserved_lvs", None, ht.TMaybeListOf(ht.TNonEmptyString),
      "List of reserved LVs"),
     ("hidden_os", None, _TestClusterOsList,
-     "Modify list of hidden operating systems. Each modification must have"
-     " two items, the operation and the OS name. The operation can be"
-     " ``%s`` or ``%s``." % (constants.DDM_ADD, constants.DDM_REMOVE)),
+     "Modify list of hidden operating systems: each modification must have"
+     " two items, the operation and the OS name; the operation can be"
+     " ``%s`` or ``%s``" % (constants.DDM_ADD, constants.DDM_REMOVE)),
     ("blacklisted_os", None, _TestClusterOsList,
-     "Modify list of blacklisted operating systems. Each modification must have"
-     " two items, the operation and the OS name. The operation can be"
-     " ``%s`` or ``%s``." % (constants.DDM_ADD, constants.DDM_REMOVE)),
+     "Modify list of blacklisted operating systems: each modification must"
+     " have two items, the operation and the OS name; the operation can be"
+     " ``%s`` or ``%s``" % (constants.DDM_ADD, constants.DDM_REMOVE)),
+    ("use_external_mip_script", None, ht.TMaybeBool,
+     "Whether to use an external master IP address setup script"),
     ]
+  OP_RESULT = ht.TNone
 
 
 class OpClusterRedistConf(OpCode):
   """Force a full push of the cluster configuration.
 
   """
+  OP_RESULT = ht.TNone
+
+
+class OpClusterActivateMasterIp(OpCode):
+  """Activate the master IP on the master node.
+
+  """
+  OP_RESULT = ht.TNone
+
+
+class OpClusterDeactivateMasterIp(OpCode):
+  """Deactivate the master IP on the master node.
+
+  """
+  OP_RESULT = ht.TNone
 
 
 class OpQuery(OpCode):
@@ -790,7 +908,7 @@ class OpQuery(OpCode):
 
   @ivar what: Resources to query for, must be one of L{constants.QR_VIA_OP}
   @ivar fields: List of fields to retrieve
-  @ivar filter: Query filter
+  @ivar qfilter: Query filter
 
   """
   OP_DSC_FIELD = "what"
@@ -799,9 +917,14 @@ class OpQuery(OpCode):
     _PUseLocking,
     ("fields", ht.NoDefault, ht.TListOf(ht.TNonEmptyString),
      "Requested fields"),
-    ("filter", None, ht.TOr(ht.TNone, ht.TList),
+    ("qfilter", None, ht.TOr(ht.TNone, ht.TList),
      "Query filter"),
     ]
+  OP_RESULT = \
+    _GenerateObjectTypeCheck(objects.QueryResponse, {
+      "fields": ht.TListOf(_TQueryFieldDef),
+      "data": _TQueryResult,
+      })
 
 
 class OpQueryFields(OpCode):
@@ -814,9 +937,13 @@ class OpQueryFields(OpCode):
   OP_DSC_FIELD = "what"
   OP_PARAMS = [
     _PQueryWhat,
-    ("fields", None, ht.TOr(ht.TNone, ht.TListOf(ht.TNonEmptyString)),
+    ("fields", None, ht.TMaybeListOf(ht.TNonEmptyString),
      "Requested fields; if not given, all are returned"),
     ]
+  OP_RESULT = \
+    _GenerateObjectTypeCheck(objects.QueryFieldsResponse, {
+      "fields": ht.TListOf(_TQueryFieldDef),
+      })
 
 
 class OpOobCommand(OpCode):
@@ -833,6 +960,8 @@ class OpOobCommand(OpCode):
     ("power_delay", constants.OOB_POWER_DELAY, ht.TPositiveFloat,
      "Time in seconds to wait between powering on nodes"),
     ]
+  # Fixme: Make it more specific with all the special cases in LUOobCommand
+  OP_RESULT = _TQueryResult
 
 
 # node opcodes
@@ -849,6 +978,7 @@ class OpNodeRemove(OpCode):
   OP_PARAMS = [
     _PNodeName,
     ]
+  OP_RESULT = ht.TNone
 
 
 class OpNodeAdd(OpCode):
@@ -882,6 +1012,8 @@ class OpNodeAdd(OpCode):
   OP_DSC_FIELD = "node_name"
   OP_PARAMS = [
     _PNodeName,
+    _PHvState,
+    _PDiskState,
     ("primary_ip", None, ht.NoType, "Primary IP address"),
     ("secondary_ip", None, ht.TMaybeString, "Secondary IP address"),
     ("readd", False, ht.TBool, "Whether node is re-added to cluster"),
@@ -892,6 +1024,7 @@ class OpNodeAdd(OpCode):
      "Whether node can host instances"),
     ("ndparams", None, ht.TMaybeDict, "Node parameters"),
     ]
+  OP_RESULT = ht.TNone
 
 
 class OpNodeQuery(OpCode):
@@ -902,6 +1035,7 @@ class OpNodeQuery(OpCode):
     ("names", ht.EmptyList, ht.TListOf(ht.TNonEmptyString),
      "Empty list to query all nodes, node names otherwise"),
     ]
+  OP_RESULT = _TOldQueryResult
 
 
 class OpNodeQueryvols(OpCode):
@@ -911,6 +1045,7 @@ class OpNodeQueryvols(OpCode):
     ("nodes", ht.EmptyList, ht.TListOf(ht.TNonEmptyString),
      "Empty list to query all nodes, node names otherwise"),
     ]
+  OP_RESULT = ht.TListOf(ht.TAny)
 
 
 class OpNodeQueryStorage(OpCode):
@@ -921,6 +1056,7 @@ class OpNodeQueryStorage(OpCode):
     ("nodes", ht.EmptyList, ht.TListOf(ht.TNonEmptyString), "List of nodes"),
     ("name", None, ht.TMaybeString, "Storage name"),
     ]
+  OP_RESULT = _TOldQueryResult
 
 
 class OpNodeModifyStorage(OpCode):
@@ -931,6 +1067,7 @@ class OpNodeModifyStorage(OpCode):
     _PStorageName,
     ("changes", ht.NoDefault, ht.TDict, "Requested changes"),
     ]
+  OP_RESULT = ht.TNone
 
 
 class OpRepairNodeStorage(OpCode):
@@ -942,6 +1079,7 @@ class OpRepairNodeStorage(OpCode):
     _PStorageName,
     _PIgnoreConsistency,
     ]
+  OP_RESULT = ht.TNone
 
 
 class OpNodeSetParams(OpCode):
@@ -950,6 +1088,8 @@ class OpNodeSetParams(OpCode):
   OP_PARAMS = [
     _PNodeName,
     _PForce,
+    _PHvState,
+    _PDiskState,
     ("master_candidate", None, ht.TMaybeBool,
      "Whether the node should become a master candidate"),
     ("offline", None, ht.TMaybeBool,
@@ -978,6 +1118,7 @@ class OpNodePowercycle(OpCode):
     _PNodeName,
     _PForce,
     ]
+  OP_RESULT = ht.TMaybeString
 
 
 class OpNodeMigrate(OpCode):
@@ -988,6 +1129,8 @@ class OpNodeMigrate(OpCode):
     _PMigrationMode,
     _PMigrationLive,
     _PMigrationTargetNode,
+    _PAllowRuntimeChgs,
+    _PIgnoreIpolicy,
     ("iallocator", None, ht.TMaybeString,
      "Iallocator for deciding the target node for shared-storage instances"),
     ]
@@ -1002,7 +1145,7 @@ class OpNodeEvacuate(OpCode):
     _PNodeName,
     ("remote_node", None, ht.TMaybeString, "New secondary node"),
     ("iallocator", None, ht.TMaybeString, "Iallocator for computing solution"),
-    ("mode", ht.NoDefault, ht.TElemOf(constants.IALLOCATOR_NEVAC_MODES),
+    ("mode", ht.NoDefault, ht.TElemOf(constants.NODE_EVAC_MODES),
      "Node evacuation mode"),
     ]
   OP_RESULT = TJobIdListOnly
@@ -1028,11 +1171,9 @@ class OpInstanceCreate(OpCode):
     _PForceVariant,
     _PWaitForSync,
     _PNameCheck,
+    _PIgnoreIpolicy,
     ("beparams", ht.EmptyDict, ht.TDict, "Backend parameters for instance"),
-    ("disks", ht.NoDefault,
-     # TODO: Generate check from constants.IDISK_PARAMS_TYPES
-     ht.TListOf(ht.TDictOf(ht.TElemOf(constants.IDISK_PARAMS),
-                           ht.TOr(ht.TNonEmptyString, ht.TInt))),
+    ("disks", ht.NoDefault, ht.TListOf(_TDiskParams),
      "Disk descriptions, for example ``[{\"%s\": 100}, {\"%s\": 5}]``;"
      " each disk definition must contain a ``%s`` value and"
      " can contain an optional ``%s`` value denoting the disk access mode"
@@ -1040,7 +1181,8 @@ class OpInstanceCreate(OpCode):
      (constants.IDISK_SIZE, constants.IDISK_SIZE, constants.IDISK_SIZE,
       constants.IDISK_MODE,
       " or ".join("``%s``" % i for i in sorted(constants.DISK_ACCESS_SET)))),
-    ("disk_template", ht.NoDefault, _CheckDiskTemplate, "Disk template"),
+    ("disk_template", ht.NoDefault, _BuildDiskTemplateCheck(True),
+     "Disk template"),
     ("file_driver", None, ht.TOr(ht.TNone, ht.TElemOf(constants.FILE_DRIVER)),
      "Driver for file-backed disks"),
     ("file_storage_dir", None, ht.TMaybeString,
@@ -1093,6 +1235,7 @@ class OpInstanceReinstall(OpCode):
     ("os_type", None, ht.TMaybeString, "Instance operating system"),
     ("osparams", None, ht.TMaybeDict, "Temporary OS parameters"),
     ]
+  OP_RESULT = ht.TNone
 
 
 class OpInstanceRemove(OpCode):
@@ -1104,6 +1247,7 @@ class OpInstanceRemove(OpCode):
     ("ignore_failures", False, ht.TBool,
      "Whether to ignore failures during removal"),
     ]
+  OP_RESULT = ht.TNone
 
 
 class OpInstanceRename(OpCode):
@@ -1130,6 +1274,7 @@ class OpInstanceStartup(OpCode):
     _PNoRemember,
     _PStartupPaused,
     ]
+  OP_RESULT = ht.TNone
 
 
 class OpInstanceShutdown(OpCode):
@@ -1142,6 +1287,7 @@ class OpInstanceShutdown(OpCode):
      "How long to wait for instance to shut down"),
     _PNoRemember,
     ]
+  OP_RESULT = ht.TNone
 
 
 class OpInstanceReboot(OpCode):
@@ -1155,6 +1301,7 @@ class OpInstanceReboot(OpCode):
     ("reboot_type", ht.NoDefault, ht.TElemOf(constants.REBOOT_TYPES),
      "How to reboot instance"),
     ]
+  OP_RESULT = ht.TNone
 
 
 class OpInstanceReplaceDisks(OpCode):
@@ -1163,6 +1310,7 @@ class OpInstanceReplaceDisks(OpCode):
   OP_PARAMS = [
     _PInstanceName,
     _PEarlyRelease,
+    _PIgnoreIpolicy,
     ("mode", ht.NoDefault, ht.TElemOf(constants.REPLACE_MODES),
      "Replacement mode"),
     ("disks", ht.EmptyList, ht.TListOf(ht.TPositiveInt),
@@ -1171,6 +1319,7 @@ class OpInstanceReplaceDisks(OpCode):
     ("iallocator", None, ht.TMaybeString,
      "Iallocator for deciding new secondary node"),
     ]
+  OP_RESULT = ht.TNone
 
 
 class OpInstanceFailover(OpCode):
@@ -1181,9 +1330,11 @@ class OpInstanceFailover(OpCode):
     _PShutdownTimeout,
     _PIgnoreConsistency,
     _PMigrationTargetNode,
+    _PIgnoreIpolicy,
     ("iallocator", None, ht.TMaybeString,
      "Iallocator for deciding the target node for shared-storage instances"),
     ]
+  OP_RESULT = ht.TNone
 
 
 class OpInstanceMigrate(OpCode):
@@ -1202,6 +1353,8 @@ class OpInstanceMigrate(OpCode):
     _PMigrationMode,
     _PMigrationLive,
     _PMigrationTargetNode,
+    _PAllowRuntimeChgs,
+    _PIgnoreIpolicy,
     ("cleanup", False, ht.TBool,
      "Whether a previously failed migration should be cleaned up"),
     ("iallocator", None, ht.TMaybeString,
@@ -1209,6 +1362,7 @@ class OpInstanceMigrate(OpCode):
     ("allow_failover", False, ht.TBool,
      "Whether we can fallback to failover if migration is not possible"),
     ]
+  OP_RESULT = ht.TNone
 
 
 class OpInstanceMove(OpCode):
@@ -1225,9 +1379,11 @@ class OpInstanceMove(OpCode):
   OP_PARAMS = [
     _PInstanceName,
     _PShutdownTimeout,
+    _PIgnoreIpolicy,
     ("target_node", ht.NoDefault, ht.TNonEmptyString, "Target node"),
     _PIgnoreConsistency,
     ]
+  OP_RESULT = ht.TNone
 
 
 class OpInstanceConsole(OpCode):
@@ -1236,6 +1392,7 @@ class OpInstanceConsole(OpCode):
   OP_PARAMS = [
     _PInstanceName
     ]
+  OP_RESULT = ht.TDict
 
 
 class OpInstanceActivateDisks(OpCode):
@@ -1245,6 +1402,10 @@ class OpInstanceActivateDisks(OpCode):
     _PInstanceName,
     ("ignore_size", False, ht.TBool, "Whether to ignore recorded size"),
     ]
+  OP_RESULT = ht.TListOf(ht.TAnd(ht.TIsLength(3),
+                                 ht.TItems([ht.TNonEmptyString,
+                                            ht.TNonEmptyString,
+                                            ht.TNonEmptyString])))
 
 
 class OpInstanceDeactivateDisks(OpCode):
@@ -1254,18 +1415,27 @@ class OpInstanceDeactivateDisks(OpCode):
     _PInstanceName,
     _PForce,
     ]
+  OP_RESULT = ht.TNone
 
 
 class OpInstanceRecreateDisks(OpCode):
-  """Deactivate an instance's disks."""
+  """Recreate an instance's disks."""
+  _TDiskChanges = \
+    ht.TAnd(ht.TIsLength(2),
+            ht.TItems([ht.Comment("Disk index")(ht.TPositiveInt),
+                       ht.Comment("Parameters")(_TDiskParams)]))
+
   OP_DSC_FIELD = "instance_name"
   OP_PARAMS = [
     _PInstanceName,
-    ("disks", ht.EmptyList, ht.TListOf(ht.TPositiveInt),
-     "List of disk indexes"),
+    ("disks", ht.EmptyList,
+     ht.TOr(ht.TListOf(ht.TPositiveInt), ht.TListOf(_TDiskChanges)),
+     "List of disk indexes (deprecated) or a list of tuples containing a disk"
+     " index and a possibly empty dictionary with disk parameter changes"),
     ("nodes", ht.EmptyList, ht.TListOf(ht.TNonEmptyString),
      "New instance nodes, if relocation is desired"),
     ]
+  OP_RESULT = ht.TNone
 
 
 class OpInstanceQuery(OpCode):
@@ -1276,6 +1446,7 @@ class OpInstanceQuery(OpCode):
     ("names", ht.EmptyList, ht.TListOf(ht.TNonEmptyString),
      "Empty list to query all instances, instance names otherwise"),
     ]
+  OP_RESULT = _TOldQueryResult
 
 
 class OpInstanceQueryData(OpCode):
@@ -1288,35 +1459,72 @@ class OpInstanceQueryData(OpCode):
      "Whether to only return configuration data without querying"
      " nodes"),
     ]
+  OP_RESULT = ht.TDictOf(ht.TNonEmptyString, ht.TDict)
+
+
+def _TestInstSetParamsModList(fn):
+  """Generates a check for modification lists.
+
+  """
+  # Old format
+  # TODO: Remove in version 2.8 including support in LUInstanceSetParams
+  old_mod_item_fn = \
+    ht.TAnd(ht.TIsLength(2), ht.TItems([
+      ht.TOr(ht.TElemOf(constants.DDMS_VALUES), ht.TPositiveInt),
+      fn,
+      ]))
+
+  # New format, supporting adding/removing disks/NICs at arbitrary indices
+  mod_item_fn = \
+    ht.TAnd(ht.TIsLength(3), ht.TItems([
+      ht.TElemOf(constants.DDMS_VALUES_WITH_MODIFY),
+      ht.Comment("Disk index, can be negative, e.g. -1 for last disk")(ht.TInt),
+      fn,
+      ]))
+
+  return ht.TOr(ht.Comment("Recommended")(ht.TListOf(mod_item_fn)),
+                ht.Comment("Deprecated")(ht.TListOf(old_mod_item_fn)))
 
 
 class OpInstanceSetParams(OpCode):
-  """Change the parameters of an instance."""
+  """Change the parameters of an instance.
+
+  """
+  TestNicModifications = _TestInstSetParamsModList(_TestNicDef)
+  TestDiskModifications = _TestInstSetParamsModList(_TDiskParams)
+
   OP_DSC_FIELD = "instance_name"
   OP_PARAMS = [
     _PInstanceName,
     _PForce,
     _PForceVariant,
-    # TODO: Use _TestNicDef
-    ("nics", ht.EmptyList, ht.TList,
-     "List of NIC changes. Each item is of the form ``(op, settings)``."
-     " ``op`` can be ``%s`` to add a new NIC with the specified settings,"
-     " ``%s`` to remove the last NIC or a number to modify the settings"
-     " of the NIC with that index." %
-     (constants.DDM_ADD, constants.DDM_REMOVE)),
-    ("disks", ht.EmptyList, ht.TList, "List of disk changes. See ``nics``."),
+    _PIgnoreIpolicy,
+    ("nics", ht.EmptyList, TestNicModifications,
+     "List of NIC changes: each item is of the form ``(op, index, settings)``,"
+     " ``op`` is one of ``%s``, ``%s`` or ``%s``, ``index`` can be either -1"
+     " to refer to the last position, or a zero-based index number; a"
+     " deprecated version of this parameter used the form ``(op, settings)``,"
+     " where ``op`` can be ``%s`` to add a new NIC with the specified"
+     " settings, ``%s`` to remove the last NIC or a number to modify the"
+     " settings of the NIC with that index" %
+     (constants.DDM_ADD, constants.DDM_MODIFY, constants.DDM_REMOVE,
+      constants.DDM_ADD, constants.DDM_REMOVE)),
+    ("disks", ht.EmptyList, TestDiskModifications,
+     "List of disk changes; see ``nics``"),
     ("beparams", ht.EmptyDict, ht.TDict, "Per-instance backend parameters"),
+    ("runtime_mem", None, ht.TMaybeStrictPositiveInt, "New runtime memory"),
     ("hvparams", ht.EmptyDict, ht.TDict,
      "Per-instance hypervisor parameters, hypervisor-dependent"),
-    ("disk_template", None, ht.TOr(ht.TNone, _CheckDiskTemplate),
+    ("disk_template", None, ht.TOr(ht.TNone, _BuildDiskTemplateCheck(False)),
      "Disk template for instance"),
     ("remote_node", None, ht.TMaybeString,
      "Secondary node (used when changing disk template)"),
     ("os_name", None, ht.TMaybeString,
-     "Change instance's OS name. Does not reinstall the instance."),
+     "Change the instance's OS without reinstalling the instance"),
     ("osparams", None, ht.TMaybeDict, "Per-instance OS parameters"),
     ("wait_for_sync", True, ht.TBool,
      "Whether to wait for the disk to synchronize, when changing template"),
+    ("offline", None, ht.TMaybeBool, "Whether to mark instance as offline"),
     ]
   OP_RESULT = _TSetParamsResult
 
@@ -1328,9 +1536,12 @@ class OpInstanceGrowDisk(OpCode):
     _PInstanceName,
     _PWaitForSync,
     ("disk", ht.NoDefault, ht.TInt, "Disk index"),
-    ("amount", ht.NoDefault, ht.TInt,
+    ("amount", ht.NoDefault, ht.TPositiveInt,
      "Amount of disk space to add (megabytes)"),
+    ("absolute", False, ht.TBool,
+     "Whether the amount parameter is an absolute target or a relative one"),
     ]
+  OP_RESULT = ht.TNone
 
 
 class OpInstanceChangeGroup(OpCode):
@@ -1340,7 +1551,7 @@ class OpInstanceChangeGroup(OpCode):
     _PInstanceName,
     _PEarlyRelease,
     ("iallocator", None, ht.TMaybeString, "Iallocator for computing solution"),
-    ("target_groups", None, ht.TOr(ht.TNone, ht.TListOf(ht.TNonEmptyString)),
+    ("target_groups", None, ht.TMaybeListOf(ht.TNonEmptyString),
      "Destination group names or UUIDs (defaults to \"all but current group\""),
     ]
   OP_RESULT = TJobIdListOnly
@@ -1355,7 +1566,13 @@ class OpGroupAdd(OpCode):
     _PGroupName,
     _PNodeGroupAllocPolicy,
     _PGroupNodeParams,
+    _PDiskParams,
+    _PHvState,
+    _PDiskState,
+    ("ipolicy", None, ht.TMaybeDict,
+     "Group-wide :ref:`instance policy <rapi-ipolicy>` specs"),
     ]
+  OP_RESULT = ht.TNone
 
 
 class OpGroupAssignNodes(OpCode):
@@ -1367,6 +1584,7 @@ class OpGroupAssignNodes(OpCode):
     ("nodes", ht.NoDefault, ht.TListOf(ht.TNonEmptyString),
      "List of nodes to assign"),
     ]
+  OP_RESULT = ht.TNone
 
 
 class OpGroupQuery(OpCode):
@@ -1376,6 +1594,7 @@ class OpGroupQuery(OpCode):
     ("names", ht.EmptyList, ht.TListOf(ht.TNonEmptyString),
      "Empty list to query all groups, group names otherwise"),
     ]
+  OP_RESULT = _TOldQueryResult
 
 
 class OpGroupSetParams(OpCode):
@@ -1385,6 +1604,10 @@ class OpGroupSetParams(OpCode):
     _PGroupName,
     _PNodeGroupAllocPolicy,
     _PGroupNodeParams,
+    _PDiskParams,
+    _PHvState,
+    _PDiskState,
+    ("ipolicy", None, ht.TMaybeDict, "Group-wide instance policy specs"),
     ]
   OP_RESULT = _TSetParamsResult
 
@@ -1395,6 +1618,7 @@ class OpGroupRemove(OpCode):
   OP_PARAMS = [
     _PGroupName,
     ]
+  OP_RESULT = ht.TNone
 
 
 class OpGroupRename(OpCode):
@@ -1413,7 +1637,7 @@ class OpGroupEvacuate(OpCode):
     _PGroupName,
     _PEarlyRelease,
     ("iallocator", None, ht.TMaybeString, "Iallocator for computing solution"),
-    ("target_groups", None, ht.TOr(ht.TNone, ht.TListOf(ht.TNonEmptyString)),
+    ("target_groups", None, ht.TMaybeListOf(ht.TNonEmptyString),
      "Destination group names or UUIDs"),
     ]
   OP_RESULT = TJobIdListOnly
@@ -1427,6 +1651,7 @@ class OpOsDiagnose(OpCode):
     ("names", ht.EmptyList, ht.TListOf(ht.TNonEmptyString),
      "Which operating systems to diagnose"),
     ]
+  OP_RESULT = _TOldQueryResult
 
 
 # Exports opcodes
@@ -1437,6 +1662,9 @@ class OpBackupQuery(OpCode):
     ("nodes", ht.EmptyList, ht.TListOf(ht.TNonEmptyString),
      "Empty list to query all nodes, node names otherwise"),
     ]
+  OP_RESULT = ht.TDictOf(ht.TNonEmptyString,
+                         ht.TOr(ht.Comment("False on error")(ht.TBool),
+                                ht.TListOf(ht.TNonEmptyString)))
 
 
 class OpBackupPrepare(OpCode):
@@ -1452,6 +1680,7 @@ class OpBackupPrepare(OpCode):
     ("mode", ht.NoDefault, ht.TElemOf(constants.EXPORT_MODES),
      "Export mode"),
     ]
+  OP_RESULT = ht.TOr(ht.TNone, ht.TDict)
 
 
 class OpBackupExport(OpCode):
@@ -1490,6 +1719,11 @@ class OpBackupExport(OpCode):
     ("destination_x509_ca", None, ht.TMaybeString,
      "Destination X509 CA (remote export only)"),
     ]
+  OP_RESULT = \
+    ht.TAnd(ht.TIsLength(2), ht.TItems([
+      ht.Comment("Finalizing status")(ht.TBool),
+      ht.Comment("Status for every exported disk")(ht.TListOf(ht.TBool)),
+      ]))
 
 
 class OpBackupRemove(OpCode):
@@ -1498,6 +1732,7 @@ class OpBackupRemove(OpCode):
   OP_PARAMS = [
     _PInstanceName,
     ]
+  OP_RESULT = ht.TNone
 
 
 # Tags opcodes
@@ -1506,17 +1741,26 @@ class OpTagsGet(OpCode):
   OP_DSC_FIELD = "name"
   OP_PARAMS = [
     _PTagKind,
+    # Not using _PUseLocking as the default is different for historical reasons
+    ("use_locking", True, ht.TBool, "Whether to use synchronization"),
     # Name is only meaningful for nodes and instances
-    ("name", ht.NoDefault, ht.TMaybeString, None),
+    ("name", ht.NoDefault, ht.TMaybeString,
+     "Name of object to retrieve tags from"),
     ]
+  OP_RESULT = ht.TListOf(ht.TNonEmptyString)
 
 
 class OpTagsSearch(OpCode):
   """Searches the tags in the cluster for a given pattern."""
   OP_DSC_FIELD = "pattern"
   OP_PARAMS = [
-    ("pattern", ht.NoDefault, ht.TNonEmptyString, None),
+    ("pattern", ht.NoDefault, ht.TNonEmptyString,
+     "Search pattern (regular expression)"),
     ]
+  OP_RESULT = ht.TListOf(ht.TAnd(ht.TIsLength(2), ht.TItems([
+    ht.TNonEmptyString,
+    ht.TNonEmptyString,
+    ])))
 
 
 class OpTagsSet(OpCode):
@@ -1525,8 +1769,10 @@ class OpTagsSet(OpCode):
     _PTagKind,
     _PTags,
     # Name is only meaningful for nodes and instances
-    ("name", ht.NoDefault, ht.TMaybeString, None),
+    ("name", ht.NoDefault, ht.TMaybeString,
+     "Name of object where tag(s) should be added"),
     ]
+  OP_RESULT = ht.TNone
 
 
 class OpTagsDel(OpCode):
@@ -1535,8 +1781,10 @@ class OpTagsDel(OpCode):
     _PTagKind,
     _PTags,
     # Name is only meaningful for nodes and instances
-    ("name", ht.NoDefault, ht.TMaybeString, None),
+    ("name", ht.NoDefault, ht.TMaybeString,
+     "Name of object where tag(s) should be deleted"),
     ]
+  OP_RESULT = ht.TNone
 
 
 # Test opcodes
@@ -1587,9 +1835,12 @@ class OpTestAllocator(OpCode):
      ht.TElemOf(constants.VALID_IALLOCATOR_DIRECTIONS), None),
     ("mode", ht.NoDefault, ht.TElemOf(constants.VALID_IALLOCATOR_MODES), None),
     ("name", ht.NoDefault, ht.TNonEmptyString, None),
-    ("nics", ht.NoDefault, ht.TOr(ht.TNone, ht.TListOf(
-     ht.TDictOf(ht.TElemOf([constants.INIC_MAC, constants.INIC_IP, "bridge"]),
-                ht.TOr(ht.TNone, ht.TNonEmptyString)))), None),
+    ("nics", ht.NoDefault,
+     ht.TMaybeListOf(ht.TDictOf(ht.TElemOf([constants.INIC_MAC,
+                                            constants.INIC_IP,
+                                            "bridge"]),
+                                ht.TOr(ht.TNone, ht.TNonEmptyString))),
+     None),
     ("disks", ht.NoDefault, ht.TOr(ht.TNone, ht.TList), None),
     ("hypervisor", None, ht.TMaybeString, None),
     ("allocator", None, ht.TMaybeString, None),
@@ -1598,12 +1849,10 @@ class OpTestAllocator(OpCode):
     ("vcpus", None, ht.TOr(ht.TNone, ht.TPositiveInt), None),
     ("os", None, ht.TMaybeString, None),
     ("disk_template", None, ht.TMaybeString, None),
-    ("instances", None, ht.TOr(ht.TNone, ht.TListOf(ht.TNonEmptyString)),
-     None),
+    ("instances", None, ht.TMaybeListOf(ht.TNonEmptyString), None),
     ("evac_mode", None,
      ht.TOr(ht.TNone, ht.TElemOf(constants.IALLOCATOR_NEVAC_MODES)), None),
-    ("target_groups", None, ht.TOr(ht.TNone, ht.TListOf(ht.TNonEmptyString)),
-     None),
+    ("target_groups", None, ht.TMaybeListOf(ht.TNonEmptyString), None),
     ]
 
 
diff --git a/lib/ovf.py b/lib/ovf.py
new file mode 100644 (file)
index 0000000..6ce13e1
--- /dev/null
@@ -0,0 +1,1814 @@
+#!/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.
+
+
+"""Converter tools between ovf and ganeti config file
+
+"""
+
+# pylint: disable=F0401, E1101
+
+# F0401 because ElementTree is not default for python 2.4
+# E1101 makes no sense - pylint assumes that ElementTree object is a tuple
+
+
+import ConfigParser
+import errno
+import logging
+import os
+import os.path
+import re
+import shutil
+import tarfile
+import tempfile
+import xml.dom.minidom
+import xml.parsers.expat
+try:
+  import xml.etree.ElementTree as ET
+except ImportError:
+  import elementtree.ElementTree as ET
+
+try:
+  ParseError = ET.ParseError # pylint: disable=E1103
+except AttributeError:
+  ParseError = None
+
+from ganeti import constants
+from ganeti import errors
+from ganeti import utils
+
+
+# Schemas used in OVF format
+GANETI_SCHEMA = "http://ganeti"
+OVF_SCHEMA = "http://schemas.dmtf.org/ovf/envelope/1"
+RASD_SCHEMA = ("http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/"
+               "CIM_ResourceAllocationSettingData")
+VSSD_SCHEMA = ("http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/"
+               "CIM_VirtualSystemSettingData")
+XML_SCHEMA = "http://www.w3.org/2001/XMLSchema-instance"
+
+# File extensions in OVF package
+OVA_EXT = ".ova"
+OVF_EXT = ".ovf"
+MF_EXT = ".mf"
+CERT_EXT = ".cert"
+COMPRESSION_EXT = ".gz"
+FILE_EXTENSIONS = [
+  OVF_EXT,
+  MF_EXT,
+  CERT_EXT,
+]
+
+COMPRESSION_TYPE = "gzip"
+NO_COMPRESSION = [None, "identity"]
+COMPRESS = "compression"
+DECOMPRESS = "decompression"
+ALLOWED_ACTIONS = [COMPRESS, DECOMPRESS]
+
+VMDK = "vmdk"
+RAW = "raw"
+COW = "cow"
+ALLOWED_FORMATS = [RAW, COW, VMDK]
+
+# ResourceType values
+RASD_TYPE = {
+  "vcpus": "3",
+  "memory": "4",
+  "scsi-controller": "6",
+  "ethernet-adapter": "10",
+  "disk": "17",
+}
+
+SCSI_SUBTYPE = "lsilogic"
+VS_TYPE = {
+  "ganeti": "ganeti-ovf",
+  "external": "vmx-04",
+}
+
+# AllocationUnits values and conversion
+ALLOCATION_UNITS = {
+  "b": ["bytes", "b"],
+  "kb": ["kilobytes", "kb", "byte * 2^10", "kibibytes", "kib"],
+  "mb": ["megabytes", "mb", "byte * 2^20", "mebibytes", "mib"],
+  "gb": ["gigabytes", "gb", "byte * 2^30", "gibibytes", "gib"],
+}
+CONVERT_UNITS_TO_MB = {
+  "b": lambda x: x / (1024 * 1024),
+  "kb": lambda x: x / 1024,
+  "mb": lambda x: x,
+  "gb": lambda x: x * 1024,
+}
+
+# Names of the config fields
+NAME = "name"
+OS = "os"
+HYPERV = "hypervisor"
+VCPUS = "vcpus"
+MEMORY = "memory"
+AUTO_BALANCE = "auto_balance"
+DISK_TEMPLATE = "disk_template"
+TAGS = "tags"
+VERSION = "version"
+
+# Instance IDs of System and SCSI controller
+INSTANCE_ID = {
+  "system": 0,
+  "vcpus": 1,
+  "memory": 2,
+  "scsi": 3,
+}
+
+# Disk format descriptions
+DISK_FORMAT = {
+  RAW: "http://en.wikipedia.org/wiki/Byte",
+  VMDK: "http://www.vmware.com/interfaces/specifications/vmdk.html"
+          "#monolithicSparse",
+  COW: "http://www.gnome.org/~markmc/qcow-image-format.html",
+}
+
+
+def CheckQemuImg():
+  """ Make sure that qemu-img is present before performing operations.
+
+  @raise errors.OpPrereqError: when qemu-img was not found in the system
+
+  """
+  if not constants.QEMUIMG_PATH:
+    raise errors.OpPrereqError("qemu-img not found at build time, unable"
+                               " to continue")
+
+
+def LinkFile(old_path, prefix=None, suffix=None, directory=None):
+  """Create link with a given prefix and suffix.
+
+  This is a wrapper over os.link. It tries to create a hard link for given file,
+  but instead of rising error when file exists, the function changes the name
+  a little bit.
+
+  @type old_path:string
+  @param old_path: path to the file that is to be linked
+  @type prefix: string
+  @param prefix: prefix of filename for the link
+  @type suffix: string
+  @param suffix: suffix of the filename for the link
+  @type directory: string
+  @param directory: directory of the link
+
+  @raise errors.OpPrereqError: when error on linking is different than
+    "File exists"
+
+  """
+  assert(prefix is not None or suffix is not None)
+  if directory is None:
+    directory = os.getcwd()
+  new_path = utils.PathJoin(directory, "%s%s" % (prefix, suffix))
+  counter = 1
+  while True:
+    try:
+      os.link(old_path, new_path)
+      break
+    except OSError, err:
+      if err.errno == errno.EEXIST:
+        new_path = utils.PathJoin(directory,
+          "%s_%s%s" % (prefix, counter, suffix))
+        counter += 1
+      else:
+        raise errors.OpPrereqError("Error moving the file %s to %s location:"
+                                   " %s" % (old_path, new_path, err))
+  return new_path
+
+
+class OVFReader(object):
+  """Reader class for OVF files.
+
+  @type files_list: list
+  @ivar files_list: list of files in the OVF package
+  @type tree: ET.ElementTree
+  @ivar tree: XML tree of the .ovf file
+  @type schema_name: string
+  @ivar schema_name: name of the .ovf file
+  @type input_dir: string
+  @ivar input_dir: directory in which the .ovf file resides
+
+  """
+  def __init__(self, input_path):
+    """Initialiaze the reader - load the .ovf file to XML parser.
+
+    It is assumed that names of manifesto (.mf), certificate (.cert) and ovf
+    files are the same. In order to account any other files as part of the ovf
+    package, they have to be explicitly mentioned in the Resources section
+    of the .ovf file.
+
+    @type input_path: string
+    @param input_path: absolute path to the .ovf file
+
+    @raise errors.OpPrereqError: when .ovf file is not a proper XML file or some
+      of the files mentioned in Resources section do not exist
+
+    """
+    self.tree = ET.ElementTree()
+    try:
+      self.tree.parse(input_path)
+    except (ParseError, xml.parsers.expat.ExpatError), err:
+      raise errors.OpPrereqError("Error while reading %s file: %s" %
+                                 (OVF_EXT, err))
+
+    # Create a list of all files in the OVF package
+    (input_dir, input_file) = os.path.split(input_path)
+    (input_name, _) = os.path.splitext(input_file)
+    files_directory = utils.ListVisibleFiles(input_dir)
+    files_list = []
+    for file_name in files_directory:
+      (name, extension) = os.path.splitext(file_name)
+      if extension in FILE_EXTENSIONS and name == input_name:
+        files_list.append(file_name)
+    files_list += self._GetAttributes("{%s}References/{%s}File" %
+                                      (OVF_SCHEMA, OVF_SCHEMA),
+                                      "{%s}href" % OVF_SCHEMA)
+    for file_name in files_list:
+      file_path = utils.PathJoin(input_dir, file_name)
+      if not os.path.exists(file_path):
+        raise errors.OpPrereqError("File does not exist: %s" % file_path)
+    logging.info("Files in the OVF package: %s", " ".join(files_list))
+    self.files_list = files_list
+    self.input_dir = input_dir
+    self.schema_name = input_name
+
+  def _GetAttributes(self, path, attribute):
+    """Get specified attribute from all nodes accessible using given path.
+
+    Function follows the path from root node to the desired tags using path,
+    then reads the apropriate attribute values.
+
+    @type path: string
+    @param path: path of nodes to visit
+    @type attribute: string
+    @param attribute: attribute for which we gather the information
+    @rtype: list
+    @return: for each accessible tag with the attribute value set, value of the
+      attribute
+
+    """
+    current_list = self.tree.findall(path)
+    results = [x.get(attribute) for x in current_list]
+    return filter(None, results)
+
+  def _GetElementMatchingAttr(self, path, match_attr):
+    """Searches for element on a path that matches certain attribute value.
+
+    Function follows the path from root node to the desired tags using path,
+    then searches for the first one matching the attribute value.
+
+    @type path: string
+    @param path: path of nodes to visit
+    @type match_attr: tuple
+    @param match_attr: pair (attribute, value) for which we search
+    @rtype: ET.ElementTree or None
+    @return: first element matching match_attr or None if nothing matches
+
+    """
+    potential_elements = self.tree.findall(path)
+    (attr, val) = match_attr
+    for elem in potential_elements:
+      if elem.get(attr) == val:
+        return elem
+    return None
+
+  def _GetElementMatchingText(self, path, match_text):
+    """Searches for element on a path that matches certain text value.
+
+    Function follows the path from root node to the desired tags using path,
+    then searches for the first one matching the text value.
+
+    @type path: string
+    @param path: path of nodes to visit
+    @type match_text: tuple
+    @param match_text: pair (node, text) for which we search
+    @rtype: ET.ElementTree or None
+    @return: first element matching match_text or None if nothing matches
+
+    """
+    potential_elements = self.tree.findall(path)
+    (node, text) = match_text
+    for elem in potential_elements:
+      if elem.findtext(node) == text:
+        return elem
+    return None
+
+  @staticmethod
+  def _GetDictParameters(root, schema):
+    """Reads text in all children and creates the dictionary from the contents.
+
+    @type root: ET.ElementTree or None
+    @param root: father of the nodes we want to collect data about
+    @type schema: string
+    @param schema: schema name to be removed from the tag
+    @rtype: dict
+    @return: dictionary containing tags and their text contents, tags have their
+      schema fragment removed or empty dictionary, when root is None
+
+    """
+    if not root:
+      return {}
+    results = {}
+    for element in list(root):
+      pref_len = len("{%s}" % schema)
+      assert(schema in element.tag)
+      tag = element.tag[pref_len:]
+      results[tag] = element.text
+    return results
+
+  def VerifyManifest(self):
+    """Verifies manifest for the OVF package, if one is given.
+
+    @raise errors.OpPrereqError: if SHA1 checksums do not match
+
+    """
+    if "%s%s" % (self.schema_name, MF_EXT) in self.files_list:
+      logging.warning("Verifying SHA1 checksums, this may take a while")
+      manifest_filename = "%s%s" % (self.schema_name, MF_EXT)
+      manifest_path = utils.PathJoin(self.input_dir, manifest_filename)
+      manifest_content = utils.ReadFile(manifest_path).splitlines()
+      manifest_files = {}
+      regexp = r"SHA1\((\S+)\)= (\S+)"
+      for line in manifest_content:
+        match = re.match(regexp, line)
+        if match:
+          file_name = match.group(1)
+          sha1_sum = match.group(2)
+          manifest_files[file_name] = sha1_sum
+      files_with_paths = [utils.PathJoin(self.input_dir, file_name)
+        for file_name in self.files_list]
+      sha1_sums = utils.FingerprintFiles(files_with_paths)
+      for file_name, value in manifest_files.iteritems():
+        if sha1_sums.get(utils.PathJoin(self.input_dir, file_name)) != value:
+          raise errors.OpPrereqError("SHA1 checksum of %s does not match the"
+                                     " value in manifest file" % file_name)
+      logging.info("SHA1 checksums verified")
+
+  def GetInstanceName(self):
+    """Provides information about instance name.
+
+    @rtype: string
+    @return: instance name string
+
+    """
+    find_name = "{%s}VirtualSystem/{%s}Name" % (OVF_SCHEMA, OVF_SCHEMA)
+    return self.tree.findtext(find_name)
+
+  def GetDiskTemplate(self):
+    """Returns disk template from .ovf file
+
+    @rtype: string or None
+    @return: name of the template
+    """
+    find_template = ("{%s}GanetiSection/{%s}DiskTemplate" %
+                     (GANETI_SCHEMA, GANETI_SCHEMA))
+    return self.tree.findtext(find_template)
+
+  def GetHypervisorData(self):
+    """Provides hypervisor information - hypervisor name and options.
+
+    @rtype: dict
+    @return: dictionary containing name of the used hypervisor and all the
+      specified options
+
+    """
+    hypervisor_search = ("{%s}GanetiSection/{%s}Hypervisor" %
+                         (GANETI_SCHEMA, GANETI_SCHEMA))
+    hypervisor_data = self.tree.find(hypervisor_search)
+    if not hypervisor_data:
+      return {"hypervisor_name": constants.VALUE_AUTO}
+    results = {
+      "hypervisor_name": hypervisor_data.findtext("{%s}Name" % GANETI_SCHEMA,
+                           default=constants.VALUE_AUTO),
+    }
+    parameters = hypervisor_data.find("{%s}Parameters" % GANETI_SCHEMA)
+    results.update(self._GetDictParameters(parameters, GANETI_SCHEMA))
+    return results
+
+  def GetOSData(self):
+    """ Provides operating system information - os name and options.
+
+    @rtype: dict
+    @return: dictionary containing name and options for the chosen OS
+
+    """
+    results = {}
+    os_search = ("{%s}GanetiSection/{%s}OperatingSystem" %
+                 (GANETI_SCHEMA, GANETI_SCHEMA))
+    os_data = self.tree.find(os_search)
+    if os_data:
+      results["os_name"] = os_data.findtext("{%s}Name" % GANETI_SCHEMA)
+      parameters = os_data.find("{%s}Parameters" % GANETI_SCHEMA)
+      results.update(self._GetDictParameters(parameters, GANETI_SCHEMA))
+    return results
+
+  def GetBackendData(self):
+    """ Provides backend information - vcpus, memory, auto balancing options.
+
+    @rtype: dict
+    @return: dictionary containing options for vcpus, memory and auto balance
+      settings
+
+    """
+    results = {}
+
+    find_vcpus = ("{%s}VirtualSystem/{%s}VirtualHardwareSection/{%s}Item" %
+                   (OVF_SCHEMA, OVF_SCHEMA, OVF_SCHEMA))
+    match_vcpus = ("{%s}ResourceType" % RASD_SCHEMA, RASD_TYPE["vcpus"])
+    vcpus = self._GetElementMatchingText(find_vcpus, match_vcpus)
+    if vcpus:
+      vcpus_count = vcpus.findtext("{%s}VirtualQuantity" % RASD_SCHEMA,
+        default=constants.VALUE_AUTO)
+    else:
+      vcpus_count = constants.VALUE_AUTO
+    results["vcpus"] = str(vcpus_count)
+
+    find_memory = find_vcpus
+    match_memory = ("{%s}ResourceType" % RASD_SCHEMA, RASD_TYPE["memory"])
+    memory = self._GetElementMatchingText(find_memory, match_memory)
+    memory_raw = None
+    if memory:
+      alloc_units = memory.findtext("{%s}AllocationUnits" % RASD_SCHEMA)
+      matching_units = [units for units, variants in
+        ALLOCATION_UNITS.iteritems() if alloc_units.lower() in variants]
+      if matching_units == []:
+        raise errors.OpPrereqError("Unit %s for RAM memory unknown",
+          alloc_units)
+      units = matching_units[0]
+      memory_raw = int(memory.findtext("{%s}VirtualQuantity" % RASD_SCHEMA,
+            default=constants.VALUE_AUTO))
+      memory_count = CONVERT_UNITS_TO_MB[units](memory_raw)
+    else:
+      memory_count = constants.VALUE_AUTO
+    results["memory"] = str(memory_count)
+
+    find_balance = ("{%s}GanetiSection/{%s}AutoBalance" %
+                   (GANETI_SCHEMA, GANETI_SCHEMA))
+    balance = self.tree.findtext(find_balance, default=constants.VALUE_AUTO)
+    results["auto_balance"] = balance
+
+    return results
+
+  def GetTagsData(self):
+    """Provides tags information for instance.
+
+    @rtype: string or None
+    @return: string of comma-separated tags for the instance
+
+    """
+    find_tags = "{%s}GanetiSection/{%s}Tags" % (GANETI_SCHEMA, GANETI_SCHEMA)
+    results = self.tree.findtext(find_tags)
+    if results:
+      return results
+    else:
+      return None
+
+  def GetVersionData(self):
+    """Provides version number read from .ovf file
+
+    @rtype: string
+    @return: string containing the version number
+
+    """
+    find_version = ("{%s}GanetiSection/{%s}Version" %
+                    (GANETI_SCHEMA, GANETI_SCHEMA))
+    return self.tree.findtext(find_version)
+
+  def GetNetworkData(self):
+    """Provides data about the network in the OVF instance.
+
+    The method gathers the data about networks used by OVF instance. It assumes
+    that 'name' tag means something - in essence, if it contains one of the
+    words 'bridged' or 'routed' then that will be the mode of this network in
+    Ganeti. The information about the network can be either in GanetiSection or
+    VirtualHardwareSection.
+
+    @rtype: dict
+    @return: dictionary containing all the network information
+
+    """
+    results = {}
+    networks_search = ("{%s}NetworkSection/{%s}Network" %
+                       (OVF_SCHEMA, OVF_SCHEMA))
+    network_names = self._GetAttributes(networks_search,
+      "{%s}name" % OVF_SCHEMA)
+    required = ["ip", "mac", "link", "mode"]
+    for (counter, network_name) in enumerate(network_names):
+      network_search = ("{%s}VirtualSystem/{%s}VirtualHardwareSection/{%s}Item"
+                        % (OVF_SCHEMA, OVF_SCHEMA, OVF_SCHEMA))
+      ganeti_search = ("{%s}GanetiSection/{%s}Network/{%s}Nic" %
+                       (GANETI_SCHEMA, GANETI_SCHEMA, GANETI_SCHEMA))
+      network_match = ("{%s}Connection" % RASD_SCHEMA, network_name)
+      ganeti_match = ("{%s}name" % OVF_SCHEMA, network_name)
+      network_data = self._GetElementMatchingText(network_search, network_match)
+      network_ganeti_data = self._GetElementMatchingAttr(ganeti_search,
+        ganeti_match)
+
+      ganeti_data = {}
+      if network_ganeti_data:
+        ganeti_data["mode"] = network_ganeti_data.findtext("{%s}Mode" %
+                                                           GANETI_SCHEMA)
+        ganeti_data["mac"] = network_ganeti_data.findtext("{%s}MACAddress" %
+                                                          GANETI_SCHEMA)
+        ganeti_data["ip"] = network_ganeti_data.findtext("{%s}IPAddress" %
+                                                         GANETI_SCHEMA)
+        ganeti_data["link"] = network_ganeti_data.findtext("{%s}Link" %
+                                                           GANETI_SCHEMA)
+      mac_data = None
+      if network_data:
+        mac_data = network_data.findtext("{%s}Address" % RASD_SCHEMA)
+
+      network_name = network_name.lower()
+
+      # First, some not Ganeti-specific information is collected
+      if constants.NIC_MODE_BRIDGED in network_name:
+        results["nic%s_mode" % counter] = "bridged"
+      elif constants.NIC_MODE_ROUTED in network_name:
+        results["nic%s_mode" % counter] = "routed"
+      results["nic%s_mac" % counter] = mac_data
+
+      # GanetiSection data overrides 'manually' collected data
+      for name, value in ganeti_data.iteritems():
+        results["nic%s_%s" % (counter, name)] = value
+
+      # Bridged network has no IP - unless specifically stated otherwise
+      if (results.get("nic%s_mode" % counter) == "bridged" and
+          not results.get("nic%s_ip" % counter)):
+        results["nic%s_ip" % counter] = constants.VALUE_NONE
+
+      for option in required:
+        if not results.get("nic%s_%s" % (counter, option)):
+          results["nic%s_%s" % (counter, option)] = constants.VALUE_AUTO
+
+    if network_names:
+      results["nic_count"] = str(len(network_names))
+    return results
+
+  def GetDisksNames(self):
+    """Provides list of file names for the disks used by the instance.
+
+    @rtype: list
+    @return: list of file names, as referenced in .ovf file
+
+    """
+    results = []
+    disks_search = "{%s}DiskSection/{%s}Disk" % (OVF_SCHEMA, OVF_SCHEMA)
+    disk_ids = self._GetAttributes(disks_search, "{%s}fileRef" % OVF_SCHEMA)
+    for disk in disk_ids:
+      disk_search = "{%s}References/{%s}File" % (OVF_SCHEMA, OVF_SCHEMA)
+      disk_match = ("{%s}id" % OVF_SCHEMA, disk)
+      disk_elem = self._GetElementMatchingAttr(disk_search, disk_match)
+      if disk_elem is None:
+        raise errors.OpPrereqError("%s file corrupted - disk %s not found in"
+                                   " references" % (OVF_EXT, disk))
+      disk_name = disk_elem.get("{%s}href" % OVF_SCHEMA)
+      disk_compression = disk_elem.get("{%s}compression" % OVF_SCHEMA)
+      results.append((disk_name, disk_compression))
+    return results
+
+
+def SubElementText(parent, tag, text, attrib={}, **extra):
+# pylint: disable=W0102
+  """This is just a wrapper on ET.SubElement that always has text content.
+
+  """
+  if text is None:
+    return None
+  elem = ET.SubElement(parent, tag, attrib=attrib, **extra)
+  elem.text = str(text)
+  return elem
+
+
+class OVFWriter(object):
+  """Writer class for OVF files.
+
+  @type tree: ET.ElementTree
+  @ivar tree: XML tree that we are constructing
+  @type virtual_system_type: string
+  @ivar virtual_system_type: value of vssd:VirtualSystemType, for external usage
+    in VMWare this requires to be vmx
+  @type hardware_list: list
+  @ivar hardware_list: list of items prepared for VirtualHardwareSection
+  @type next_instance_id: int
+  @ivar next_instance_id: next instance id to be used when creating elements on
+    hardware_list
+
+  """
+  def __init__(self, has_gnt_section):
+    """Initialize the writer - set the top element.
+
+    @type has_gnt_section: bool
+    @param has_gnt_section: if the Ganeti schema should be added - i.e. this
+      means that Ganeti section will be present
+
+    """
+    env_attribs = {
+      "xmlns:xsi": XML_SCHEMA,
+      "xmlns:vssd": VSSD_SCHEMA,
+      "xmlns:rasd": RASD_SCHEMA,
+      "xmlns:ovf": OVF_SCHEMA,
+      "xmlns": OVF_SCHEMA,
+      "xml:lang": "en-US",
+    }
+    if has_gnt_section:
+      env_attribs["xmlns:gnt"] = GANETI_SCHEMA
+      self.virtual_system_type = VS_TYPE["ganeti"]
+    else:
+      self.virtual_system_type = VS_TYPE["external"]
+    self.tree = ET.Element("Envelope", attrib=env_attribs)
+    self.hardware_list = []
+    # INSTANCE_ID contains statically assigned IDs, starting from 0
+    self.next_instance_id = len(INSTANCE_ID) # FIXME: hackish
+
+  def SaveDisksData(self, disks):
+    """Convert disk information to certain OVF sections.
+
+    @type disks: list
+    @param disks: list of dictionaries of disk options from config.ini
+
+    """
+    references = ET.SubElement(self.tree, "References")
+    disk_section = ET.SubElement(self.tree, "DiskSection")
+    SubElementText(disk_section, "Info", "Virtual disk information")
+    for counter, disk in enumerate(disks):
+      file_id = "file%s" % counter
+      disk_id = "disk%s" % counter
+      file_attribs = {
+        "ovf:href": disk["path"],
+        "ovf:size": str(disk["real-size"]),
+        "ovf:id": file_id,
+      }
+      disk_attribs = {
+        "ovf:capacity": str(disk["virt-size"]),
+        "ovf:diskId": disk_id,
+        "ovf:fileRef": file_id,
+        "ovf:format": DISK_FORMAT.get(disk["format"], disk["format"]),
+      }
+      if "compression" in disk:
+        file_attribs["ovf:compression"] = disk["compression"]
+      ET.SubElement(references, "File", attrib=file_attribs)
+      ET.SubElement(disk_section, "Disk", attrib=disk_attribs)
+
+      # Item in VirtualHardwareSection creation
+      disk_item = ET.Element("Item")
+      SubElementText(disk_item, "rasd:ElementName", disk_id)
+      SubElementText(disk_item, "rasd:HostResource", "ovf:/disk/%s" % disk_id)
+      SubElementText(disk_item, "rasd:InstanceID", self.next_instance_id)
+      SubElementText(disk_item, "rasd:Parent", INSTANCE_ID["scsi"])
+      SubElementText(disk_item, "rasd:ResourceType", RASD_TYPE["disk"])
+      self.hardware_list.append(disk_item)
+      self.next_instance_id += 1
+
+  def SaveNetworksData(self, networks):
+    """Convert network information to NetworkSection.
+
+    @type networks: list
+    @param networks: list of dictionaries of network options form config.ini
+
+    """
+    network_section = ET.SubElement(self.tree, "NetworkSection")
+    SubElementText(network_section, "Info", "List of logical networks")
+    for counter, network in enumerate(networks):
+      network_name = "%s%s" % (network["mode"], counter)
+      network_attrib = {"ovf:name": network_name}
+      ET.SubElement(network_section, "Network", attrib=network_attrib)
+
+      # Item in VirtualHardwareSection creation
+      network_item = ET.Element("Item")
+      SubElementText(network_item, "rasd:Address", network["mac"])
+      SubElementText(network_item, "rasd:Connection", network_name)
+      SubElementText(network_item, "rasd:ElementName", network_name)
+      SubElementText(network_item, "rasd:InstanceID", self.next_instance_id)
+      SubElementText(network_item, "rasd:ResourceType",
+        RASD_TYPE["ethernet-adapter"])
+      self.hardware_list.append(network_item)
+      self.next_instance_id += 1
+
+  @staticmethod
+  def _SaveNameAndParams(root, data):
+    """Save name and parameters information under root using data.
+
+    @type root: ET.Element
+    @param root: root element for the Name and Parameters
+    @type data: dict
+    @param data: data from which we gather the values
+
+    """
+    assert(data.get("name"))
+    name = SubElementText(root, "gnt:Name", data["name"])
+    params = ET.SubElement(root, "gnt:Parameters")
+    for name, value in data.iteritems():
+      if name != "name":
+        SubElementText(params, "gnt:%s" % name, value)
+
+  def SaveGanetiData(self, ganeti, networks):
+    """Convert Ganeti-specific information to GanetiSection.
+
+    @type ganeti: dict
+    @param ganeti: dictionary of Ganeti-specific options from config.ini
+    @type networks: list
+    @param networks: list of dictionaries of network options form config.ini
+
+    """
+    ganeti_section = ET.SubElement(self.tree, "gnt:GanetiSection")
+
+    SubElementText(ganeti_section, "gnt:Version", ganeti.get("version"))
+    SubElementText(ganeti_section, "gnt:DiskTemplate",
+      ganeti.get("disk_template"))
+    SubElementText(ganeti_section, "gnt:AutoBalance",
+      ganeti.get("auto_balance"))
+    SubElementText(ganeti_section, "gnt:Tags", ganeti.get("tags"))
+
+    osys = ET.SubElement(ganeti_section, "gnt:OperatingSystem")
+    self._SaveNameAndParams(osys, ganeti["os"])
+
+    hypervisor = ET.SubElement(ganeti_section, "gnt:Hypervisor")
+    self._SaveNameAndParams(hypervisor, ganeti["hypervisor"])
+
+    network_section = ET.SubElement(ganeti_section, "gnt:Network")
+    for counter, network in enumerate(networks):
+      network_name = "%s%s" % (network["mode"], counter)
+      nic_attrib = {"ovf:name": network_name}
+      nic = ET.SubElement(network_section, "gnt:Nic", attrib=nic_attrib)
+      SubElementText(nic, "gnt:Mode", network["mode"])
+      SubElementText(nic, "gnt:MACAddress", network["mac"])
+      SubElementText(nic, "gnt:IPAddress", network["ip"])
+      SubElementText(nic, "gnt:Link", network["link"])
+
+  def SaveVirtualSystemData(self, name, vcpus, memory):
+    """Convert virtual system information to OVF sections.
+
+    @type name: string
+    @param name: name of the instance
+    @type vcpus: int
+    @param vcpus: number of VCPUs
+    @type memory: int
+    @param memory: RAM memory in MB
+
+    """
+    assert(vcpus > 0)
+    assert(memory > 0)
+    vs_attrib = {"ovf:id": name}
+    virtual_system = ET.SubElement(self.tree, "VirtualSystem", attrib=vs_attrib)
+    SubElementText(virtual_system, "Info", "A virtual machine")
+
+    name_section = ET.SubElement(virtual_system, "Name")
+    name_section.text = name
+    os_attrib = {"ovf:id": "0"}
+    os_section = ET.SubElement(virtual_system, "OperatingSystemSection",
+      attrib=os_attrib)
+    SubElementText(os_section, "Info", "Installed guest operating system")
+    hardware_section = ET.SubElement(virtual_system, "VirtualHardwareSection")
+    SubElementText(hardware_section, "Info", "Virtual hardware requirements")
+
+    # System description
+    system = ET.SubElement(hardware_section, "System")
+    SubElementText(system, "vssd:ElementName", "Virtual Hardware Family")
+    SubElementText(system, "vssd:InstanceID", INSTANCE_ID["system"])
+    SubElementText(system, "vssd:VirtualSystemIdentifier", name)
+    SubElementText(system, "vssd:VirtualSystemType", self.virtual_system_type)
+
+    # Item for vcpus
+    vcpus_item = ET.SubElement(hardware_section, "Item")
+    SubElementText(vcpus_item, "rasd:ElementName",
+      "%s virtual CPU(s)" % vcpus)
+    SubElementText(vcpus_item, "rasd:InstanceID", INSTANCE_ID["vcpus"])
+    SubElementText(vcpus_item, "rasd:ResourceType", RASD_TYPE["vcpus"])
+    SubElementText(vcpus_item, "rasd:VirtualQuantity", vcpus)
+
+    # Item for memory
+    memory_item = ET.SubElement(hardware_section, "Item")
+    SubElementText(memory_item, "rasd:AllocationUnits", "byte * 2^20")
+    SubElementText(memory_item, "rasd:ElementName", "%sMB of memory" % memory)
+    SubElementText(memory_item, "rasd:InstanceID", INSTANCE_ID["memory"])
+    SubElementText(memory_item, "rasd:ResourceType", RASD_TYPE["memory"])
+    SubElementText(memory_item, "rasd:VirtualQuantity", memory)
+
+    # Item for scsi controller
+    scsi_item = ET.SubElement(hardware_section, "Item")
+    SubElementText(scsi_item, "rasd:Address", INSTANCE_ID["system"])
+    SubElementText(scsi_item, "rasd:ElementName", "scsi_controller0")
+    SubElementText(scsi_item, "rasd:InstanceID", INSTANCE_ID["scsi"])
+    SubElementText(scsi_item, "rasd:ResourceSubType", SCSI_SUBTYPE)
+    SubElementText(scsi_item, "rasd:ResourceType", RASD_TYPE["scsi-controller"])
+
+    # Other items - from self.hardware_list
+    for item in self.hardware_list:
+      hardware_section.append(item)
+
+  def PrettyXmlDump(self):
+    """Formatter of the XML file.
+
+    @rtype: string
+    @return: XML tree in the form of nicely-formatted string
+
+    """
+    raw_string = ET.tostring(self.tree)
+    parsed_xml = xml.dom.minidom.parseString(raw_string)
+    xml_string = parsed_xml.toprettyxml(indent="  ")
+    text_re = re.compile(">\n\s+([^<>\s].*?)\n\s+</", re.DOTALL)
+    return text_re.sub(">\g<1></", xml_string)
+
+
+class Converter(object):
+  """Converter class for OVF packages.
+
+  Converter is a class above both ImporterOVF and ExporterOVF. It's purpose is
+  to provide a common interface for the two.
+
+  @type options: optparse.Values
+  @ivar options: options parsed from the command line
+  @type output_dir: string
+  @ivar output_dir: directory to which the results of conversion shall be
+    written
+  @type temp_file_manager: L{utils.TemporaryFileManager}
+  @ivar temp_file_manager: container for temporary files created during
+    conversion
+  @type temp_dir: string
+  @ivar temp_dir: temporary directory created then we deal with OVA
+
+  """
+  def __init__(self, input_path, options):
+    """Initialize the converter.
+
+    @type input_path: string
+    @param input_path: path to the Converter input file
+    @type options: optparse.Values
+    @param options: command line options
+
+    @raise errors.OpPrereqError: if file does not exist
+
+    """
+    input_path = os.path.abspath(input_path)
+    if not os.path.isfile(input_path):
+      raise errors.OpPrereqError("File does not exist: %s" % input_path)
+    self.options = options
+    self.temp_file_manager = utils.TemporaryFileManager()
+    self.temp_dir = None
+    self.output_dir = None
+    self._ReadInputData(input_path)
+
+  def _ReadInputData(self, input_path):
+    """Reads the data on which the conversion will take place.
+
+    @type input_path: string
+    @param input_path: absolute path to the Converter input file
+
+    """
+    raise NotImplementedError()
+
+  def _CompressDisk(self, disk_path, compression, action):
+    """Performs (de)compression on the disk and returns the new path
+
+    @type disk_path: string
+    @param disk_path: path to the disk
+    @type compression: string
+    @param compression: compression type
+    @type action: string
+    @param action: whether the action is compression or decompression
+    @rtype: string
+    @return: new disk path after (de)compression
+
+    @raise errors.OpPrereqError: disk (de)compression failed or "compression"
+      is not supported
+
+    """
+    assert(action in ALLOWED_ACTIONS)
+    # For now we only support gzip, as it is used in ovftool
+    if compression != COMPRESSION_TYPE:
+      raise errors.OpPrereqError("Unsupported compression type: %s"
+                                 % compression)
+    disk_file = os.path.basename(disk_path)
+    if action == DECOMPRESS:
+      (disk_name, _) = os.path.splitext(disk_file)
+      prefix = disk_name
+    elif action == COMPRESS:
+      prefix = disk_file
+    new_path = utils.GetClosedTempfile(suffix=COMPRESSION_EXT, prefix=prefix,
+      dir=self.output_dir)
+    self.temp_file_manager.Add(new_path)
+    args = ["gzip", "-c", disk_path]
+    run_result = utils.RunCmd(args, output=new_path)
+    if run_result.failed:
+      raise errors.OpPrereqError("Disk %s failed with output: %s"
+                                 % (action, run_result.stderr))
+    logging.info("The %s of the disk is completed", action)
+    return (COMPRESSION_EXT, new_path)
+
+  def _ConvertDisk(self, disk_format, disk_path):
+    """Performes conversion to specified format.
+
+    @type disk_format: string
+    @param disk_format: format to which the disk should be converted
+    @type disk_path: string
+    @param disk_path: path to the disk that should be converted
+    @rtype: string
+    @return path to the output disk
+
+    @raise errors.OpPrereqError: convertion of the disk failed
+
+    """
+    CheckQemuImg()
+    disk_file = os.path.basename(disk_path)
+    (disk_name, disk_extension) = os.path.splitext(disk_file)
+    if disk_extension != disk_format:
+      logging.warning("Conversion of disk image to %s format, this may take"
+                      " a while", disk_format)
+
+    new_disk_path = utils.GetClosedTempfile(suffix=".%s" % disk_format,
+      prefix=disk_name, dir=self.output_dir)
+    self.temp_file_manager.Add(new_disk_path)
+    args = [
+      constants.QEMUIMG_PATH,
+      "convert",
+      "-O",
+      disk_format,
+      disk_path,
+      new_disk_path,
+    ]
+    run_result = utils.RunCmd(args, cwd=os.getcwd())
+    if run_result.failed:
+      raise errors.OpPrereqError("Convertion to %s failed, qemu-img output was"
+                                 ": %s" % (disk_format, run_result.stderr))
+    return (".%s" % disk_format, new_disk_path)
+
+  @staticmethod
+  def _GetDiskQemuInfo(disk_path, regexp):
+    """Figures out some information of the disk using qemu-img.
+
+    @type disk_path: string
+    @param disk_path: path to the disk we want to know the format of
+    @type regexp: string
+    @param regexp: string that has to be matched, it has to contain one group
+    @rtype: string
+    @return: disk format
+
+    @raise errors.OpPrereqError: format information cannot be retrieved
+
+    """
+    CheckQemuImg()
+    args = [constants.QEMUIMG_PATH, "info", disk_path]
+    run_result = utils.RunCmd(args, cwd=os.getcwd())
+    if run_result.failed:
+      raise errors.OpPrereqError("Gathering info about the disk using qemu-img"
+                                 " failed, output was: %s" % run_result.stderr)
+    result = run_result.output
+    regexp = r"%s" % regexp
+    match = re.search(regexp, result)
+    if match:
+      disk_format = match.group(1)
+    else:
+      raise errors.OpPrereqError("No file information matching %s found in:"
+                                 " %s" % (regexp, result))
+    return disk_format
+
+  def Parse(self):
+    """Parses the data and creates a structure containing all required info.
+
+    """
+    raise NotImplementedError()
+
+  def Save(self):
+    """Saves the gathered configuration in an apropriate format.
+
+    """
+    raise NotImplementedError()
+
+  def Cleanup(self):
+    """Cleans the temporary directory, if one was created.
+
+    """
+    self.temp_file_manager.Cleanup()
+    if self.temp_dir:
+      shutil.rmtree(self.temp_dir)
+      self.temp_dir = None
+
+
+class OVFImporter(Converter):
+  """Converter from OVF to Ganeti config file.
+
+  @type input_dir: string
+  @ivar input_dir: directory in which the .ovf file resides
+  @type output_dir: string
+  @ivar output_dir: directory to which the results of conversion shall be
+    written
+  @type input_path: string
+  @ivar input_path: complete path to the .ovf file
+  @type ovf_reader: L{OVFReader}
+  @ivar ovf_reader: OVF reader instance collects data from .ovf file
+  @type results_name: string
+  @ivar results_name: name of imported instance
+  @type results_template: string
+  @ivar results_template: disk template read from .ovf file or command line
+    arguments
+  @type results_hypervisor: dict
+  @ivar results_hypervisor: hypervisor information gathered from .ovf file or
+    command line arguments
+  @type results_os: dict
+  @ivar results_os: operating system information gathered from .ovf file or
+    command line arguments
+  @type results_backend: dict
+  @ivar results_backend: backend information gathered from .ovf file or
+    command line arguments
+  @type results_tags: string
+  @ivar results_tags: string containing instance-specific tags
+  @type results_version: string
+  @ivar results_version: version as required by Ganeti import
+  @type results_network: dict
+  @ivar results_network: network information gathered from .ovf file or command
+    line arguments
+  @type results_disk: dict
+  @ivar results_disk: disk information gathered from .ovf file or command line
+    arguments
+
+  """
+  def _ReadInputData(self, input_path):
+    """Reads the data on which the conversion will take place.
+
+    @type input_path: string
+    @param input_path: absolute path to the .ovf or .ova input file
+
+    @raise errors.OpPrereqError: if input file is neither .ovf nor .ova
+
+    """
+    (input_dir, input_file) = os.path.split(input_path)
+    (_, input_extension) = os.path.splitext(input_file)
+
+    if input_extension == OVF_EXT:
+      logging.info("%s file extension found, no unpacking necessary", OVF_EXT)
+      self.input_dir = input_dir
+      self.input_path = input_path
+      self.temp_dir = None
+    elif input_extension == OVA_EXT:
+      logging.info("%s file extension found, proceeding to unpacking", OVA_EXT)
+      self._UnpackOVA(input_path)
+    else:
+      raise errors.OpPrereqError("Unknown file extension; expected %s or %s"
+                                 " file" % (OVA_EXT, OVF_EXT))
+    assert ((input_extension == OVA_EXT and self.temp_dir) or
+            (input_extension == OVF_EXT and not self.temp_dir))
+    assert self.input_dir in self.input_path
+
+    if self.options.output_dir:
+      self.output_dir = os.path.abspath(self.options.output_dir)
+      if (os.path.commonprefix([constants.EXPORT_DIR, self.output_dir]) !=
+          constants.EXPORT_DIR):
+        logging.warning("Export path is not under %s directory, import to"
+                        " Ganeti using gnt-backup may fail",
+                        constants.EXPORT_DIR)
+    else:
+      self.output_dir = constants.EXPORT_DIR
+
+    self.ovf_reader = OVFReader(self.input_path)
+    self.ovf_reader.VerifyManifest()
+
+  def _UnpackOVA(self, input_path):
+    """Unpacks the .ova package into temporary directory.
+
+    @type input_path: string
+    @param input_path: path to the .ova package file
+
+    @raise errors.OpPrereqError: if file is not a proper tarball, one of the
+        files in the archive seem malicious (e.g. path starts with '../') or
+        .ova package does not contain .ovf file
+
+    """
+    input_name = None
+    if not tarfile.is_tarfile(input_path):
+      raise errors.OpPrereqError("The provided %s file is not a proper tar"
+                                 " archive", OVA_EXT)
+    ova_content = tarfile.open(input_path)
+    temp_dir = tempfile.mkdtemp()
+    self.temp_dir = temp_dir
+    for file_name in ova_content.getnames():
+      file_normname = os.path.normpath(file_name)
+      try:
+        utils.PathJoin(temp_dir, file_normname)
+      except ValueError, err:
+        raise errors.OpPrereqError("File %s inside %s package is not safe" %
+                                   (file_name, OVA_EXT))
+      if file_name.endswith(OVF_EXT):
+        input_name = file_name
+    if not input_name:
+      raise errors.OpPrereqError("No %s file in %s package found" %
+                                 (OVF_EXT, OVA_EXT))
+    logging.warning("Unpacking the %s archive, this may take a while",
+      input_path)
+    self.input_dir = temp_dir
+    self.input_path = utils.PathJoin(self.temp_dir, input_name)
+    try:
+      try:
+        extract = ova_content.extractall
+      except AttributeError:
+        # This is a prehistorical case of using python < 2.5
+        for member in ova_content.getmembers():
+          ova_content.extract(member, path=self.temp_dir)
+      else:
+        extract(self.temp_dir)
+    except tarfile.TarError, err:
+      raise errors.OpPrereqError("Error while extracting %s archive: %s" %
+                                 (OVA_EXT, err))
+    logging.info("OVA package extracted to %s directory", self.temp_dir)
+
+  def Parse(self):
+    """Parses the data and creates a structure containing all required info.
+
+    The method reads the information given either as a command line option or as
+    a part of the OVF description.
+
+    @raise errors.OpPrereqError: if some required part of the description of
+      virtual instance is missing or unable to create output directory
+
+    """
+    self.results_name = self._GetInfo("instance name", self.options.name,
+      self._ParseNameOptions, self.ovf_reader.GetInstanceName)
+    if not self.results_name:
+      raise errors.OpPrereqError("Name of instance not provided")
+
+    self.output_dir = utils.PathJoin(self.output_dir, self.results_name)
+    try:
+      utils.Makedirs(self.output_dir)
+    except OSError, err:
+      raise errors.OpPrereqError("Failed to create directory %s: %s" %
+                                 (self.output_dir, err))
+
+    self.results_template = self._GetInfo("disk template",
+      self.options.disk_template, self._ParseTemplateOptions,
+      self.ovf_reader.GetDiskTemplate)
+    if not self.results_template:
+      logging.info("Disk template not given")
+
+    self.results_hypervisor = self._GetInfo("hypervisor",
+      self.options.hypervisor, self._ParseHypervisorOptions,
+      self.ovf_reader.GetHypervisorData)
+    assert self.results_hypervisor["hypervisor_name"]
+    if self.results_hypervisor["hypervisor_name"] == constants.VALUE_AUTO:
+      logging.debug("Default hypervisor settings from the cluster will be used")
+
+    self.results_os = self._GetInfo("OS", self.options.os,
+      self._ParseOSOptions, self.ovf_reader.GetOSData)
+    if not self.results_os.get("os_name"):
+      raise errors.OpPrereqError("OS name must be provided")
+
+    self.results_backend = self._GetInfo("backend", self.options.beparams,
+      self._ParseBackendOptions, self.ovf_reader.GetBackendData)
+    assert self.results_backend.get("vcpus")
+    assert self.results_backend.get("memory")
+    assert self.results_backend.get("auto_balance") is not None
+
+    self.results_tags = self._GetInfo("tags", self.options.tags,
+      self._ParseTags, self.ovf_reader.GetTagsData)
+
+    ovf_version = self.ovf_reader.GetVersionData()
+    if ovf_version:
+      self.results_version = ovf_version
+    else:
+      self.results_version = constants.EXPORT_VERSION
+
+    self.results_network = self._GetInfo("network", self.options.nics,
+      self._ParseNicOptions, self.ovf_reader.GetNetworkData,
+      ignore_test=self.options.no_nics)
+
+    self.results_disk = self._GetInfo("disk", self.options.disks,
+      self._ParseDiskOptions, self._GetDiskInfo,
+      ignore_test=self.results_template == constants.DT_DISKLESS)
+
+    if not self.results_disk and not self.results_network:
+      raise errors.OpPrereqError("Either disk specification or network"
+                                 " description must be present")
+
+  @staticmethod
+  def _GetInfo(name, cmd_arg, cmd_function, nocmd_function,
+    ignore_test=False):
+    """Get information about some section - e.g. disk, network, hypervisor.
+
+    @type name: string
+    @param name: name of the section
+    @type cmd_arg: dict
+    @param cmd_arg: command line argument specific for section 'name'
+    @type cmd_function: callable
+    @param cmd_function: function to call if 'cmd_args' exists
+    @type nocmd_function: callable
+    @param nocmd_function: function to call if 'cmd_args' is not there
+
+    """
+    if ignore_test:
+      logging.info("Information for %s will be ignored", name)
+      return {}
+    if cmd_arg:
+      logging.info("Information for %s will be parsed from command line", name)
+      results = cmd_function()
+    else:
+      logging.info("Information for %s will be parsed from %s file",
+        name, OVF_EXT)
+      results = nocmd_function()
+    logging.info("Options for %s were succesfully read", name)
+    return results
+
+  def _ParseNameOptions(self):
+    """Returns name if one was given in command line.
+
+    @rtype: string
+    @return: name of an instance
+
+    """
+    return self.options.name
+
+  def _ParseTemplateOptions(self):
+    """Returns disk template if one was given in command line.
+
+    @rtype: string
+    @return: disk template name
+
+    """
+    return self.options.disk_template
+
+  def _ParseHypervisorOptions(self):
+    """Parses hypervisor options given in a command line.
+
+    @rtype: dict
+    @return: dictionary containing name of the chosen hypervisor and all the
+      options
+
+    """
+    assert type(self.options.hypervisor) is tuple
+    assert len(self.options.hypervisor) == 2
+    results = {}
+    if self.options.hypervisor[0]:
+      results["hypervisor_name"] = self.options.hypervisor[0]
+    else:
+      results["hypervisor_name"] = constants.VALUE_AUTO
+    results.update(self.options.hypervisor[1])
+    return results
+
+  def _ParseOSOptions(self):
+    """Parses OS options given in command line.
+
+    @rtype: dict
+    @return: dictionary containing name of chosen OS and all its options
+
+    """
+    assert self.options.os
+    results = {}
+    results["os_name"] = self.options.os
+    results.update(self.options.osparams)
+    return results
+
+  def _ParseBackendOptions(self):
+    """Parses backend options given in command line.
+
+    @rtype: dict
+    @return: dictionary containing vcpus, memory and auto-balance options
+
+    """
+    assert self.options.beparams
+    backend = {}
+    backend.update(self.options.beparams)
+    must_contain = ["vcpus", "memory", "auto_balance"]
+    for element in must_contain:
+      if backend.get(element) is None:
+        backend[element] = constants.VALUE_AUTO
+    return backend
+
+  def _ParseTags(self):
+    """Returns tags list given in command line.
+
+    @rtype: string
+    @return: string containing comma-separated tags
+
+    """
+    return self.options.tags
+
+  def _ParseNicOptions(self):
+    """Parses network options given in a command line or as a dictionary.
+
+    @rtype: dict
+    @return: dictionary of network-related options
+
+    """
+    assert self.options.nics
+    results = {}
+    for (nic_id, nic_desc) in self.options.nics:
+      results["nic%s_mode" % nic_id] = \
+        nic_desc.get("mode", constants.VALUE_AUTO)
+      results["nic%s_mac" % nic_id] = nic_desc.get("mac", constants.VALUE_AUTO)
+      results["nic%s_link" % nic_id] = \
+        nic_desc.get("link", constants.VALUE_AUTO)
+      if nic_desc.get("mode") == "bridged":
+        results["nic%s_ip" % nic_id] = constants.VALUE_NONE
+      else:
+        results["nic%s_ip" % nic_id] = constants.VALUE_AUTO
+    results["nic_count"] = str(len(self.options.nics))
+    return results
+
+  def _ParseDiskOptions(self):
+    """Parses disk options given in a command line.
+
+    @rtype: dict
+    @return: dictionary of disk-related options
+
+    @raise errors.OpPrereqError: disk description does not contain size
+      information or size information is invalid or creation failed
+
+    """
+    CheckQemuImg()
+    assert self.options.disks
+    results = {}
+    for (disk_id, disk_desc) in self.options.disks:
+      results["disk%s_ivname" % disk_id] = "disk/%s" % disk_id
+      if disk_desc.get("size"):
+        try:
+          disk_size = utils.ParseUnit(disk_desc["size"])
+        except ValueError:
+          raise errors.OpPrereqError("Invalid disk size for disk %s: %s" %
+                                     (disk_id, disk_desc["size"]))
+        new_path = utils.PathJoin(self.output_dir, str(disk_id))
+        args = [
+          constants.QEMUIMG_PATH,
+          "create",
+          "-f",
+          "raw",
+          new_path,
+          disk_size,
+        ]
+        run_result = utils.RunCmd(args)
+        if run_result.failed:
+          raise errors.OpPrereqError("Creation of disk %s failed, output was:"
+                                     " %s" % (new_path, run_result.stderr))
+        results["disk%s_size" % disk_id] = str(disk_size)
+        results["disk%s_dump" % disk_id] = "disk%s.raw" % disk_id
+      else:
+        raise errors.OpPrereqError("Disks created for import must have their"
+                                   " size specified")
+    results["disk_count"] = str(len(self.options.disks))
+    return results
+
+  def _GetDiskInfo(self):
+    """Gathers information about disks used by instance, perfomes conversion.
+
+    @rtype: dict
+    @return: dictionary of disk-related options
+
+    @raise errors.OpPrereqError: disk is not in the same directory as .ovf file
+
+    """
+    results = {}
+    disks_list = self.ovf_reader.GetDisksNames()
+    for (counter, (disk_name, disk_compression)) in enumerate(disks_list):
+      if os.path.dirname(disk_name):
+        raise errors.OpPrereqError("Disks are not allowed to have absolute"
+                                   " paths or paths outside main OVF directory")
+      disk, _ = os.path.splitext(disk_name)
+      disk_path = utils.PathJoin(self.input_dir, disk_name)
+      if disk_compression not in NO_COMPRESSION:
+        _, disk_path = self._CompressDisk(disk_path, disk_compression,
+          DECOMPRESS)
+        disk, _ = os.path.splitext(disk)
+      if self._GetDiskQemuInfo(disk_path, "file format: (\S+)") != "raw":
+        logging.info("Conversion to raw format is required")
+      ext, new_disk_path = self._ConvertDisk("raw", disk_path)
+
+      final_disk_path = LinkFile(new_disk_path, prefix=disk, suffix=ext,
+        directory=self.output_dir)
+      final_name = os.path.basename(final_disk_path)
+      disk_size = os.path.getsize(final_disk_path) / (1024 * 1024)
+      results["disk%s_dump" % counter] = final_name
+      results["disk%s_size" % counter] = str(disk_size)
+      results["disk%s_ivname" % counter] = "disk/%s" % str(counter)
+    if disks_list:
+      results["disk_count"] = str(len(disks_list))
+    return results
+
+  def Save(self):
+    """Saves all the gathered information in a constant.EXPORT_CONF_FILE file.
+
+    @raise errors.OpPrereqError: when saving to config file failed
+
+    """
+    logging.info("Conversion was succesfull, saving %s in %s directory",
+                 constants.EXPORT_CONF_FILE, self.output_dir)
+    results = {
+      constants.INISECT_INS: {},
+      constants.INISECT_BEP: {},
+      constants.INISECT_EXP: {},
+      constants.INISECT_OSP: {},
+      constants.INISECT_HYP: {},
+    }
+
+    results[constants.INISECT_INS].update(self.results_disk)
+    results[constants.INISECT_INS].update(self.results_network)
+    results[constants.INISECT_INS]["hypervisor"] = \
+      self.results_hypervisor["hypervisor_name"]
+    results[constants.INISECT_INS]["name"] = self.results_name
+    if self.results_template:
+      results[constants.INISECT_INS]["disk_template"] = self.results_template
+    if self.results_tags:
+      results[constants.INISECT_INS]["tags"] = self.results_tags
+
+    results[constants.INISECT_BEP].update(self.results_backend)
+
+    results[constants.INISECT_EXP]["os"] = self.results_os["os_name"]
+    results[constants.INISECT_EXP]["version"] = self.results_version
+
+    del self.results_os["os_name"]
+    results[constants.INISECT_OSP].update(self.results_os)
+
+    del self.results_hypervisor["hypervisor_name"]
+    results[constants.INISECT_HYP].update(self.results_hypervisor)
+
+    output_file_name = utils.PathJoin(self.output_dir,
+      constants.EXPORT_CONF_FILE)
+
+    output = []
+    for section, options in results.iteritems():
+      output.append("[%s]" % section)
+      for name, value in options.iteritems():
+        if value is None:
+          value = ""
+        output.append("%s = %s" % (name, value))
+      output.append("")
+    output_contents = "\n".join(output)
+
+    try:
+      utils.WriteFile(output_file_name, data=output_contents)
+    except errors.ProgrammerError, err:
+      raise errors.OpPrereqError("Saving the config file failed: %s" % err)
+
+    self.Cleanup()
+
+
+class ConfigParserWithDefaults(ConfigParser.SafeConfigParser):
+  """This is just a wrapper on SafeConfigParser, that uses default values
+
+  """
+  def get(self, section, options, raw=None, vars=None): # pylint: disable=W0622
+    try:
+      result = ConfigParser.SafeConfigParser.get(self, section, options, \
+        raw=raw, vars=vars)
+    except ConfigParser.NoOptionError:
+      result = None
+    return result
+
+  def getint(self, section, options):
+    try:
+      result = ConfigParser.SafeConfigParser.get(self, section, options)
+    except ConfigParser.NoOptionError:
+      result = 0
+    return int(result)
+
+
+class OVFExporter(Converter):
+  """Converter from Ganeti config file to OVF
+
+  @type input_dir: string
+  @ivar input_dir: directory in which the config.ini file resides
+  @type output_dir: string
+  @ivar output_dir: directory to which the results of conversion shall be
+    written
+  @type packed_dir: string
+  @ivar packed_dir: if we want OVA package, this points to the real (i.e. not
+    temp) output directory
+  @type input_path: string
+  @ivar input_path: complete path to the config.ini file
+  @type output_path: string
+  @ivar output_path: complete path to .ovf file
+  @type config_parser: L{ConfigParserWithDefaults}
+  @ivar config_parser: parser for the config.ini file
+  @type reference_files: list
+  @ivar reference_files: files referenced in the ovf file
+  @type results_disk: list
+  @ivar results_disk: list of dictionaries of disk options from config.ini
+  @type results_network: list
+  @ivar results_network: list of dictionaries of network options form config.ini
+  @type results_name: string
+  @ivar results_name: name of the instance
+  @type results_vcpus: string
+  @ivar results_vcpus: number of VCPUs
+  @type results_memory: string
+  @ivar results_memory: RAM memory in MB
+  @type results_ganeti: dict
+  @ivar results_ganeti: dictionary of Ganeti-specific options from config.ini
+
+  """
+  def _ReadInputData(self, input_path):
+    """Reads the data on which the conversion will take place.
+
+    @type input_path: string
+    @param input_path: absolute path to the config.ini input file
+
+    @raise errors.OpPrereqError: error when reading the config file
+
+    """
+    input_dir = os.path.dirname(input_path)
+    self.input_path = input_path
+    self.input_dir = input_dir
+    if self.options.output_dir:
+      self.output_dir = os.path.abspath(self.options.output_dir)
+    else:
+      self.output_dir = input_dir
+    self.config_parser = ConfigParserWithDefaults()
+    logging.info("Reading configuration from %s file", input_path)
+    try:
+      self.config_parser.read(input_path)
+    except ConfigParser.MissingSectionHeaderError, err:
+      raise errors.OpPrereqError("Error when trying to read %s: %s" %
+                                 (input_path, err))
+    if self.options.ova_package:
+      self.temp_dir = tempfile.mkdtemp()
+      self.packed_dir = self.output_dir
+      self.output_dir = self.temp_dir
+
+    self.ovf_writer = OVFWriter(not self.options.ext_usage)
+
+  def _ParseName(self):
+    """Parses name from command line options or config file.
+
+    @rtype: string
+    @return: name of Ganeti instance
+
+    @raise errors.OpPrereqError: if name of the instance is not provided
+
+    """
+    if self.options.name:
+      name = self.options.name
+    else:
+      name = self.config_parser.get(constants.INISECT_INS, NAME)
+    if name is None:
+      raise errors.OpPrereqError("No instance name found")
+    return name
+
+  def _ParseVCPUs(self):
+    """Parses vcpus number from config file.
+
+    @rtype: int
+    @return: number of virtual CPUs
+
+    @raise errors.OpPrereqError: if number of VCPUs equals 0
+
+    """
+    vcpus = self.config_parser.getint(constants.INISECT_BEP, VCPUS)
+    if vcpus == 0:
+      raise errors.OpPrereqError("No CPU information found")
+    return vcpus
+
+  def _ParseMemory(self):
+    """Parses vcpus number from config file.
+
+    @rtype: int
+    @return: amount of memory in MB
+
+    @raise errors.OpPrereqError: if amount of memory equals 0
+
+    """
+    memory = self.config_parser.getint(constants.INISECT_BEP, MEMORY)
+    if memory == 0:
+      raise errors.OpPrereqError("No memory information found")
+    return memory
+
+  def _ParseGaneti(self):
+    """Parses Ganeti data from config file.
+
+    @rtype: dictionary
+    @return: dictionary of Ganeti-specific options
+
+    """
+    results = {}
+    # hypervisor
+    results["hypervisor"] = {}
+    hyp_name = self.config_parser.get(constants.INISECT_INS, HYPERV)
+    if hyp_name is None:
+      raise errors.OpPrereqError("No hypervisor information found")
+    results["hypervisor"]["name"] = hyp_name
+    pairs = self.config_parser.items(constants.INISECT_HYP)
+    for (name, value) in pairs:
+      results["hypervisor"][name] = value
+    # os
+    results["os"] = {}
+    os_name = self.config_parser.get(constants.INISECT_EXP, OS)
+    if os_name is None:
+      raise errors.OpPrereqError("No operating system information found")
+    results["os"]["name"] = os_name
+    pairs = self.config_parser.items(constants.INISECT_OSP)
+    for (name, value) in pairs:
+      results["os"][name] = value
+    # other
+    others = [
+      (constants.INISECT_INS, DISK_TEMPLATE, "disk_template"),
+      (constants.INISECT_BEP, AUTO_BALANCE, "auto_balance"),
+      (constants.INISECT_INS, TAGS, "tags"),
+      (constants.INISECT_EXP, VERSION, "version"),
+    ]
+    for (section, element, name) in others:
+      results[name] = self.config_parser.get(section, element)
+    return results
+
+  def _ParseNetworks(self):
+    """Parses network data from config file.
+
+    @rtype: list
+    @return: list of dictionaries of network options
+
+    @raise errors.OpPrereqError: then network mode is not recognized
+
+    """
+    results = []
+    counter = 0
+    while True:
+      data_link = \
+        self.config_parser.get(constants.INISECT_INS, "nic%s_link" % counter)
+      if data_link is None:
+        break
+      results.append({
+        "mode": self.config_parser.get(constants.INISECT_INS,
+           "nic%s_mode" % counter),
+        "mac": self.config_parser.get(constants.INISECT_INS,
+           "nic%s_mac" % counter),
+        "ip": self.config_parser.get(constants.INISECT_INS,
+           "nic%s_ip" % counter),
+        "link": data_link,
+      })
+      if results[counter]["mode"] not in constants.NIC_VALID_MODES:
+        raise errors.OpPrereqError("Network mode %s not recognized"
+                                   % results[counter]["mode"])
+      counter += 1
+    return results
+
+  def _GetDiskOptions(self, disk_file, compression):
+    """Convert the disk and gather disk info for .ovf file.
+
+    @type disk_file: string
+    @param disk_file: name of the disk (without the full path)
+    @type compression: bool
+    @param compression: whether the disk should be compressed or not
+
+    @raise errors.OpPrereqError: when disk image does not exist
+
+    """
+    disk_path = utils.PathJoin(self.input_dir, disk_file)
+    results = {}
+    if not os.path.isfile(disk_path):
+      raise errors.OpPrereqError("Disk image does not exist: %s" % disk_path)
+    if os.path.dirname(disk_file):
+      raise errors.OpPrereqError("Path for the disk: %s contains a directory"
+                                 " name" % disk_path)
+    disk_name, _ = os.path.splitext(disk_file)
+    ext, new_disk_path = self._ConvertDisk(self.options.disk_format, disk_path)
+    results["format"] = self.options.disk_format
+    results["virt-size"] = self._GetDiskQemuInfo(new_disk_path,
+      "virtual size: \S+ \((\d+) bytes\)")
+    if compression:
+      ext2, new_disk_path = self._CompressDisk(new_disk_path, "gzip",
+        COMPRESS)
+      disk_name, _ = os.path.splitext(disk_name)
+      results["compression"] = "gzip"
+      ext += ext2
+    final_disk_path = LinkFile(new_disk_path, prefix=disk_name, suffix=ext,
+      directory=self.output_dir)
+    final_disk_name = os.path.basename(final_disk_path)
+    results["real-size"] = os.path.getsize(final_disk_path)
+    results["path"] = final_disk_name
+    self.references_files.append(final_disk_path)
+    return results
+
+  def _ParseDisks(self):
+    """Parses disk data from config file.
+
+    @rtype: list
+    @return: list of dictionaries of disk options
+
+    """
+    results = []
+    counter = 0
+    while True:
+      disk_file = \
+        self.config_parser.get(constants.INISECT_INS, "disk%s_dump" % counter)
+      if disk_file is None:
+        break
+      results.append(self._GetDiskOptions(disk_file, self.options.compression))
+      counter += 1
+    return results
+
+  def Parse(self):
+    """Parses the data and creates a structure containing all required info.
+
+    """
+    try:
+      utils.Makedirs(self.output_dir)
+    except OSError, err:
+      raise errors.OpPrereqError("Failed to create directory %s: %s" %
+                                 (self.output_dir, err))
+
+    self.references_files = []
+    self.results_name = self._ParseName()
+    self.results_vcpus = self._ParseVCPUs()
+    self.results_memory = self._ParseMemory()
+    if not self.options.ext_usage:
+      self.results_ganeti = self._ParseGaneti()
+    self.results_network = self._ParseNetworks()
+    self.results_disk = self._ParseDisks()
+
+  def _PrepareManifest(self, path):
+    """Creates manifest for all the files in OVF package.
+
+    @type path: string
+    @param path: path to manifesto file
+
+    @raise errors.OpPrereqError: if error occurs when writing file
+
+    """
+    logging.info("Preparing manifest for the OVF package")
+    lines = []
+    files_list = [self.output_path]
+    files_list.extend(self.references_files)
+    logging.warning("Calculating SHA1 checksums, this may take a while")
+    sha1_sums = utils.FingerprintFiles(files_list)
+    for file_path, value in sha1_sums.iteritems():
+      file_name = os.path.basename(file_path)
+      lines.append("SHA1(%s)= %s" % (file_name, value))
+    lines.append("")
+    data = "\n".join(lines)
+    try:
+      utils.WriteFile(path, data=data)
+    except errors.ProgrammerError, err:
+      raise errors.OpPrereqError("Saving the manifest file failed: %s" % err)
+
+  @staticmethod
+  def _PrepareTarFile(tar_path, files_list):
+    """Creates tarfile from the files in OVF package.
+
+    @type tar_path: string
+    @param tar_path: path to the resulting file
+    @type files_list: list
+    @param files_list: list of files in the OVF package
+
+    """
+    logging.info("Preparing tarball for the OVF package")
+    open(tar_path, mode="w").close()
+    ova_package = tarfile.open(name=tar_path, mode="w")
+    for file_path in files_list:
+      file_name = os.path.basename(file_path)
+      ova_package.add(file_path, arcname=file_name)
+    ova_package.close()
+
+  def Save(self):
+    """Saves the gathered configuration in an apropriate format.
+
+    @raise errors.OpPrereqError: if unable to create output directory
+
+    """
+    output_file = "%s%s" % (self.results_name, OVF_EXT)
+    output_path = utils.PathJoin(self.output_dir, output_file)
+    self.ovf_writer = OVFWriter(not self.options.ext_usage)
+    logging.info("Saving read data to %s", output_path)
+
+    self.output_path = utils.PathJoin(self.output_dir, output_file)
+    files_list = [self.output_path]
+
+    self.ovf_writer.SaveDisksData(self.results_disk)
+    self.ovf_writer.SaveNetworksData(self.results_network)
+    if not self.options.ext_usage:
+      self.ovf_writer.SaveGanetiData(self.results_ganeti, self.results_network)
+
+    self.ovf_writer.SaveVirtualSystemData(self.results_name, self.results_vcpus,
+      self.results_memory)
+
+    data = self.ovf_writer.PrettyXmlDump()
+    utils.WriteFile(self.output_path, data=data)
+
+    manifest_file = "%s%s" % (self.results_name, MF_EXT)
+    manifest_path = utils.PathJoin(self.output_dir, manifest_file)
+    self._PrepareManifest(manifest_path)
+    files_list.append(manifest_path)
+
+    files_list.extend(self.references_files)
+
+    if self.options.ova_package:
+      ova_file = "%s%s" % (self.results_name, OVA_EXT)
+      packed_path = utils.PathJoin(self.packed_dir, ova_file)
+      try:
+        utils.Makedirs(self.packed_dir)
+      except OSError, err:
+        raise errors.OpPrereqError("Failed to create directory %s: %s" %
+                                   (self.packed_dir, err))
+      self._PrepareTarFile(packed_path, files_list)
+    logging.info("Creation of the OVF package was successfull")
+    self.Cleanup()
index bd2a3fd..2923194 100644 (file)
@@ -58,12 +58,16 @@ OP_TRUE = "?"
 # operator-specific value
 OP_EQUAL = "="
 OP_NOT_EQUAL = "!="
+OP_LT = "<"
+OP_LE = "<="
+OP_GT = ">"
+OP_GE = ">="
 OP_REGEXP = "=~"
 OP_CONTAINS = "=[]"
 
 
 #: Characters used for detecting user-written filters (see L{_CheckFilter})
-FILTER_DETECTION_CHARS = frozenset("()=/!~'\"\\" + string.whitespace)
+FILTER_DETECTION_CHARS = frozenset("()=/!~'\"\\<>" + string.whitespace)
 
 #: Characters used to detect globbing filters (see L{_CheckGlobbing})
 GLOB_DETECTION_CHARS = frozenset("*?")
@@ -165,6 +169,10 @@ def BuildFilterParser():
   binopstbl = {
     "==": OP_EQUAL,
     "!=": OP_NOT_EQUAL,
+    "<": OP_LT,
+    "<=": OP_LE,
+    ">": OP_GT,
+    ">=": OP_GE,
     }
 
   binary_cond = (field_name + pyp.oneOf(binopstbl.keys()) + rval)
@@ -292,7 +300,7 @@ def _MakeFilterPart(namefield, text):
     return [OP_EQUAL, namefield, text]
 
 
-def MakeFilter(args, force_filter):
+def MakeFilter(args, force_filter, namefield=None):
   """Try to make a filter from arguments to a command.
 
   If the name could be a filter it is parsed as such. If it's just a globbing
@@ -303,10 +311,16 @@ def MakeFilter(args, force_filter):
   @param args: Arguments to command
   @type force_filter: bool
   @param force_filter: Whether to force treatment as a full-fledged filter
+  @type namefield: string
+  @param namefield: Name of field to use for simple filters (use L{None} for
+    a default of "name")
   @rtype: list
   @return: Query filter
 
   """
+  if namefield is None:
+    namefield = "name"
+
   if (force_filter or
       (args and len(args) == 1 and _CheckFilter(args[0]))):
     try:
@@ -317,7 +331,7 @@ def MakeFilter(args, force_filter):
 
     result = ParseFilter(filter_text)
   elif args:
-    result = [OP_OR] + map(compat.partial(_MakeFilterPart, "name"), args)
+    result = [OP_OR] + map(compat.partial(_MakeFilterPart, namefield), args)
   else:
     result = None
 
index 7a9360c..a8f19f0 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2010, 2011 Google Inc.
+# Copyright (C) 2010, 2011, 2012 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
@@ -62,6 +62,7 @@ from ganeti import utils
 from ganeti import compat
 from ganeti import objects
 from ganeti import ht
+from ganeti import runtime
 from ganeti import qlang
 
 from ganeti.constants import (QFT_UNKNOWN, QFT_TEXT, QFT_BOOL, QFT_NUMBER,
@@ -92,7 +93,12 @@ from ganeti.constants import (QFT_UNKNOWN, QFT_TEXT, QFT_BOOL, QFT_NUMBER,
 
 (GQ_CONFIG,
  GQ_NODE,
- GQ_INST) = range(200, 203)
+ GQ_INST,
+ GQ_DISKPARAMS) = range(200, 204)
+
+(CQ_CONFIG,
+ CQ_QUEUE_DRAINED,
+ CQ_WATCHER_PAUSE) = range(300, 303)
 
 # Query field flags
 QFF_HOSTNAME = 0x01
@@ -136,6 +142,12 @@ _VTToQFT = {
 
 _SERIAL_NO_DOC = "%s object serial number, incremented on each modification"
 
+# TODO: Consider moving titles closer to constants
+NDP_TITLE = {
+  constants.ND_OOB_PROGRAM: "OutOfBandProgram",
+  constants.ND_SPINDLE_COUNT: "SpindleCount",
+  }
+
 
 def _GetUnknownField(ctx, item): # pylint: disable=W0613
   """Gets the contents of an unknown field.
@@ -391,6 +403,18 @@ class _FilterCompilerHelper:
     qlang.OP_NOT_EQUAL:
       (_OPTYPE_BINARY, [(flags, compat.partial(_WrapNot, fn), valprepfn)
                         for (flags, fn, valprepfn) in _EQUALITY_CHECKS]),
+    qlang.OP_LT: (_OPTYPE_BINARY, [
+      (None, operator.lt, None),
+      ]),
+    qlang.OP_GT: (_OPTYPE_BINARY, [
+      (None, operator.gt, None),
+      ]),
+    qlang.OP_LE: (_OPTYPE_BINARY, [
+      (None, operator.le, None),
+      ]),
+    qlang.OP_GE: (_OPTYPE_BINARY, [
+      (None, operator.ge, None),
+      ]),
     qlang.OP_REGEXP: (_OPTYPE_BINARY, [
       (None, lambda lhs, rhs: rhs.search(lhs), _PrepareRegex),
       ]),
@@ -409,13 +433,13 @@ class _FilterCompilerHelper:
     self._hints = None
     self._op_handler = None
 
-  def __call__(self, hints, filter_):
+  def __call__(self, hints, qfilter):
     """Converts a query filter into a callable function.
 
     @type hints: L{_FilterHints} or None
     @param hints: Callbacks doing analysis on filter
-    @type filter_: list
-    @param filter_: Filter structure
+    @type qfilter: list
+    @param qfilter: Filter structure
     @rtype: callable
     @return: Function receiving context and item as parameters, returning
              boolean as to whether item matches filter
@@ -431,20 +455,20 @@ class _FilterCompilerHelper:
       }
 
     try:
-      filter_fn = self._Compile(filter_, 0)
+      filter_fn = self._Compile(qfilter, 0)
     finally:
       self._op_handler = None
 
     return filter_fn
 
-  def _Compile(self, filter_, level):
+  def _Compile(self, qfilter, level):
     """Inner function for converting filters.
 
     Calls the correct handler functions for the top-level operator. This
     function is called recursively (e.g. for logic operators).
 
     """
-    if not (isinstance(filter_, (list, tuple)) and filter_):
+    if not (isinstance(qfilter, (list, tuple)) and qfilter):
       raise errors.ParameterError("Invalid filter on level %s" % level)
 
     # Limit recursion
@@ -453,7 +477,7 @@ class _FilterCompilerHelper:
                                   " nested too deep)" % self._LEVELS_MAX)
 
     # Create copy to be modified
-    operands = filter_[:]
+    operands = qfilter[:]
     op = operands.pop(0)
 
     try:
@@ -581,7 +605,7 @@ class _FilterCompilerHelper:
                                  " (op '%s', flags %s)" % (op, field_flags))
 
 
-def _CompileFilter(fields, hints, filter_):
+def _CompileFilter(fields, hints, qfilter):
   """Converts a query filter into a callable function.
 
   See L{_FilterCompilerHelper} for details.
@@ -589,11 +613,11 @@ def _CompileFilter(fields, hints, filter_):
   @rtype: callable
 
   """
-  return _FilterCompilerHelper(fields)(hints, filter_)
+  return _FilterCompilerHelper(fields)(hints, qfilter)
 
 
 class Query:
-  def __init__(self, fieldlist, selected, filter_=None, namefield=None):
+  def __init__(self, fieldlist, selected, qfilter=None, namefield=None):
     """Initializes this class.
 
     The field definition is a dictionary with the field's name as a key and a
@@ -620,7 +644,7 @@ class Query:
     self._requested_names = None
     self._filter_datakinds = frozenset()
 
-    if filter_ is not None:
+    if qfilter is not None:
       # Collect requested names if wanted
       if namefield:
         hints = _FilterHints(namefield)
@@ -628,7 +652,7 @@ class Query:
         hints = None
 
       # Build filter function
-      self._filter_fn = _CompileFilter(fieldlist, hints, filter_)
+      self._filter_fn = _CompileFilter(fieldlist, hints, qfilter)
       if hints:
         self._requested_names = hints.RequestedNames()
         self._filter_datakinds = hints.ReferencedData()
@@ -763,7 +787,23 @@ def _VerifyResultRow(fields, row):
     elif value is not None:
       errs.append("abnormal field %s has a non-None value" % fdef.name)
   assert not errs, ("Failed validation: %s in row %s" %
-                    (utils.CommaJoin(errors), row))
+                    (utils.CommaJoin(errs), row))
+
+
+def _FieldDictKey((fdef, _, flags, fn)):
+  """Generates key for field dictionary.
+
+  """
+  assert fdef.name and fdef.title, "Name and title are required"
+  assert FIELD_NAME_RE.match(fdef.name)
+  assert TITLE_RE.match(fdef.title)
+  assert (DOC_RE.match(fdef.doc) and len(fdef.doc.splitlines()) == 1 and
+          fdef.doc.strip() == fdef.doc), \
+         "Invalid description for field '%s'" % fdef.name
+  assert callable(fn)
+  assert (flags & ~QFF_ALL) == 0, "Unknown flags for field '%s'" % fdef.name
+
+  return fdef.name
 
 
 def _PrepareFieldList(fields, aliases):
@@ -787,23 +827,7 @@ def _PrepareFieldList(fields, aliases):
                                       for (fdef, _, _, _) in fields)
     assert not duplicates, "Duplicate title(s) found: %r" % duplicates
 
-  result = {}
-
-  for field in fields:
-    (fdef, _, flags, fn) = field
-
-    assert fdef.name and fdef.title, "Name and title are required"
-    assert FIELD_NAME_RE.match(fdef.name)
-    assert TITLE_RE.match(fdef.title)
-    assert (DOC_RE.match(fdef.doc) and len(fdef.doc.splitlines()) == 1 and
-            fdef.doc.strip() == fdef.doc), \
-           "Invalid description for field '%s'" % fdef.name
-    assert callable(fn)
-    assert fdef.name not in result, \
-           "Duplicate field name '%s' found" % fdef.name
-    assert (flags & ~QFF_ALL) == 0, "Unknown flags for field '%s'" % fdef.name
-
-    result[fdef.name] = field
+  result = utils.SequenceToDict(fields, key=_FieldDictKey)
 
   for alias, target in aliases:
     assert alias not in result, "Alias %s overrides an existing field" % alias
@@ -868,6 +892,20 @@ def _MakeField(name, title, kind, doc):
                                       doc=doc)
 
 
+def _StaticValueInner(value, ctx, _): # pylint: disable=W0613
+  """Returns a static value.
+
+  """
+  return value
+
+
+def _StaticValue(value):
+  """Prepares a function to return a static value.
+
+  """
+  return compat.partial(_StaticValueInner, value)
+
+
 def _GetNodeRole(node, master_name):
   """Determine node role.
 
@@ -899,6 +937,34 @@ def _GetItemAttr(attr):
   return lambda _, item: getter(item)
 
 
+def _GetNDParam(name):
+  """Return a field function to return an ND parameter out of the context.
+
+  """
+  def _helper(ctx, _):
+    if ctx.ndparams is None:
+      return _FS_UNAVAIL
+    else:
+      return ctx.ndparams.get(name, None)
+  return _helper
+
+
+def _BuildNDFields(is_group):
+  """Builds all the ndparam fields.
+
+  @param is_group: whether this is called at group or node level
+
+  """
+  if is_group:
+    field_kind = GQ_CONFIG
+  else:
+    field_kind = NQ_GROUP
+  return [(_MakeField("ndp/%s" % name, NDP_TITLE.get(name, "ndp/%s" % name),
+                      _VTToQFT[kind], "The \"%s\" node parameter" % name),
+           field_kind, 0, _GetNDParam(name))
+          for name, kind in constants.NDS_PARAMETER_TYPES.items()]
+
+
 def _ConvWrapInner(convert, fn, ctx, item):
   """Wrapper for converting values.
 
@@ -982,6 +1048,7 @@ class NodeQueryData:
 
     # Used for individual rows
     self.curlive_data = None
+    self.ndparams = None
 
   def __iter__(self):
     """Iterate over all nodes.
@@ -991,6 +1058,11 @@ class NodeQueryData:
 
     """
     for node in self.nodes:
+      group = self.groups.get(node.group, None)
+      if group is None:
+        self.ndparams = None
+      else:
+        self.ndparams = self.cluster.FillND(node, group)
       if self.live_data:
         self.curlive_data = self.live_data.get(node.name, None)
       else:
@@ -1140,6 +1212,32 @@ def _GetLiveNodeField(field, kind, ctx, node):
     return _FS_UNAVAIL
 
 
+def _GetNodeHvState(_, node):
+  """Converts node's hypervisor state for query result.
+
+  """
+  hv_state = node.hv_state
+
+  if hv_state is None:
+    return _FS_UNAVAIL
+
+  return dict((name, value.ToDict()) for (name, value) in hv_state.items())
+
+
+def _GetNodeDiskState(_, node):
+  """Converts node's disk state for query result.
+
+  """
+  disk_state = node.disk_state
+
+  if disk_state is None:
+    return _FS_UNAVAIL
+
+  return dict((disk_kind, dict((name, value.ToDict())
+                               for (name, value) in kind_state.items()))
+              for (disk_kind, kind_state) in disk_state.items())
+
+
 def _BuildNodeFields():
   """Builds list of fields for node queries.
 
@@ -1166,8 +1264,14 @@ def _BuildNodeFields():
     (_MakeField("custom_ndparams", "CustomNodeParameters", QFT_OTHER,
                 "Custom node parameters"),
       NQ_GROUP, 0, _GetItemAttr("ndparams")),
+    (_MakeField("hv_state", "HypervisorState", QFT_OTHER, "Hypervisor state"),
+     NQ_CONFIG, 0, _GetNodeHvState),
+    (_MakeField("disk_state", "DiskState", QFT_OTHER, "Disk state"),
+     NQ_CONFIG, 0, _GetNodeDiskState),
     ]
 
+  fields.extend(_BuildNDFields(False))
+
   # Node role
   role_values = (constants.NR_MASTER, constants.NR_MCANDIDATE,
                  constants.NR_REGULAR, constants.NR_DRAINED,
@@ -1348,15 +1452,17 @@ def _GetInstStatus(ctx, inst):
   if bool(ctx.live_data.get(inst.name)):
     if inst.name in ctx.wrongnode_inst:
       return constants.INSTST_WRONGNODE
-    elif inst.admin_up:
+    elif inst.admin_state == constants.ADMINST_UP:
       return constants.INSTST_RUNNING
     else:
       return constants.INSTST_ERRORUP
 
-  if inst.admin_up:
+  if inst.admin_state == constants.ADMINST_UP:
     return constants.INSTST_ERRORDOWN
+  elif inst.admin_state == constants.ADMINST_DOWN:
+    return constants.INSTST_ADMINDOWN
 
-  return constants.INSTST_ADMINDOWN
+  return constants.INSTST_ADMINOFFLINE
 
 
 def _GetInstDiskSize(index):
@@ -1624,7 +1730,8 @@ def _GetInstanceParameterFields():
   # TODO: Consider moving titles closer to constants
   be_title = {
     constants.BE_AUTO_BALANCE: "Auto_balance",
-    constants.BE_MEMORY: "ConfigMemory",
+    constants.BE_MAXMEM: "ConfigMaxMem",
+    constants.BE_MINMEM: "ConfigMinMem",
     constants.BE_VCPUS: "ConfigVCPUs",
     }
 
@@ -1775,10 +1882,12 @@ def _BuildInstanceFields():
      IQ_NODES, 0,
      lambda ctx, inst: map(compat.partial(_GetInstNodeGroup, ctx, None),
                            inst.secondary_nodes)),
-    (_MakeField("admin_state", "Autostart", QFT_BOOL,
-                "Desired state of instance (if set, the instance should be"
-                " up)"),
-     IQ_CONFIG, 0, _GetItemAttr("admin_up")),
+    (_MakeField("admin_state", "InstanceState", QFT_TEXT,
+                "Desired state of instance"),
+     IQ_CONFIG, 0, _GetItemAttr("admin_state")),
+    (_MakeField("admin_up", "Autostart", QFT_BOOL,
+                "Desired state of instance"),
+     IQ_CONFIG, 0, lambda ctx, inst: inst.admin_state == constants.ADMINST_UP),
     (_MakeField("tags", "Tags", QFT_OTHER, "Tags"), IQ_CONFIG, 0,
      lambda ctx, inst: list(inst.GetTags())),
     (_MakeField("console", "Console", QFT_OTHER,
@@ -1808,15 +1917,16 @@ def _BuildInstanceFields():
   status_values = (constants.INSTST_RUNNING, constants.INSTST_ADMINDOWN,
                    constants.INSTST_WRONGNODE, constants.INSTST_ERRORUP,
                    constants.INSTST_ERRORDOWN, constants.INSTST_NODEDOWN,
-                   constants.INSTST_NODEOFFLINE)
+                   constants.INSTST_NODEOFFLINE, constants.INSTST_ADMINOFFLINE)
   status_doc = ("Instance status; \"%s\" if instance is set to be running"
                 " and actually is, \"%s\" if instance is stopped and"
                 " is not running, \"%s\" if instance running, but not on its"
                 " designated primary node, \"%s\" if instance should be"
                 " stopped, but is actually running, \"%s\" if instance should"
                 " run, but doesn't, \"%s\" if instance's primary node is down,"
-                " \"%s\" if instance's primary node is marked offline" %
-                status_values)
+                " \"%s\" if instance's primary node is marked offline,"
+                " \"%s\" if instance is offline and does not use dynamic"
+                " resources" % status_values)
   fields.append((_MakeField("status", "Status", QFT_TEXT, status_doc),
                  IQ_LIVE, 0, _GetInstStatus))
   assert set(status_values) == constants.INSTST_ALL, \
@@ -1831,6 +1941,7 @@ def _BuildInstanceFields():
 
   aliases = [
     ("vcpus", "be/vcpus"),
+    ("be/memory", "be/maxmem"),
     ("sda_size", "disk.size/0"),
     ("sdb_size", "disk.size/1"),
     ] + network_aliases
@@ -1904,25 +2015,46 @@ class GroupQueryData:
   """Data container for node group data queries.
 
   """
-  def __init__(self, groups, group_to_nodes, group_to_instances):
+  def __init__(self, cluster, groups, group_to_nodes, group_to_instances,
+               want_diskparams):
     """Initializes this class.
 
+    @param cluster: Cluster object
     @param groups: List of node group objects
     @type group_to_nodes: dict; group UUID as key
     @param group_to_nodes: Per-group list of nodes
     @type group_to_instances: dict; group UUID as key
     @param group_to_instances: Per-group list of (primary) instances
+    @type want_diskparams: bool
+    @param want_diskparams: Whether diskparamters should be calculated
 
     """
     self.groups = groups
     self.group_to_nodes = group_to_nodes
     self.group_to_instances = group_to_instances
+    self.cluster = cluster
+    self.want_diskparams = want_diskparams
+
+    # Used for individual rows
+    self.group_ipolicy = None
+    self.ndparams = None
+    self.group_dp = None
 
   def __iter__(self):
     """Iterate over all node groups.
 
+    This function has side-effects and only one instance of the resulting
+    generator should be used at a time.
+
     """
-    return iter(self.groups)
+    for group in self.groups:
+      self.group_ipolicy = self.cluster.SimpleFillIPolicy(group.ipolicy)
+      self.ndparams = self.cluster.SimpleFillND(group.ndparams)
+      if self.want_diskparams:
+        self.group_dp = self.cluster.SimpleFillDP(group.diskparams)
+      else:
+        self.group_dp = None
+      yield group
 
 
 _GROUP_SIMPLE_FIELDS = {
@@ -1930,7 +2062,6 @@ _GROUP_SIMPLE_FIELDS = {
   "name": ("Group", QFT_TEXT, "Group name"),
   "serial_no": ("SerialNo", QFT_NUMBER, _SERIAL_NO_DOC % "Group"),
   "uuid": ("UUID", QFT_TEXT, "Group UUID"),
-  "ndparams": ("NDParams", QFT_OTHER, "Node parameters"),
   }
 
 
@@ -1974,8 +2105,29 @@ def _BuildGroupFields():
   fields.extend([
     (_MakeField("tags", "Tags", QFT_OTHER, "Tags"), GQ_CONFIG, 0,
      lambda ctx, group: list(group.GetTags())),
+    (_MakeField("ipolicy", "InstancePolicy", QFT_OTHER,
+                "Instance policy limitations (merged)"),
+     GQ_CONFIG, 0, lambda ctx, _: ctx.group_ipolicy),
+    (_MakeField("custom_ipolicy", "CustomInstancePolicy", QFT_OTHER,
+                "Custom instance policy limitations"),
+     GQ_CONFIG, 0, _GetItemAttr("ipolicy")),
+    (_MakeField("custom_ndparams", "CustomNDParams", QFT_OTHER,
+                "Custom node parameters"),
+     GQ_CONFIG, 0, _GetItemAttr("ndparams")),
+    (_MakeField("ndparams", "NDParams", QFT_OTHER,
+                "Node parameters"),
+     GQ_CONFIG, 0, lambda ctx, _: ctx.ndparams),
+    (_MakeField("diskparams", "DiskParameters", QFT_OTHER,
+                "Disk parameters (merged)"),
+     GQ_DISKPARAMS, 0, lambda ctx, _: ctx.group_dp),
+    (_MakeField("custom_diskparams", "CustomDiskParameters", QFT_OTHER,
+                "Custom disk parameters"),
+     GQ_CONFIG, 0, _GetItemAttr("diskparams")),
     ])
 
+  # ND parameters
+  fields.extend(_BuildNDFields(True))
+
   fields.extend(_GetItemTimestampFields(GQ_CONFIG))
 
   return _PrepareFieldList(fields, [])
@@ -2028,6 +2180,230 @@ def _BuildOsFields():
   return _PrepareFieldList(fields, [])
 
 
+def _JobUnavailInner(fn, ctx, (job_id, job)): # pylint: disable=W0613
+  """Return L{_FS_UNAVAIL} if job is None.
+
+  When listing specifc jobs (e.g. "gnt-job list 1 2 3"), a job may not be
+  found, in which case this function converts it to L{_FS_UNAVAIL}.
+
+  """
+  if job is None:
+    return _FS_UNAVAIL
+  else:
+    return fn(job)
+
+
+def _JobUnavail(inner):
+  """Wrapper for L{_JobUnavailInner}.
+
+  """
+  return compat.partial(_JobUnavailInner, inner)
+
+
+def _PerJobOpInner(fn, job):
+  """Executes a function per opcode in a job.
+
+  """
+  return map(fn, job.ops)
+
+
+def _PerJobOp(fn):
+  """Wrapper for L{_PerJobOpInner}.
+
+  """
+  return _JobUnavail(compat.partial(_PerJobOpInner, fn))
+
+
+def _JobTimestampInner(fn, job):
+  """Converts unavailable timestamp to L{_FS_UNAVAIL}.
+
+  """
+  timestamp = fn(job)
+
+  if timestamp is None:
+    return _FS_UNAVAIL
+  else:
+    return timestamp
+
+
+def _JobTimestamp(fn):
+  """Wrapper for L{_JobTimestampInner}.
+
+  """
+  return _JobUnavail(compat.partial(_JobTimestampInner, fn))
+
+
+def _BuildJobFields():
+  """Builds list of fields for job queries.
+
+  """
+  fields = [
+    (_MakeField("id", "ID", QFT_TEXT, "Job ID"),
+     None, 0, lambda _, (job_id, job): job_id),
+    (_MakeField("status", "Status", QFT_TEXT, "Job status"),
+     None, 0, _JobUnavail(lambda job: job.CalcStatus())),
+    (_MakeField("priority", "Priority", QFT_NUMBER,
+                ("Current job priority (%s to %s)" %
+                 (constants.OP_PRIO_LOWEST, constants.OP_PRIO_HIGHEST))),
+     None, 0, _JobUnavail(lambda job: job.CalcPriority())),
+    (_MakeField("ops", "OpCodes", QFT_OTHER, "List of all opcodes"),
+     None, 0, _PerJobOp(lambda op: op.input.__getstate__())),
+    (_MakeField("opresult", "OpCode_result", QFT_OTHER,
+                "List of opcodes results"),
+     None, 0, _PerJobOp(operator.attrgetter("result"))),
+    (_MakeField("opstatus", "OpCode_status", QFT_OTHER,
+                "List of opcodes status"),
+     None, 0, _PerJobOp(operator.attrgetter("status"))),
+    (_MakeField("oplog", "OpCode_log", QFT_OTHER,
+                "List of opcode output logs"),
+     None, 0, _PerJobOp(operator.attrgetter("log"))),
+    (_MakeField("opstart", "OpCode_start", QFT_OTHER,
+                "List of opcode start timestamps (before acquiring locks)"),
+     None, 0, _PerJobOp(operator.attrgetter("start_timestamp"))),
+    (_MakeField("opexec", "OpCode_exec", QFT_OTHER,
+                "List of opcode execution start timestamps (after acquiring"
+                " locks)"),
+     None, 0, _PerJobOp(operator.attrgetter("exec_timestamp"))),
+    (_MakeField("opend", "OpCode_end", QFT_OTHER,
+                "List of opcode execution end timestamps"),
+     None, 0, _PerJobOp(operator.attrgetter("end_timestamp"))),
+    (_MakeField("oppriority", "OpCode_prio", QFT_OTHER,
+                "List of opcode priorities"),
+     None, 0, _PerJobOp(operator.attrgetter("priority"))),
+    (_MakeField("received_ts", "Received", QFT_OTHER,
+                "Timestamp of when job was received"),
+     None, 0, _JobTimestamp(operator.attrgetter("received_timestamp"))),
+    (_MakeField("start_ts", "Start", QFT_OTHER,
+                "Timestamp of job start"),
+     None, 0, _JobTimestamp(operator.attrgetter("start_timestamp"))),
+    (_MakeField("end_ts", "End", QFT_OTHER,
+                "Timestamp of job end"),
+     None, 0, _JobTimestamp(operator.attrgetter("end_timestamp"))),
+    (_MakeField("summary", "Summary", QFT_OTHER,
+                "List of per-opcode summaries"),
+     None, 0, _PerJobOp(lambda op: op.input.Summary())),
+    ]
+
+  return _PrepareFieldList(fields, [])
+
+
+def _GetExportName(_, (node_name, expname)): # pylint: disable=W0613
+  """Returns an export name if available.
+
+  """
+  if expname is None:
+    return _FS_UNAVAIL
+  else:
+    return expname
+
+
+def _BuildExportFields():
+  """Builds list of fields for exports.
+
+  """
+  fields = [
+    (_MakeField("node", "Node", QFT_TEXT, "Node name"),
+     None, QFF_HOSTNAME, lambda _, (node_name, expname): node_name),
+    (_MakeField("export", "Export", QFT_TEXT, "Export name"),
+     None, 0, _GetExportName),
+    ]
+
+  return _PrepareFieldList(fields, [])
+
+
+_CLUSTER_VERSION_FIELDS = {
+  "software_version": ("SoftwareVersion", QFT_TEXT, constants.RELEASE_VERSION,
+                       "Software version"),
+  "protocol_version": ("ProtocolVersion", QFT_NUMBER,
+                       constants.PROTOCOL_VERSION,
+                       "RPC protocol version"),
+  "config_version": ("ConfigVersion", QFT_NUMBER, constants.CONFIG_VERSION,
+                     "Configuration format version"),
+  "os_api_version": ("OsApiVersion", QFT_NUMBER, max(constants.OS_API_VERSIONS),
+                     "API version for OS template scripts"),
+  "export_version": ("ExportVersion", QFT_NUMBER, constants.EXPORT_VERSION,
+                     "Import/export file format version"),
+  }
+
+
+_CLUSTER_SIMPLE_FIELDS = {
+  "cluster_name": ("Name", QFT_TEXT, QFF_HOSTNAME, "Cluster name"),
+  "master_node": ("Master", QFT_TEXT, QFF_HOSTNAME, "Master node name"),
+  "volume_group_name": ("VgName", QFT_TEXT, 0, "LVM volume group name"),
+  }
+
+
+class ClusterQueryData:
+  def __init__(self, cluster, drain_flag, watcher_pause):
+    """Initializes this class.
+
+    @type cluster: L{objects.Cluster}
+    @param cluster: Instance of cluster object
+    @type drain_flag: bool
+    @param drain_flag: Whether job queue is drained
+    @type watcher_pause: number
+    @param watcher_pause: Until when watcher is paused (Unix timestamp)
+
+    """
+    self._cluster = cluster
+    self.drain_flag = drain_flag
+    self.watcher_pause = watcher_pause
+
+  def __iter__(self):
+    return iter([self._cluster])
+
+
+def _ClusterWatcherPause(ctx, _):
+  """Returns until when watcher is paused (if available).
+
+  """
+  if ctx.watcher_pause is None:
+    return _FS_UNAVAIL
+  else:
+    return ctx.watcher_pause
+
+
+def _BuildClusterFields():
+  """Builds list of fields for cluster information.
+
+  """
+  fields = [
+    (_MakeField("tags", "Tags", QFT_OTHER, "Tags"), CQ_CONFIG, 0,
+     lambda ctx, cluster: list(cluster.GetTags())),
+    (_MakeField("architecture", "ArchInfo", QFT_OTHER,
+                "Architecture information"), None, 0,
+     lambda ctx, _: runtime.GetArchInfo()),
+    (_MakeField("drain_flag", "QueueDrained", QFT_BOOL,
+                "Flag whether job queue is drained"), CQ_QUEUE_DRAINED, 0,
+     lambda ctx, _: ctx.drain_flag),
+    (_MakeField("watcher_pause", "WatcherPause", QFT_TIMESTAMP,
+                "Until when watcher is paused"), CQ_WATCHER_PAUSE, 0,
+     _ClusterWatcherPause),
+    ]
+
+  # Simple fields
+  fields.extend([
+    (_MakeField(name, title, kind, doc), CQ_CONFIG, flags, _GetItemAttr(name))
+    for (name, (title, kind, flags, doc)) in _CLUSTER_SIMPLE_FIELDS.items()
+    ])
+
+  # Version fields
+  fields.extend([
+    (_MakeField(name, title, kind, doc), None, 0, _StaticValue(value))
+    for (name, (title, kind, value, doc)) in _CLUSTER_VERSION_FIELDS.items()
+    ])
+
+  # Add timestamps
+  fields.extend(_GetItemTimestampFields(CQ_CONFIG))
+
+  return _PrepareFieldList(fields, [
+    ("name", "cluster_name"),
+    ])
+
+
+#: Fields for cluster information
+CLUSTER_FIELDS = _BuildClusterFields()
+
 #: Fields available for node queries
 NODE_FIELDS = _BuildNodeFields()
 
@@ -2043,13 +2419,22 @@ GROUP_FIELDS = _BuildGroupFields()
 #: Fields available for operating system queries
 OS_FIELDS = _BuildOsFields()
 
+#: Fields available for job queries
+JOB_FIELDS = _BuildJobFields()
+
+#: Fields available for exports
+EXPORT_FIELDS = _BuildExportFields()
+
 #: All available resources
 ALL_FIELDS = {
+  constants.QR_CLUSTER: CLUSTER_FIELDS,
   constants.QR_INSTANCE: INSTANCE_FIELDS,
   constants.QR_NODE: NODE_FIELDS,
   constants.QR_LOCK: LOCK_FIELDS,
   constants.QR_GROUP: GROUP_FIELDS,
   constants.QR_OS: OS_FIELDS,
+  constants.QR_JOB: JOB_FIELDS,
+  constants.QR_EXPORT: EXPORT_FIELDS,
   }
 
 #: All available field lists
index 1daed10..b44d454 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007, 2008 Google Inc.
+# Copyright (C) 2006, 2007, 2008, 2012 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
@@ -32,15 +32,33 @@ import logging
 from ganeti import luxi
 from ganeti import rapi
 from ganeti import http
-from ganeti import ssconf
-from ganeti import constants
-from ganeti import opcodes
 from ganeti import errors
+from ganeti import compat
 
 
 # Dummy value to detect unchanged parameters
 _DEFAULT = object()
 
+#: Supported HTTP methods
+_SUPPORTED_METHODS = frozenset([
+  http.HTTP_DELETE,
+  http.HTTP_GET,
+  http.HTTP_POST,
+  http.HTTP_PUT,
+  ])
+
+
+def _BuildOpcodeAttributes():
+  """Builds list of attributes used for per-handler opcodes.
+
+  """
+  return [(method, "%s_OPCODE" % method, "%s_RENAME" % method,
+           "Get%sOpInput" % method.capitalize())
+          for method in _SUPPORTED_METHODS]
+
+
+_OPCODE_ATTRS = _BuildOpcodeAttributes()
+
 
 def BuildUriList(ids, uri_format, uri_fields=("name", "uri")):
   """Builds a URI list as used by index resources.
@@ -91,49 +109,6 @@ def MapFields(names, data):
   return dict(zip(names, data))
 
 
-def _Tags_GET(kind, name):
-  """Helper function to retrieve tags.
-
-  """
-  if kind in (constants.TAG_INSTANCE,
-              constants.TAG_NODEGROUP,
-              constants.TAG_NODE):
-    if not name:
-      raise http.HttpBadRequest("Missing name on tag request")
-    cl = GetClient()
-    if kind == constants.TAG_INSTANCE:
-      fn = cl.QueryInstances
-    elif kind == constants.TAG_NODEGROUP:
-      fn = cl.QueryGroups
-    else:
-      fn = cl.QueryNodes
-    result = fn(names=[name], fields=["tags"], use_locking=False)
-    if not result or not result[0]:
-      raise http.HttpBadGateway("Invalid response from tag query")
-    tags = result[0][0]
-  elif kind == constants.TAG_CLUSTER:
-    ssc = ssconf.SimpleStore()
-    tags = ssc.GetClusterTags()
-
-  return list(tags)
-
-
-def _Tags_PUT(kind, tags, name, dry_run):
-  """Helper function to set tags.
-
-  """
-  return SubmitJob([opcodes.OpTagsSet(kind=kind, name=name,
-                                      tags=tags, dry_run=dry_run)])
-
-
-def _Tags_DELETE(kind, tags, name, dry_run):
-  """Helper function to delete tags.
-
-  """
-  return SubmitJob([opcodes.OpTagsDel(kind=kind, name=name,
-                                      tags=tags, dry_run=dry_run)])
-
-
 def MapBulkFields(itemslist, fields):
   """Map value to field name in to one dictionary.
 
@@ -231,35 +206,6 @@ def FillOpcode(opcls, body, static, rename=None):
   return op
 
 
-def SubmitJob(op, cl=None):
-  """Generic wrapper for submit job, for better http compatibility.
-
-  @type op: list
-  @param op: the list of opcodes for the job
-  @type cl: None or luxi.Client
-  @param cl: optional luxi client to use
-  @rtype: string
-  @return: the job ID
-
-  """
-  try:
-    if cl is None:
-      cl = GetClient()
-    return cl.SubmitJob(op)
-  except errors.JobQueueFull:
-    raise http.HttpServiceUnavailable("Job queue is full, needs archiving")
-  except errors.JobQueueDrainError:
-    raise http.HttpServiceUnavailable("Job queue is drained, cannot submit")
-  except luxi.NoMasterError, err:
-    raise http.HttpBadGateway("Master seems to be unreachable: %s" % str(err))
-  except luxi.PermissionError:
-    raise http.HttpInternalServerError("Internal error: no permission to"
-                                       " connect to the master daemon")
-  except luxi.TimeoutError, err:
-    raise http.HttpGatewayTimeout("Timeout while talking to the master"
-                                  " daemon. Error: %s" % str(err))
-
-
 def HandleItemQueryErrors(fn, *args, **kwargs):
   """Converts errors when querying a single item.
 
@@ -273,19 +219,6 @@ def HandleItemQueryErrors(fn, *args, **kwargs):
     raise
 
 
-def GetClient():
-  """Geric wrapper for luxi.Client(), for better http compatiblity.
-
-  """
-  try:
-    return luxi.Client()
-  except luxi.NoMasterError, err:
-    raise http.HttpBadGateway("Master seems to unreachable: %s" % str(err))
-  except luxi.PermissionError:
-    raise http.HttpInternalServerError("Internal error: no permission to"
-                                       " connect to the master daemon")
-
-
 def FeedbackFn(msg):
   """Feedback logging function for jobs.
 
@@ -346,7 +279,7 @@ def CheckParameter(data, name, default=_DEFAULT, exptype=_DEFAULT):
   return CheckType(value, exptype, "'%s' parameter" % name)
 
 
-class R_Generic(object):
+class ResourceBase(object):
   """Generic class for resources.
 
   """
@@ -356,17 +289,24 @@ class R_Generic(object):
   POST_ACCESS = [rapi.RAPI_ACCESS_WRITE]
   DELETE_ACCESS = [rapi.RAPI_ACCESS_WRITE]
 
-  def __init__(self, items, queryargs, req):
+  def __init__(self, items, queryargs, req, _client_cls=None):
     """Generic resource constructor.
 
     @param items: a list with variables encoded in the URL
     @param queryargs: a dictionary with additional options from URL
+    @param req: Request context
+    @param _client_cls: L{luxi} client class (unittests only)
 
     """
     self.items = items
     self.queryargs = queryargs
     self._req = req
 
+    if _client_cls is None:
+      _client_cls = luxi.Client
+
+    self._client_cls = _client_cls
+
   def _GetRequestBody(self):
     """Returns the body data.
 
@@ -441,3 +381,135 @@ class R_Generic(object):
 
     """
     return bool(self._checkIntVariable("dry-run"))
+
+  def GetClient(self):
+    """Wrapper for L{luxi.Client} with HTTP-specific error handling.
+
+    """
+    # Could be a function, pylint: disable=R0201
+    try:
+      return self._client_cls()
+    except luxi.NoMasterError, err:
+      raise http.HttpBadGateway("Can't connect to master daemon: %s" % err)
+    except luxi.PermissionError:
+      raise http.HttpInternalServerError("Internal error: no permission to"
+                                         " connect to the master daemon")
+
+  def SubmitJob(self, op, cl=None):
+    """Generic wrapper for submit job, for better http compatibility.
+
+    @type op: list
+    @param op: the list of opcodes for the job
+    @type cl: None or luxi.Client
+    @param cl: optional luxi client to use
+    @rtype: string
+    @return: the job ID
+
+    """
+    if cl is None:
+      cl = self.GetClient()
+    try:
+      return cl.SubmitJob(op)
+    except errors.JobQueueFull:
+      raise http.HttpServiceUnavailable("Job queue is full, needs archiving")
+    except errors.JobQueueDrainError:
+      raise http.HttpServiceUnavailable("Job queue is drained, cannot submit")
+    except luxi.NoMasterError, err:
+      raise http.HttpBadGateway("Master seems to be unreachable: %s" % err)
+    except luxi.PermissionError:
+      raise http.HttpInternalServerError("Internal error: no permission to"
+                                         " connect to the master daemon")
+    except luxi.TimeoutError, err:
+      raise http.HttpGatewayTimeout("Timeout while talking to the master"
+                                    " daemon: %s" % err)
+
+
+def GetResourceOpcodes(cls):
+  """Returns all opcodes used by a resource.
+
+  """
+  return frozenset(filter(None, (getattr(cls, op_attr, None)
+                                 for (_, op_attr, _, _) in _OPCODE_ATTRS)))
+
+
+class _MetaOpcodeResource(type):
+  """Meta class for RAPI resources.
+
+  """
+  def __call__(mcs, *args, **kwargs):
+    """Instantiates class and patches it for use by the RAPI daemon.
+
+    """
+    # Access to private attributes of a client class, pylint: disable=W0212
+    obj = type.__call__(mcs, *args, **kwargs)
+
+    for (method, op_attr, rename_attr, fn_attr) in _OPCODE_ATTRS:
+      if hasattr(obj, method):
+        # If the method handler is already defined, "*_RENAME" or "Get*OpInput"
+        # shouldn't be (they're only used by the automatically generated
+        # handler)
+        assert not hasattr(obj, rename_attr)
+        assert not hasattr(obj, fn_attr)
+      else:
+        # Try to generate handler method on handler instance
+        try:
+          opcode = getattr(obj, op_attr)
+        except AttributeError:
+          pass
+        else:
+          setattr(obj, method,
+                  compat.partial(obj._GenericHandler, opcode,
+                                 getattr(obj, rename_attr, None),
+                                 getattr(obj, fn_attr, obj._GetDefaultData)))
+
+    return obj
+
+
+class OpcodeResource(ResourceBase):
+  """Base class for opcode-based RAPI resources.
+
+  Instances of this class automatically gain handler functions through
+  L{_MetaOpcodeResource} for any method for which a C{$METHOD$_OPCODE} variable
+  is defined at class level. Subclasses can define a C{Get$Method$OpInput}
+  method to do their own opcode input processing (e.g. for static values). The
+  C{$METHOD$_RENAME} variable defines which values are renamed (see
+  L{baserlib.FillOpcode}).
+
+  @cvar GET_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
+    automatically generate a GET handler submitting the opcode
+  @cvar GET_RENAME: Set this to rename parameters in the GET handler (see
+    L{baserlib.FillOpcode})
+  @ivar GetGetOpInput: Define this to override the default method for
+    getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
+
+  @cvar PUT_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
+    automatically generate a PUT handler submitting the opcode
+  @cvar PUT_RENAME: Set this to rename parameters in the PUT handler (see
+    L{baserlib.FillOpcode})
+  @ivar GetPutOpInput: Define this to override the default method for
+    getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
+
+  @cvar POST_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
+    automatically generate a POST handler submitting the opcode
+  @cvar POST_RENAME: Set this to rename parameters in the DELETE handler (see
+    L{baserlib.FillOpcode})
+  @ivar GetPostOpInput: Define this to override the default method for
+    getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
+
+  @cvar DELETE_OPCODE: Set this to a class derived from L{opcodes.OpCode} to
+    automatically generate a GET handler submitting the opcode
+  @cvar DELETE_RENAME: Set this to rename parameters in the DELETE handler (see
+    L{baserlib.FillOpcode})
+  @ivar GetDeleteOpInput: Define this to override the default method for
+    getting opcode parameters (see L{baserlib.OpcodeResource._GetDefaultData})
+
+  """
+  __metaclass__ = _MetaOpcodeResource
+
+  def _GetDefaultData(self):
+    return (self.request_body, None)
+
+  def _GenericHandler(self, opcode, rename, fn):
+    (body, static) = fn()
+    op = FillOpcode(opcode, body, static, rename=rename)
+    return self.SubmitJob([op])
index 4566fca..b5a4df5 100644 (file)
@@ -97,17 +97,20 @@ JOB_STATUS_WAITLOCK = JOB_STATUS_WAITING
 
 # Internal constants
 _REQ_DATA_VERSION_FIELD = "__version__"
-_INST_CREATE_REQV1 = "instance-create-reqv1"
-_INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
-_NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
-_NODE_EVAC_RES1 = "node-evac-res1"
-_INST_NIC_PARAMS = frozenset(["mac", "ip", "mode", "link"])
-_INST_CREATE_V0_DISK_PARAMS = frozenset(["size"])
-_INST_CREATE_V0_PARAMS = frozenset([
-  "os", "pnode", "snode", "iallocator", "start", "ip_check", "name_check",
-  "hypervisor", "file_storage_dir", "file_driver", "dry_run",
-  ])
-_INST_CREATE_V0_DPARAMS = frozenset(["beparams", "hvparams"])
+_QPARAM_DRY_RUN = "dry-run"
+_QPARAM_FORCE = "force"
+
+# Feature strings
+INST_CREATE_REQV1 = "instance-create-reqv1"
+INST_REINSTALL_REQV1 = "instance-reinstall-reqv1"
+NODE_MIGRATE_REQV1 = "node-migrate-reqv1"
+NODE_EVAC_RES1 = "node-evac-res1"
+
+# Old feature constant names in case they're references by users of this module
+_INST_CREATE_REQV1 = INST_CREATE_REQV1
+_INST_REINSTALL_REQV1 = INST_REINSTALL_REQV1
+_NODE_MIGRATE_REQV1 = NODE_MIGRATE_REQV1
+_NODE_EVAC_RES1 = NODE_EVAC_RES1
 
 # Older pycURL versions don't have all error constants
 try:
@@ -130,20 +133,54 @@ class Error(Exception):
   pass
 
 
-class CertificateError(Error):
+class GanetiApiError(Error):
+  """Generic error raised from Ganeti API.
+
+  """
+  def __init__(self, msg, code=None):
+    Error.__init__(self, msg)
+    self.code = code
+
+
+class CertificateError(GanetiApiError):
   """Raised when a problem is found with the SSL certificate.
 
   """
   pass
 
 
-class GanetiApiError(Error):
-  """Generic error raised from Ganeti API.
+def _AppendIf(container, condition, value):
+  """Appends to a list if a condition evaluates to truth.
 
   """
-  def __init__(self, msg, code=None):
-    Error.__init__(self, msg)
-    self.code = code
+  if condition:
+    container.append(value)
+
+  return condition
+
+
+def _AppendDryRunIf(container, condition):
+  """Appends a "dry-run" parameter if a condition evaluates to truth.
+
+  """
+  return _AppendIf(container, condition, (_QPARAM_DRY_RUN, 1))
+
+
+def _AppendForceIf(container, condition):
+  """Appends a "force" parameter if a condition evaluates to truth.
+
+  """
+  return _AppendIf(container, condition, (_QPARAM_FORCE, 1))
+
+
+def _SetItemIf(container, condition, item, value):
+  """Sets an item if a condition evaluates to truth.
+
+  """
+  if condition:
+    container[item] = value
+
+  return condition
 
 
 def UsesRapiClient(fn):
@@ -219,6 +256,9 @@ def GenericCurlConfig(verbose=False, use_signal=False,
     lcsslver = sslver.lower()
     if lcsslver.startswith("openssl/"):
       pass
+    elif lcsslver.startswith("nss/"):
+      # TODO: investigate compatibility beyond a simple test
+      pass
     elif lcsslver.startswith("gnutls/"):
       if capath:
         raise Error("cURL linked against GnuTLS has no support for a"
@@ -433,9 +473,10 @@ class GanetiRapiClient(object): # pylint: disable=R0904
         curl.perform()
       except pycurl.error, err:
         if err.args[0] in _CURL_SSL_CERT_ERRORS:
-          raise CertificateError("SSL certificate error %s" % err)
+          raise CertificateError("SSL certificate error %s" % err,
+                                 code=err.args[0])
 
-        raise GanetiApiError(str(err))
+        raise GanetiApiError(str(err), code=err.args[0])
     finally:
       # Reset settings to not keep references to large objects in memory
       # between requests
@@ -558,8 +599,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904
 
     """
     query = [("tag", t) for t in tags]
-    if dry_run:
-      query.append(("dry-run", 1))
+    _AppendDryRunIf(query, dry_run)
 
     return self._SendRequest(HTTP_PUT, "/%s/tags" % GANETI_RAPI_VERSION,
                              query, None)
@@ -576,8 +616,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904
 
     """
     query = [("tag", t) for t in tags]
-    if dry_run:
-      query.append(("dry-run", 1))
+    _AppendDryRunIf(query, dry_run)
 
     return self._SendRequest(HTTP_DELETE, "/%s/tags" % GANETI_RAPI_VERSION,
                              query, None)
@@ -593,8 +632,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904
 
     """
     query = []
-    if bulk:
-      query.append(("bulk", 1))
+    _AppendIf(query, bulk, ("bulk", 1))
 
     instances = self._SendRequest(HTTP_GET,
                                   "/%s/instances" % GANETI_RAPI_VERSION,
@@ -662,8 +700,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904
     """
     query = []
 
-    if kwargs.get("dry_run"):
-      query.append(("dry-run", 1))
+    _AppendDryRunIf(query, kwargs.get("dry_run"))
 
     if _INST_CREATE_REQV1 in self.GetFeatures():
       # All required fields for request data version 1
@@ -701,8 +738,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904
 
     """
     query = []
-    if dry_run:
-      query.append(("dry-run", 1))
+    _AppendDryRunIf(query, dry_run)
 
     return self._SendRequest(HTTP_DELETE,
                              ("/%s/instances/%s" %
@@ -737,8 +773,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904
 
     """
     query = []
-    if ignore_size:
-      query.append(("ignore_size", 1))
+    _AppendIf(query, ignore_size, ("ignore_size", 1))
 
     return self._SendRequest(HTTP_PUT,
                              ("/%s/instances/%s/activate-disks" %
@@ -757,6 +792,27 @@ class GanetiRapiClient(object): # pylint: disable=R0904
                              ("/%s/instances/%s/deactivate-disks" %
                               (GANETI_RAPI_VERSION, instance)), None, None)
 
+  def RecreateInstanceDisks(self, instance, disks=None, nodes=None):
+    """Recreate an instance's disks.
+
+    @type instance: string
+    @param instance: Instance name
+    @type disks: list of int
+    @param disks: List of disk indexes
+    @type nodes: list of string
+    @param nodes: New instance nodes, if relocation is desired
+    @rtype: string
+    @return: job id
+
+    """
+    body = {}
+    _SetItemIf(body, disks is not None, "disks", disks)
+    _SetItemIf(body, nodes is not None, "nodes", nodes)
+
+    return self._SendRequest(HTTP_POST,
+                             ("/%s/instances/%s/recreate-disks" %
+                              (GANETI_RAPI_VERSION, instance)), None, body)
+
   def GrowInstanceDisk(self, instance, disk, amount, wait_for_sync=None):
     """Grows a disk of an instance.
 
@@ -778,8 +834,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904
       "amount": amount,
       }
 
-    if wait_for_sync is not None:
-      body["wait_for_sync"] = wait_for_sync
+    _SetItemIf(body, wait_for_sync is not None, "wait_for_sync", wait_for_sync)
 
     return self._SendRequest(HTTP_POST,
                              ("/%s/instances/%s/disk/%s/grow" %
@@ -815,8 +870,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904
 
     """
     query = [("tag", t) for t in tags]
-    if dry_run:
-      query.append(("dry-run", 1))
+    _AppendDryRunIf(query, dry_run)
 
     return self._SendRequest(HTTP_PUT,
                              ("/%s/instances/%s/tags" %
@@ -836,8 +890,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904
 
     """
     query = [("tag", t) for t in tags]
-    if dry_run:
-      query.append(("dry-run", 1))
+    _AppendDryRunIf(query, dry_run)
 
     return self._SendRequest(HTTP_DELETE,
                              ("/%s/instances/%s/tags" %
@@ -861,12 +914,10 @@ class GanetiRapiClient(object): # pylint: disable=R0904
 
     """
     query = []
-    if reboot_type:
-      query.append(("type", reboot_type))
-    if ignore_secondaries is not None:
-      query.append(("ignore_secondaries", ignore_secondaries))
-    if dry_run:
-      query.append(("dry-run", 1))
+    _AppendDryRunIf(query, dry_run)
+    _AppendIf(query, reboot_type, ("type", reboot_type))
+    _AppendIf(query, ignore_secondaries is not None,
+              ("ignore_secondaries", ignore_secondaries))
 
     return self._SendRequest(HTTP_POST,
                              ("/%s/instances/%s/reboot" %
@@ -886,10 +937,8 @@ class GanetiRapiClient(object): # pylint: disable=R0904
 
     """
     query = []
-    if dry_run:
-      query.append(("dry-run", 1))
-    if no_remember:
-      query.append(("no-remember", 1))
+    _AppendDryRunIf(query, dry_run)
+    _AppendIf(query, no_remember, ("no-remember", 1))
 
     return self._SendRequest(HTTP_PUT,
                              ("/%s/instances/%s/shutdown" %
@@ -909,10 +958,8 @@ class GanetiRapiClient(object): # pylint: disable=R0904
 
     """
     query = []
-    if dry_run:
-      query.append(("dry-run", 1))
-    if no_remember:
-      query.append(("no-remember", 1))
+    _AppendDryRunIf(query, dry_run)
+    _AppendIf(query, no_remember, ("no-remember", 1))
 
     return self._SendRequest(HTTP_PUT,
                              ("/%s/instances/%s/startup" %
@@ -937,10 +984,8 @@ class GanetiRapiClient(object): # pylint: disable=R0904
       body = {
         "start": not no_startup,
         }
-      if os is not None:
-        body["os"] = os
-      if osparams is not None:
-        body["osparams"] = osparams
+      _SetItemIf(body, os is not None, "os", os)
+      _SetItemIf(body, osparams is not None, "osparams", osparams)
       return self._SendRequest(HTTP_POST,
                                ("/%s/instances/%s/reinstall" %
                                 (GANETI_RAPI_VERSION, instance)), None, body)
@@ -951,10 +996,9 @@ class GanetiRapiClient(object): # pylint: disable=R0904
                            " for instance reinstallation")
 
     query = []
-    if os:
-      query.append(("os", os))
-    if no_startup:
-      query.append(("nostartup", 1))
+    _AppendIf(query, os, ("os", os))
+    _AppendIf(query, no_startup, ("nostartup", 1))
+
     return self._SendRequest(HTTP_POST,
                              ("/%s/instances/%s/reinstall" %
                               (GANETI_RAPI_VERSION, instance)), query, None)
@@ -987,13 +1031,11 @@ class GanetiRapiClient(object): # pylint: disable=R0904
     # TODO: Convert to body parameters
 
     if disks is not None:
-      query.append(("disks", ",".join(str(idx) for idx in disks)))
-
-    if remote_node is not None:
-      query.append(("remote_node", remote_node))
+      _AppendIf(query, True,
+                ("disks", ",".join(str(idx) for idx in disks)))
 
-    if iallocator is not None:
-      query.append(("iallocator", iallocator))
+    _AppendIf(query, remote_node is not None, ("remote_node", remote_node))
+    _AppendIf(query, iallocator is not None, ("iallocator", iallocator))
 
     return self._SendRequest(HTTP_POST,
                              ("/%s/instances/%s/replace-disks" %
@@ -1033,17 +1075,12 @@ class GanetiRapiClient(object): # pylint: disable=R0904
       "mode": mode,
       }
 
-    if shutdown is not None:
-      body["shutdown"] = shutdown
-
-    if remove_instance is not None:
-      body["remove_instance"] = remove_instance
-
-    if x509_key_name is not None:
-      body["x509_key_name"] = x509_key_name
-
-    if destination_x509_ca is not None:
-      body["destination_x509_ca"] = destination_x509_ca
+    _SetItemIf(body, shutdown is not None, "shutdown", shutdown)
+    _SetItemIf(body, remove_instance is not None,
+               "remove_instance", remove_instance)
+    _SetItemIf(body, x509_key_name is not None, "x509_key_name", x509_key_name)
+    _SetItemIf(body, destination_x509_ca is not None,
+               "destination_x509_ca", destination_x509_ca)
 
     return self._SendRequest(HTTP_PUT,
                              ("/%s/instances/%s/export" %
@@ -1063,12 +1100,8 @@ class GanetiRapiClient(object): # pylint: disable=R0904
 
     """
     body = {}
-
-    if mode is not None:
-      body["mode"] = mode
-
-    if cleanup is not None:
-      body["cleanup"] = cleanup
+    _SetItemIf(body, mode is not None, "mode", mode)
+    _SetItemIf(body, cleanup is not None, "cleanup", cleanup)
 
     return self._SendRequest(HTTP_PUT,
                              ("/%s/instances/%s/migrate" %
@@ -1092,15 +1125,10 @@ class GanetiRapiClient(object): # pylint: disable=R0904
 
     """
     body = {}
-
-    if iallocator is not None:
-      body["iallocator"] = iallocator
-
-    if ignore_consistency is not None:
-      body["ignore_consistency"] = ignore_consistency
-
-    if target_node is not None:
-      body["target_node"] = target_node
+    _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
+    _SetItemIf(body, ignore_consistency is not None,
+               "ignore_consistency", ignore_consistency)
+    _SetItemIf(body, target_node is not None, "target_node", target_node)
 
     return self._SendRequest(HTTP_PUT,
                              ("/%s/instances/%s/failover" %
@@ -1125,11 +1153,8 @@ class GanetiRapiClient(object): # pylint: disable=R0904
       "new_name": new_name,
       }
 
-    if ip_check is not None:
-      body["ip_check"] = ip_check
-
-    if name_check is not None:
-      body["name_check"] = name_check
+    _SetItemIf(body, ip_check is not None, "ip_check", ip_check)
+    _SetItemIf(body, name_check is not None, "name_check", name_check)
 
     return self._SendRequest(HTTP_PUT,
                              ("/%s/instances/%s/rename" %
@@ -1243,8 +1268,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904
 
     """
     query = []
-    if dry_run:
-      query.append(("dry-run", 1))
+    _AppendDryRunIf(query, dry_run)
 
     return self._SendRequest(HTTP_DELETE,
                              "/%s/jobs/%s" % (GANETI_RAPI_VERSION, job_id),
@@ -1262,8 +1286,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904
 
     """
     query = []
-    if bulk:
-      query.append(("bulk", 1))
+    _AppendIf(query, bulk, ("bulk", 1))
 
     nodes = self._SendRequest(HTTP_GET, "/%s/nodes" % GANETI_RAPI_VERSION,
                               query, None)
@@ -1321,21 +1344,17 @@ class GanetiRapiClient(object): # pylint: disable=R0904
       raise GanetiApiError("Only one of iallocator or remote_node can be used")
 
     query = []
-    if dry_run:
-      query.append(("dry-run", 1))
+    _AppendDryRunIf(query, dry_run)
 
     if _NODE_EVAC_RES1 in self.GetFeatures():
       # Server supports body parameters
       body = {}
 
-      if iallocator is not None:
-        body["iallocator"] = iallocator
-      if remote_node is not None:
-        body["remote_node"] = remote_node
-      if early_release is not None:
-        body["early_release"] = early_release
-      if mode is not None:
-        body["mode"] = mode
+      _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
+      _SetItemIf(body, remote_node is not None, "remote_node", remote_node)
+      _SetItemIf(body, early_release is not None,
+                 "early_release", early_release)
+      _SetItemIf(body, mode is not None, "mode", mode)
     else:
       # Pre-2.5 request format
       body = None
@@ -1349,12 +1368,9 @@ class GanetiRapiClient(object): # pylint: disable=R0904
       if mode is not None and mode != NODE_EVAC_SEC:
         raise GanetiApiError("Server can only evacuate secondary instances")
 
-      if iallocator:
-        query.append(("iallocator", iallocator))
-      if remote_node:
-        query.append(("remote_node", remote_node))
-      if early_release:
-        query.append(("early_release", 1))
+      _AppendIf(query, iallocator, ("iallocator", iallocator))
+      _AppendIf(query, remote_node, ("remote_node", remote_node))
+      _AppendIf(query, early_release, ("early_release", 1))
 
     return self._SendRequest(HTTP_POST,
                              ("/%s/nodes/%s/evacuate" %
@@ -1381,18 +1397,14 @@ class GanetiRapiClient(object): # pylint: disable=R0904
 
     """
     query = []
-    if dry_run:
-      query.append(("dry-run", 1))
+    _AppendDryRunIf(query, dry_run)
 
     if _NODE_MIGRATE_REQV1 in self.GetFeatures():
       body = {}
 
-      if mode is not None:
-        body["mode"] = mode
-      if iallocator is not None:
-        body["iallocator"] = iallocator
-      if target_node is not None:
-        body["target_node"] = target_node
+      _SetItemIf(body, mode is not None, "mode", mode)
+      _SetItemIf(body, iallocator is not None, "iallocator", iallocator)
+      _SetItemIf(body, target_node is not None, "target_node", target_node)
 
       assert len(query) <= 1
 
@@ -1405,8 +1417,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904
         raise GanetiApiError("Server does not support specifying target node"
                              " for node migration")
 
-      if mode is not None:
-        query.append(("mode", mode))
+      _AppendIf(query, mode is not None, ("mode", mode))
 
       return self._SendRequest(HTTP_POST,
                                ("/%s/nodes/%s/migrate" %
@@ -1426,7 +1437,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904
                              ("/%s/nodes/%s/role" %
                               (GANETI_RAPI_VERSION, node)), None, None)
 
-  def SetNodeRole(self, node, role, force=False):
+  def SetNodeRole(self, node, role, force=False, auto_promote=None):
     """Sets the role for a node.
 
     @type node: str
@@ -1435,19 +1446,55 @@ class GanetiRapiClient(object): # pylint: disable=R0904
     @param role: the role to set for the node
     @type force: bool
     @param force: whether to force the role change
+    @type auto_promote: bool
+    @param auto_promote: Whether node(s) should be promoted to master candidate
+                         if necessary
 
     @rtype: string
     @return: job id
 
     """
-    query = [
-      ("force", force),
-      ]
+    query = []
+    _AppendForceIf(query, force)
+    _AppendIf(query, auto_promote is not None, ("auto-promote", auto_promote))
 
     return self._SendRequest(HTTP_PUT,
                              ("/%s/nodes/%s/role" %
                               (GANETI_RAPI_VERSION, node)), query, role)
 
+  def PowercycleNode(self, node, force=False):
+    """Powercycles a node.
+
+    @type node: string
+    @param node: Node name
+    @type force: bool
+    @param force: Whether to force the operation
+    @rtype: string
+    @return: job id
+
+    """
+    query = []
+    _AppendForceIf(query, force)
+
+    return self._SendRequest(HTTP_POST,
+                             ("/%s/nodes/%s/powercycle" %
+                              (GANETI_RAPI_VERSION, node)), query, None)
+
+  def ModifyNode(self, node, **kwargs):
+    """Modifies a node.
+
+    More details for parameters can be found in the RAPI documentation.
+
+    @type node: string
+    @param node: Node name
+    @rtype: string
+    @return: job id
+
+    """
+    return self._SendRequest(HTTP_POST,
+                             ("/%s/nodes/%s/modify" %
+                              (GANETI_RAPI_VERSION, node)), None, kwargs)
+
   def GetNodeStorageUnits(self, node, storage_type, output_fields):
     """Gets the storage units for a node.
 
@@ -1493,8 +1540,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904
       ("name", name),
       ]
 
-    if allocatable is not None:
-      query.append(("allocatable", allocatable))
+    _AppendIf(query, allocatable is not None, ("allocatable", allocatable))
 
     return self._SendRequest(HTTP_PUT,
                              ("/%s/nodes/%s/storage/modify" %
@@ -1552,8 +1598,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904
 
     """
     query = [("tag", t) for t in tags]
-    if dry_run:
-      query.append(("dry-run", 1))
+    _AppendDryRunIf(query, dry_run)
 
     return self._SendRequest(HTTP_PUT,
                              ("/%s/nodes/%s/tags" %
@@ -1574,8 +1619,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904
 
     """
     query = [("tag", t) for t in tags]
-    if dry_run:
-      query.append(("dry-run", 1))
+    _AppendDryRunIf(query, dry_run)
 
     return self._SendRequest(HTTP_DELETE,
                              ("/%s/nodes/%s/tags" %
@@ -1593,8 +1637,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904
 
     """
     query = []
-    if bulk:
-      query.append(("bulk", 1))
+    _AppendIf(query, bulk, ("bulk", 1))
 
     groups = self._SendRequest(HTTP_GET, "/%s/groups" % GANETI_RAPI_VERSION,
                                query, None)
@@ -1632,8 +1675,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904
 
     """
     query = []
-    if dry_run:
-      query.append(("dry-run", 1))
+    _AppendDryRunIf(query, dry_run)
 
     body = {
       "name": name,
@@ -1671,8 +1713,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904
 
     """
     query = []
-    if dry_run:
-      query.append(("dry-run", 1))
+    _AppendDryRunIf(query, dry_run)
 
     return self._SendRequest(HTTP_DELETE,
                              ("/%s/groups/%s" %
@@ -1711,12 +1752,8 @@ class GanetiRapiClient(object): # pylint: disable=R0904
 
     """
     query = []
-
-    if force:
-      query.append(("force", 1))
-
-    if dry_run:
-      query.append(("dry-run", 1))
+    _AppendForceIf(query, force)
+    _AppendDryRunIf(query, dry_run)
 
     body = {
       "nodes": nodes,
@@ -1755,8 +1792,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904
 
     """
     query = [("tag", t) for t in tags]
-    if dry_run:
-      query.append(("dry-run", 1))
+    _AppendDryRunIf(query, dry_run)
 
     return self._SendRequest(HTTP_PUT,
                              ("/%s/groups/%s/tags" %
@@ -1776,22 +1812,21 @@ class GanetiRapiClient(object): # pylint: disable=R0904
 
     """
     query = [("tag", t) for t in tags]
-    if dry_run:
-      query.append(("dry-run", 1))
+    _AppendDryRunIf(query, dry_run)
 
     return self._SendRequest(HTTP_DELETE,
                              ("/%s/groups/%s/tags" %
                               (GANETI_RAPI_VERSION, group)), query, None)
 
-  def Query(self, what, fields, filter_=None):
+  def Query(self, what, fields, qfilter=None):
     """Retrieves information about resources.
 
     @type what: string
     @param what: Resource name, one of L{constants.QR_VIA_RAPI}
     @type fields: list of string
     @param fields: Requested fields
-    @type filter_: None or list
-    @param filter_: Query filter
+    @type qfilter: None or list
+    @param qfilter: Query filter
 
     @rtype: string
     @return: job id
@@ -1801,8 +1836,9 @@ class GanetiRapiClient(object): # pylint: disable=R0904
       "fields": fields,
       }
 
-    if filter_ is not None:
-      body["filter"] = filter_
+    _SetItemIf(body, qfilter is not None, "qfilter", qfilter)
+    # TODO: remove "filter" after 2.7
+    _SetItemIf(body, qfilter is not None, "filter", qfilter)
 
     return self._SendRequest(HTTP_PUT,
                              ("/%s/query/%s" %
@@ -1823,7 +1859,7 @@ class GanetiRapiClient(object): # pylint: disable=R0904
     query = []
 
     if fields is not None:
-      query.append(("fields", ",".join(fields)))
+      _AppendIf(query, True, ("fields", ",".join(fields)))
 
     return self._SendRequest(HTTP_GET,
                              ("/%s/query/%s/fields" %
index 499e2e6..b701d71 100644 (file)
@@ -33,7 +33,6 @@ from ganeti import constants
 from ganeti import http
 from ganeti import utils
 
-from ganeti.rapi import baserlib
 from ganeti.rapi import rlib2
 
 
@@ -89,66 +88,6 @@ class Mapper:
     return (handler, groups, args)
 
 
-class R_root(baserlib.R_Generic):
-  """/ resource.
-
-  """
-  _ROOT_PATTERN = re.compile("^R_([a-zA-Z0-9]+)$")
-
-  @classmethod
-  def GET(cls):
-    """Show the list of mapped resources.
-
-    @return: a dictionary with 'name' and 'uri' keys for each of them.
-
-    """
-    rootlist = []
-    for handler in CONNECTOR.values():
-      m = cls._ROOT_PATTERN.match(handler.__name__)
-      if m:
-        name = m.group(1)
-        if name != "root":
-          rootlist.append(name)
-
-    return baserlib.BuildUriList(rootlist, "/%s")
-
-
-def _getResources(id_):
-  """Return a list of resources underneath given id.
-
-  This is to generalize querying of version resources lists.
-
-  @return: a list of resources names.
-
-  """
-  r_pattern = re.compile("^R_%s_([a-zA-Z0-9]+)$" % id_)
-
-  rlist = []
-  for handler in CONNECTOR.values():
-    m = r_pattern.match(handler.__name__)
-    if m:
-      name = m.group(1)
-      rlist.append(name)
-
-  return rlist
-
-
-class R_2(baserlib.R_Generic):
-  """/2 resource.
-
-  This is the root of the version 2 API.
-
-  """
-  @staticmethod
-  def GET():
-    """Show the list of mapped resources.
-
-    @return: a dictionary with 'name' and 'uri' keys for each of them.
-
-    """
-    return baserlib.BuildUriList(_getResources("2"), "/2/%s")
-
-
 def GetHandlers(node_name_pattern, instance_name_pattern,
                 group_name_pattern, job_id_pattern, disk_pattern,
                 query_res_pattern):
@@ -160,15 +99,16 @@ def GetHandlers(node_name_pattern, instance_name_pattern,
   # is more flexible and future-compatible than versioning the whole remote
   # API.
   return {
-    "/": R_root,
+    "/": rlib2.R_root,
+    "/2": rlib2.R_2,
 
     "/version": rlib2.R_version,
 
-    "/2": R_2,
-
     "/2/nodes": rlib2.R_2_nodes,
     re.compile(r"^/2/nodes/(%s)$" % node_name_pattern):
       rlib2.R_2_nodes_name,
+    re.compile(r"^/2/nodes/(%s)/powercycle$" % node_name_pattern):
+      rlib2.R_2_nodes_name_powercycle,
     re.compile(r"^/2/nodes/(%s)/tags$" % node_name_pattern):
       rlib2.R_2_nodes_name_tags,
     re.compile(r"^/2/nodes/(%s)/role$" % node_name_pattern):
@@ -177,6 +117,8 @@ def GetHandlers(node_name_pattern, instance_name_pattern,
       rlib2.R_2_nodes_name_evacuate,
     re.compile(r"^/2/nodes/(%s)/migrate$" % node_name_pattern):
       rlib2.R_2_nodes_name_migrate,
+    re.compile(r"^/2/nodes/(%s)/modify$" % node_name_pattern):
+      rlib2.R_2_nodes_name_modify,
     re.compile(r"^/2/nodes/(%s)/storage$" % node_name_pattern):
       rlib2.R_2_nodes_name_storage,
     re.compile(r"^/2/nodes/(%s)/storage/modify$" % node_name_pattern):
@@ -205,6 +147,8 @@ def GetHandlers(node_name_pattern, instance_name_pattern,
       rlib2.R_2_instances_name_activate_disks,
     re.compile(r"^/2/instances/(%s)/deactivate-disks$" % instance_name_pattern):
       rlib2.R_2_instances_name_deactivate_disks,
+    re.compile(r"^/2/instances/(%s)/recreate-disks$" % instance_name_pattern):
+      rlib2.R_2_instances_name_recreate_disks,
     re.compile(r"^/2/instances/(%s)/prepare-export$" % instance_name_pattern):
       rlib2.R_2_instances_name_prepare_export,
     re.compile(r"^/2/instances/(%s)/export$" % instance_name_pattern):
index 97ad7cd..e434340 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 Google Inc.
+# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 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
@@ -62,6 +62,7 @@ from ganeti import cli
 from ganeti import rapi
 from ganeti import ht
 from ganeti import compat
+from ganeti import ssconf
 from ganeti.rapi import baserlib
 
 
@@ -85,6 +86,7 @@ N_FIELDS = ["name", "offline", "master_candidate", "drained",
             "pip", "sip", "role",
             "pinst_list", "sinst_list",
             "master_capable", "vm_capable",
+            "ndparams",
             "group.uuid",
             ] + _COMMON_FIELDS
 
@@ -93,6 +95,12 @@ G_FIELDS = [
   "name",
   "node_cnt",
   "node_list",
+  "ipolicy",
+  "custom_ipolicy",
+  "diskparams",
+  "custom_diskparams",
+  "ndparams",
+  "custom_ndparams",
   ] + _COMMON_FIELDS
 
 J_FIELDS_BULK = [
@@ -107,14 +115,14 @@ J_FIELDS = J_FIELDS_BULK + [
   ]
 
 _NR_DRAINED = "drained"
-_NR_MASTER_CANDIATE = "master-candidate"
+_NR_MASTER_CANDIDATE = "master-candidate"
 _NR_MASTER = "master"
 _NR_OFFLINE = "offline"
 _NR_REGULAR = "regular"
 
 _NR_MAP = {
   constants.NR_MASTER: _NR_MASTER,
-  constants.NR_MCANDIDATE: _NR_MASTER_CANDIATE,
+  constants.NR_MCANDIDATE: _NR_MASTER_CANDIDATE,
   constants.NR_DRAINED: _NR_DRAINED,
   constants.NR_OFFLINE: _NR_OFFLINE,
   constants.NR_REGULAR: _NR_REGULAR,
@@ -148,7 +156,40 @@ ALL_FEATURES = frozenset([
 _WFJC_TIMEOUT = 10
 
 
-class R_version(baserlib.R_Generic):
+# FIXME: For compatibility we update the beparams/memory field. Needs to be
+#        removed in Ganeti 2.7
+def _UpdateBeparams(inst):
+  """Updates the beparams dict of inst to support the memory field.
+
+  @param inst: Inst dict
+  @return: Updated inst dict
+
+  """
+  beparams = inst["beparams"]
+  beparams[constants.BE_MEMORY] = beparams[constants.BE_MAXMEM]
+
+  return inst
+
+
+class R_root(baserlib.ResourceBase):
+  """/ resource.
+
+  """
+  @staticmethod
+  def GET():
+    """Supported for legacy reasons.
+
+    """
+    return None
+
+
+class R_2(R_root):
+  """/2 resource.
+
+  """
+
+
+class R_version(baserlib.ResourceBase):
   """/version resource.
 
   This resource should be used to determine the remote API version and
@@ -163,20 +204,21 @@ class R_version(baserlib.R_Generic):
     return constants.RAPI_VERSION
 
 
-class R_2_info(baserlib.R_Generic):
+class R_2_info(baserlib.OpcodeResource):
   """/2/info resource.
 
   """
-  @staticmethod
-  def GET():
+  GET_OPCODE = opcodes.OpClusterQuery
+
+  def GET(self):
     """Returns cluster information.
 
     """
-    client = baserlib.GetClient()
+    client = self.GetClient()
     return client.QueryClusterInfo()
 
 
-class R_2_features(baserlib.R_Generic):
+class R_2_features(baserlib.ResourceBase):
   """/2/features resource.
 
   """
@@ -188,12 +230,13 @@ class R_2_features(baserlib.R_Generic):
     return list(ALL_FEATURES)
 
 
-class R_2_os(baserlib.R_Generic):
+class R_2_os(baserlib.OpcodeResource):
   """/2/os resource.
 
   """
-  @staticmethod
-  def GET():
+  GET_OPCODE = opcodes.OpOsDiagnose
+
+  def GET(self):
     """Return a list of all OSes.
 
     Can return error 500 in case of a problem.
@@ -201,9 +244,9 @@ class R_2_os(baserlib.R_Generic):
     Example: ["debian-etch"]
 
     """
-    cl = baserlib.GetClient()
+    cl = self.GetClient()
     op = opcodes.OpOsDiagnose(output_fields=["name", "variants"], names=[])
-    job_id = baserlib.SubmitJob([op], cl)
+    job_id = self.SubmitJob([op], cl=cl)
     # we use custom feedback function, instead of print we log the status
     result = cli.PollJob(job_id, cl, feedback_fn=baserlib.FeedbackFn)
     diagnose_data = result[0]
@@ -218,35 +261,21 @@ class R_2_os(baserlib.R_Generic):
     return os_names
 
 
-class R_2_redist_config(baserlib.R_Generic):
+class R_2_redist_config(baserlib.OpcodeResource):
   """/2/redistribute-config resource.
 
   """
-  @staticmethod
-  def PUT():
-    """Redistribute configuration to all nodes.
-
-    """
-    return baserlib.SubmitJob([opcodes.OpClusterRedistConf()])
+  PUT_OPCODE = opcodes.OpClusterRedistConf
 
 
-class R_2_cluster_modify(baserlib.R_Generic):
+class R_2_cluster_modify(baserlib.OpcodeResource):
   """/2/modify resource.
 
   """
-  def PUT(self):
-    """Modifies cluster parameters.
+  PUT_OPCODE = opcodes.OpClusterSetParams
 
-    @return: a job id
 
-    """
-    op = baserlib.FillOpcode(opcodes.OpClusterSetParams, self.request_body,
-                             None)
-
-    return baserlib.SubmitJob([op])
-
-
-class R_2_jobs(baserlib.R_Generic):
+class R_2_jobs(baserlib.ResourceBase):
   """/2/jobs resource.
 
   """
@@ -256,7 +285,7 @@ class R_2_jobs(baserlib.R_Generic):
     @return: a dictionary with jobs id and uri.
 
     """
-    client = baserlib.GetClient()
+    client = self.GetClient()
 
     if self.useBulk():
       bulkdata = client.QueryJobs(None, J_FIELDS_BULK)
@@ -267,7 +296,7 @@ class R_2_jobs(baserlib.R_Generic):
                                    uri_fields=("id", "uri"))
 
 
-class R_2_jobs_id(baserlib.R_Generic):
+class R_2_jobs_id(baserlib.ResourceBase):
   """/2/jobs/[job_id] resource.
 
   """
@@ -285,7 +314,7 @@ class R_2_jobs_id(baserlib.R_Generic):
 
     """
     job_id = self.items[0]
-    result = baserlib.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
+    result = self.GetClient().QueryJobs([job_id, ], J_FIELDS)[0]
     if result is None:
       raise http.HttpNotFound()
     return baserlib.MapFields(J_FIELDS, result)
@@ -295,11 +324,11 @@ class R_2_jobs_id(baserlib.R_Generic):
 
     """
     job_id = self.items[0]
-    result = baserlib.GetClient().CancelJob(job_id)
+    result = self.GetClient().CancelJob(job_id)
     return result
 
 
-class R_2_jobs_id_wait(baserlib.R_Generic):
+class R_2_jobs_id_wait(baserlib.ResourceBase):
   """/2/jobs/[job_id]/wait resource.
 
   """
@@ -329,7 +358,7 @@ class R_2_jobs_id_wait(baserlib.R_Generic):
       raise http.HttpBadRequest("The 'previous_log_serial' parameter should"
                                 " be a number")
 
-    client = baserlib.GetClient()
+    client = self.GetClient()
     result = client.WaitForJobChangeOnce(job_id, fields,
                                          prev_job_info, prev_log_serial,
                                          timeout=_WFJC_TIMEOUT)
@@ -348,15 +377,17 @@ class R_2_jobs_id_wait(baserlib.R_Generic):
       }
 
 
-class R_2_nodes(baserlib.R_Generic):
+class R_2_nodes(baserlib.OpcodeResource):
   """/2/nodes resource.
 
   """
+  GET_OPCODE = opcodes.OpNodeQuery
+
   def GET(self):
     """Returns a list of all nodes.
 
     """
-    client = baserlib.GetClient()
+    client = self.GetClient()
 
     if self.useBulk():
       bulkdata = client.QueryNodes([], N_FIELDS, False)
@@ -368,16 +399,18 @@ class R_2_nodes(baserlib.R_Generic):
                                    uri_fields=("id", "uri"))
 
 
-class R_2_nodes_name(baserlib.R_Generic):
+class R_2_nodes_name(baserlib.OpcodeResource):
   """/2/nodes/[node_name] resource.
 
   """
+  GET_OPCODE = opcodes.OpNodeQuery
+
   def GET(self):
     """Send information about a node.
 
     """
     node_name = self.items[0]
-    client = baserlib.GetClient()
+    client = self.GetClient()
 
     result = baserlib.HandleItemQueryErrors(client.QueryNodes,
                                             names=[node_name], fields=N_FIELDS,
@@ -386,10 +419,28 @@ class R_2_nodes_name(baserlib.R_Generic):
     return baserlib.MapFields(N_FIELDS, result[0])
 
 
-class R_2_nodes_name_role(baserlib.R_Generic):
-  """ /2/nodes/[node_name]/role resource.
+class R_2_nodes_name_powercycle(baserlib.OpcodeResource):
+  """/2/nodes/[node_name]/powercycle resource.
+
+  """
+  POST_OPCODE = opcodes.OpNodePowercycle
+
+  def GetPostOpInput(self):
+    """Tries to powercycle a node.
+
+    """
+    return (self.request_body, {
+      "node_name": self.items[0],
+      "force": self.useForce(),
+      })
+
+
+class R_2_nodes_name_role(baserlib.OpcodeResource):
+  """/2/nodes/[node_name]/role resource.
 
   """
+  PUT_OPCODE = opcodes.OpNodeSetParams
+
   def GET(self):
     """Returns the current node role.
 
@@ -397,22 +448,18 @@ class R_2_nodes_name_role(baserlib.R_Generic):
 
     """
     node_name = self.items[0]
-    client = baserlib.GetClient()
+    client = self.GetClient()
     result = client.QueryNodes(names=[node_name], fields=["role"],
                                use_locking=self.useLocking())
 
     return _NR_MAP[result[0][0]]
 
-  def PUT(self):
+  def GetPutOpInput(self):
     """Sets the node role.
 
-    @return: a job id
-
     """
-    if not isinstance(self.request_body, basestring):
-      raise http.HttpBadRequest("Invalid body contents, not a string")
+    baserlib.CheckType(self.request_body, basestring, "Body contents")
 
-    node_name = self.items[0]
     role = self.request_body
 
     if role == _NR_REGULAR:
@@ -420,7 +467,7 @@ class R_2_nodes_name_role(baserlib.R_Generic):
       offline = False
       drained = False
 
-    elif role == _NR_MASTER_CANDIATE:
+    elif role == _NR_MASTER_CANDIDATE:
       candidate = True
       offline = drained = None
 
@@ -435,41 +482,44 @@ class R_2_nodes_name_role(baserlib.R_Generic):
     else:
       raise http.HttpBadRequest("Can't set '%s' role" % role)
 
-    op = opcodes.OpNodeSetParams(node_name=node_name,
-                                 master_candidate=candidate,
-                                 offline=offline,
-                                 drained=drained,
-                                 force=bool(self.useForce()))
+    assert len(self.items) == 1
 
-    return baserlib.SubmitJob([op])
+    return ({}, {
+      "node_name": self.items[0],
+      "master_candidate": candidate,
+      "offline": offline,
+      "drained": drained,
+      "force": self.useForce(),
+      "auto_promote": bool(self._checkIntVariable("auto-promote", default=0)),
+      })
 
 
-class R_2_nodes_name_evacuate(baserlib.R_Generic):
+class R_2_nodes_name_evacuate(baserlib.OpcodeResource):
   """/2/nodes/[node_name]/evacuate resource.
 
   """
-  def POST(self):
+  POST_OPCODE = opcodes.OpNodeEvacuate
+
+  def GetPostOpInput(self):
     """Evacuate all instances off a node.
 
     """
-    op = baserlib.FillOpcode(opcodes.OpNodeEvacuate, self.request_body, {
+    return (self.request_body, {
       "node_name": self.items[0],
       "dry_run": self.dryRun(),
       })
 
-    return baserlib.SubmitJob([op])
-
 
-class R_2_nodes_name_migrate(baserlib.R_Generic):
+class R_2_nodes_name_migrate(baserlib.OpcodeResource):
   """/2/nodes/[node_name]/migrate resource.
 
   """
-  def POST(self):
+  POST_OPCODE = opcodes.OpNodeMigrate
+
+  def GetPostOpInput(self):
     """Migrate all primary instances from a node.
 
     """
-    node_name = self.items[0]
-
     if self.queryargs:
       # Support old-style requests
       if "live" in self.queryargs and "mode" in self.queryargs:
@@ -490,52 +540,67 @@ class R_2_nodes_name_migrate(baserlib.R_Generic):
     else:
       data = self.request_body
 
-    op = baserlib.FillOpcode(opcodes.OpNodeMigrate, data, {
-      "node_name": node_name,
+    return (data, {
+      "node_name": self.items[0],
       })
 
-    return baserlib.SubmitJob([op])
 
+class R_2_nodes_name_modify(baserlib.OpcodeResource):
+  """/2/nodes/[node_name]/modify resource.
 
-class R_2_nodes_name_storage(baserlib.R_Generic):
+  """
+  POST_OPCODE = opcodes.OpNodeSetParams
+
+  def GetPostOpInput(self):
+    """Changes parameters of a node.
+
+    """
+    assert len(self.items) == 1
+
+    return (self.request_body, {
+      "node_name": self.items[0],
+      })
+
+
+class R_2_nodes_name_storage(baserlib.OpcodeResource):
   """/2/nodes/[node_name]/storage resource.
 
   """
   # LUNodeQueryStorage acquires locks, hence restricting access to GET
   GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
+  GET_OPCODE = opcodes.OpNodeQueryStorage
 
-  def GET(self):
-    node_name = self.items[0]
+  def GetGetOpInput(self):
+    """List storage available on a node.
 
+    """
     storage_type = self._checkStringVariable("storage_type", None)
-    if not storage_type:
-      raise http.HttpBadRequest("Missing the required 'storage_type'"
-                                " parameter")
-
     output_fields = self._checkStringVariable("output_fields", None)
+
     if not output_fields:
       raise http.HttpBadRequest("Missing the required 'output_fields'"
                                 " parameter")
 
-    op = opcodes.OpNodeQueryStorage(nodes=[node_name],
-                                    storage_type=storage_type,
-                                    output_fields=output_fields.split(","))
-    return baserlib.SubmitJob([op])
+    return ({}, {
+      "nodes": [self.items[0]],
+      "storage_type": storage_type,
+      "output_fields": output_fields.split(","),
+      })
 
 
-class R_2_nodes_name_storage_modify(baserlib.R_Generic):
+class R_2_nodes_name_storage_modify(baserlib.OpcodeResource):
   """/2/nodes/[node_name]/storage/modify resource.
 
   """
-  def PUT(self):
-    node_name = self.items[0]
+  PUT_OPCODE = opcodes.OpNodeModifyStorage
 
-    storage_type = self._checkStringVariable("storage_type", None)
-    if not storage_type:
-      raise http.HttpBadRequest("Missing the required 'storage_type'"
-                                " parameter")
+  def GetPutOpInput(self):
+    """Modifies a storage volume on a node.
 
+    """
+    storage_type = self._checkStringVariable("storage_type", None)
     name = self._checkStringVariable("name", None)
+
     if not name:
       raise http.HttpBadRequest("Missing the required 'name'"
                                 " parameter")
@@ -546,64 +611,61 @@ class R_2_nodes_name_storage_modify(baserlib.R_Generic):
       changes[constants.SF_ALLOCATABLE] = \
         bool(self._checkIntVariable("allocatable", default=1))
 
-    op = opcodes.OpNodeModifyStorage(node_name=node_name,
-                                     storage_type=storage_type,
-                                     name=name,
-                                     changes=changes)
-    return baserlib.SubmitJob([op])
+    return ({}, {
+      "node_name": self.items[0],
+      "storage_type": storage_type,
+      "name": name,
+      "changes": changes,
+      })
 
 
-class R_2_nodes_name_storage_repair(baserlib.R_Generic):
+class R_2_nodes_name_storage_repair(baserlib.OpcodeResource):
   """/2/nodes/[node_name]/storage/repair resource.
 
   """
-  def PUT(self):
-    node_name = self.items[0]
+  PUT_OPCODE = opcodes.OpRepairNodeStorage
 
-    storage_type = self._checkStringVariable("storage_type", None)
-    if not storage_type:
-      raise http.HttpBadRequest("Missing the required 'storage_type'"
-                                " parameter")
+  def GetPutOpInput(self):
+    """Repairs a storage volume on a node.
 
+    """
+    storage_type = self._checkStringVariable("storage_type", None)
     name = self._checkStringVariable("name", None)
     if not name:
       raise http.HttpBadRequest("Missing the required 'name'"
                                 " parameter")
 
-    op = opcodes.OpRepairNodeStorage(node_name=node_name,
-                                     storage_type=storage_type,
-                                     name=name)
-    return baserlib.SubmitJob([op])
-
+    return ({}, {
+      "node_name": self.items[0],
+      "storage_type": storage_type,
+      "name": name,
+      })
 
-def _ParseCreateGroupRequest(data, dry_run):
-  """Parses a request for creating a node group.
 
-  @rtype: L{opcodes.OpGroupAdd}
-  @return: Group creation opcode
+class R_2_groups(baserlib.OpcodeResource):
+  """/2/groups resource.
 
   """
-  override = {
-    "dry_run": dry_run,
-    }
-
-  rename = {
+  GET_OPCODE = opcodes.OpGroupQuery
+  POST_OPCODE = opcodes.OpGroupAdd
+  POST_RENAME = {
     "name": "group_name",
     }
 
-  return baserlib.FillOpcode(opcodes.OpGroupAdd, data, override,
-                             rename=rename)
-
+  def GetPostOpInput(self):
+    """Create a node group.
 
-class R_2_groups(baserlib.R_Generic):
-  """/2/groups resource.
+    """
+    assert not self.items
+    return (self.request_body, {
+      "dry_run": self.dryRun(),
+      })
 
-  """
   def GET(self):
     """Returns a list of all node groups.
 
     """
-    client = baserlib.GetClient()
+    client = self.GetClient()
 
     if self.useBulk():
       bulkdata = client.QueryGroups([], G_FIELDS, False)
@@ -614,27 +676,19 @@ class R_2_groups(baserlib.R_Generic):
       return baserlib.BuildUriList(groupnames, "/2/groups/%s",
                                    uri_fields=("name", "uri"))
 
-  def POST(self):
-    """Create a node group.
-
-    @return: a job id
 
-    """
-    baserlib.CheckType(self.request_body, dict, "Body contents")
-    op = _ParseCreateGroupRequest(self.request_body, self.dryRun())
-    return baserlib.SubmitJob([op])
-
-
-class R_2_groups_name(baserlib.R_Generic):
+class R_2_groups_name(baserlib.OpcodeResource):
   """/2/groups/[group_name] resource.
 
   """
+  DELETE_OPCODE = opcodes.OpGroupRemove
+
   def GET(self):
     """Send information about a node group.
 
     """
     group_name = self.items[0]
-    client = baserlib.GetClient()
+    client = self.GetClient()
 
     result = baserlib.HandleItemQueryErrors(client.QueryGroups,
                                             names=[group_name], fields=G_FIELDS,
@@ -642,148 +696,102 @@ class R_2_groups_name(baserlib.R_Generic):
 
     return baserlib.MapFields(G_FIELDS, result[0])
 
-  def DELETE(self):
+  def GetDeleteOpInput(self):
     """Delete a node group.
 
     """
-    op = opcodes.OpGroupRemove(group_name=self.items[0],
-                               dry_run=bool(self.dryRun()))
-
-    return baserlib.SubmitJob([op])
-
-
-def _ParseModifyGroupRequest(name, data):
-  """Parses a request for modifying a node group.
-
-  @rtype: L{opcodes.OpGroupSetParams}
-  @return: Group modify opcode
-
-  """
-  return baserlib.FillOpcode(opcodes.OpGroupSetParams, data, {
-    "group_name": name,
-    })
+    assert len(self.items) == 1
+    return ({}, {
+      "group_name": self.items[0],
+      "dry_run": self.dryRun(),
+      })
 
 
-class R_2_groups_name_modify(baserlib.R_Generic):
+class R_2_groups_name_modify(baserlib.OpcodeResource):
   """/2/groups/[group_name]/modify resource.
 
   """
-  def PUT(self):
-    """Changes some parameters of node group.
+  PUT_OPCODE = opcodes.OpGroupSetParams
 
-    @return: a job id
+  def GetPutOpInput(self):
+    """Changes some parameters of node group.
 
     """
-    baserlib.CheckType(self.request_body, dict, "Body contents")
-
-    op = _ParseModifyGroupRequest(self.items[0], self.request_body)
-
-    return baserlib.SubmitJob([op])
-
-
-def _ParseRenameGroupRequest(name, data, dry_run):
-  """Parses a request for renaming a node group.
-
-  @type name: string
-  @param name: name of the node group to rename
-  @type data: dict
-  @param data: the body received by the rename request
-  @type dry_run: bool
-  @param dry_run: whether to perform a dry run
-
-  @rtype: L{opcodes.OpGroupRename}
-  @return: Node group rename opcode
-
-  """
-  return baserlib.FillOpcode(opcodes.OpGroupRename, data, {
-    "group_name": name,
-    "dry_run": dry_run,
-    })
+    assert self.items
+    return (self.request_body, {
+      "group_name": self.items[0],
+      })
 
 
-class R_2_groups_name_rename(baserlib.R_Generic):
+class R_2_groups_name_rename(baserlib.OpcodeResource):
   """/2/groups/[group_name]/rename resource.
 
   """
-  def PUT(self):
-    """Changes the name of a node group.
+  PUT_OPCODE = opcodes.OpGroupRename
 
-    @return: a job id
+  def GetPutOpInput(self):
+    """Changes the name of a node group.
 
     """
-    baserlib.CheckType(self.request_body, dict, "Body contents")
-    op = _ParseRenameGroupRequest(self.items[0], self.request_body,
-                                  self.dryRun())
-    return baserlib.SubmitJob([op])
+    assert len(self.items) == 1
+    return (self.request_body, {
+      "group_name": self.items[0],
+      "dry_run": self.dryRun(),
+      })
 
 
-class R_2_groups_name_assign_nodes(baserlib.R_Generic):
+class R_2_groups_name_assign_nodes(baserlib.OpcodeResource):
   """/2/groups/[group_name]/assign-nodes resource.
 
   """
-  def PUT(self):
-    """Assigns nodes to a group.
+  PUT_OPCODE = opcodes.OpGroupAssignNodes
 
-    @return: a job id
+  def GetPutOpInput(self):
+    """Assigns nodes to a group.
 
     """
-    op = baserlib.FillOpcode(opcodes.OpGroupAssignNodes, self.request_body, {
+    assert len(self.items) == 1
+    return (self.request_body, {
       "group_name": self.items[0],
       "dry_run": self.dryRun(),
       "force": self.useForce(),
       })
 
-    return baserlib.SubmitJob([op])
 
-
-def _ParseInstanceCreateRequestVersion1(data, dry_run):
-  """Parses an instance creation request version 1.
-
-  @rtype: L{opcodes.OpInstanceCreate}
-  @return: Instance creation opcode
+class R_2_instances(baserlib.OpcodeResource):
+  """/2/instances resource.
 
   """
-  override = {
-    "dry_run": dry_run,
-    }
-
-  rename = {
+  GET_OPCODE = opcodes.OpInstanceQuery
+  POST_OPCODE = opcodes.OpInstanceCreate
+  POST_RENAME = {
     "os": "os_type",
     "name": "instance_name",
     }
 
-  return baserlib.FillOpcode(opcodes.OpInstanceCreate, data, override,
-                             rename=rename)
-
-
-class R_2_instances(baserlib.R_Generic):
-  """/2/instances resource.
-
-  """
   def GET(self):
     """Returns a list of all available instances.
 
     """
-    client = baserlib.GetClient()
+    client = self.GetClient()
 
     use_locking = self.useLocking()
     if self.useBulk():
       bulkdata = client.QueryInstances([], I_FIELDS, use_locking)
-      return baserlib.MapBulkFields(bulkdata, I_FIELDS)
+      return map(_UpdateBeparams, baserlib.MapBulkFields(bulkdata, I_FIELDS))
     else:
       instancesdata = client.QueryInstances([], ["name"], use_locking)
       instanceslist = [row[0] for row in instancesdata]
       return baserlib.BuildUriList(instanceslist, "/2/instances/%s",
                                    uri_fields=("id", "uri"))
 
-  def POST(self):
+  def GetPostOpInput(self):
     """Create an instance.
 
     @return: a job id
 
     """
-    if not isinstance(self.request_body, dict):
-      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
+    baserlib.CheckType(self.request_body, dict, "Body contents")
 
     # Default to request data version 0
     data_version = self.getBodyParameter(_REQ_DATA_VERSION, 0)
@@ -791,27 +799,31 @@ class R_2_instances(baserlib.R_Generic):
     if data_version == 0:
       raise http.HttpBadRequest("Instance creation request version 0 is no"
                                 " longer supported")
-    elif data_version == 1:
-      data = self.request_body.copy()
-      # Remove "__version__"
-      data.pop(_REQ_DATA_VERSION, None)
-      op = _ParseInstanceCreateRequestVersion1(data, self.dryRun())
-    else:
+    elif data_version != 1:
       raise http.HttpBadRequest("Unsupported request data version %s" %
                                 data_version)
 
-    return baserlib.SubmitJob([op])
+    data = self.request_body.copy()
+    # Remove "__version__"
+    data.pop(_REQ_DATA_VERSION, None)
 
+    return (data, {
+      "dry_run": self.dryRun(),
+      })
 
-class R_2_instances_name(baserlib.R_Generic):
+
+class R_2_instances_name(baserlib.OpcodeResource):
   """/2/instances/[instance_name] resource.
 
   """
+  GET_OPCODE = opcodes.OpInstanceQuery
+  DELETE_OPCODE = opcodes.OpInstanceRemove
+
   def GET(self):
     """Send information about an instance.
 
     """
-    client = baserlib.GetClient()
+    client = self.GetClient()
     instance_name = self.items[0]
 
     result = baserlib.HandleItemQueryErrors(client.QueryInstances,
@@ -819,114 +831,101 @@ class R_2_instances_name(baserlib.R_Generic):
                                             fields=I_FIELDS,
                                             use_locking=self.useLocking())
 
-    return baserlib.MapFields(I_FIELDS, result[0])
+    return _UpdateBeparams(baserlib.MapFields(I_FIELDS, result[0]))
 
-  def DELETE(self):
+  def GetDeleteOpInput(self):
     """Delete an instance.
 
     """
-    op = opcodes.OpInstanceRemove(instance_name=self.items[0],
-                                  ignore_failures=False,
-                                  dry_run=bool(self.dryRun()))
-    return baserlib.SubmitJob([op])
+    assert len(self.items) == 1
+    return ({}, {
+      "instance_name": self.items[0],
+      "ignore_failures": False,
+      "dry_run": self.dryRun(),
+      })
 
 
-class R_2_instances_name_info(baserlib.R_Generic):
+class R_2_instances_name_info(baserlib.OpcodeResource):
   """/2/instances/[instance_name]/info resource.
 
   """
-  def GET(self):
+  GET_OPCODE = opcodes.OpInstanceQueryData
+
+  def GetGetOpInput(self):
     """Request detailed instance information.
 
     """
-    instance_name = self.items[0]
-    static = bool(self._checkIntVariable("static", default=0))
-
-    op = opcodes.OpInstanceQueryData(instances=[instance_name],
-                                     static=static)
-    return baserlib.SubmitJob([op])
+    assert len(self.items) == 1
+    return ({}, {
+      "instances": [self.items[0]],
+      "static": bool(self._checkIntVariable("static", default=0)),
+      })
 
 
-class R_2_instances_name_reboot(baserlib.R_Generic):
+class R_2_instances_name_reboot(baserlib.OpcodeResource):
   """/2/instances/[instance_name]/reboot resource.
 
   Implements an instance reboot.
 
   """
-  def POST(self):
+  POST_OPCODE = opcodes.OpInstanceReboot
+
+  def GetPostOpInput(self):
     """Reboot an instance.
 
     The URI takes type=[hard|soft|full] and
     ignore_secondaries=[False|True] parameters.
 
     """
-    instance_name = self.items[0]
-    reboot_type = self.queryargs.get("type",
-                                     [constants.INSTANCE_REBOOT_HARD])[0]
-    ignore_secondaries = bool(self._checkIntVariable("ignore_secondaries"))
-    op = opcodes.OpInstanceReboot(instance_name=instance_name,
-                                  reboot_type=reboot_type,
-                                  ignore_secondaries=ignore_secondaries,
-                                  dry_run=bool(self.dryRun()))
-
-    return baserlib.SubmitJob([op])
+    return ({}, {
+      "instance_name": self.items[0],
+      "reboot_type":
+        self.queryargs.get("type", [constants.INSTANCE_REBOOT_HARD])[0],
+      "ignore_secondaries": bool(self._checkIntVariable("ignore_secondaries")),
+      "dry_run": self.dryRun(),
+      })
 
 
-class R_2_instances_name_startup(baserlib.R_Generic):
+class R_2_instances_name_startup(baserlib.OpcodeResource):
   """/2/instances/[instance_name]/startup resource.
 
   Implements an instance startup.
 
   """
-  def PUT(self):
+  PUT_OPCODE = opcodes.OpInstanceStartup
+
+  def GetPutOpInput(self):
     """Startup an instance.
 
     The URI takes force=[False|True] parameter to start the instance
     if even if secondary disks are failing.
 
     """
-    instance_name = self.items[0]
-    force_startup = bool(self._checkIntVariable("force"))
-    no_remember = bool(self._checkIntVariable("no_remember"))
-    op = opcodes.OpInstanceStartup(instance_name=instance_name,
-                                   force=force_startup,
-                                   dry_run=bool(self.dryRun()),
-                                   no_remember=no_remember)
-
-    return baserlib.SubmitJob([op])
-
-
-def _ParseShutdownInstanceRequest(name, data, dry_run, no_remember):
-  """Parses a request for an instance shutdown.
-
-  @rtype: L{opcodes.OpInstanceShutdown}
-  @return: Instance shutdown opcode
-
-  """
-  return baserlib.FillOpcode(opcodes.OpInstanceShutdown, data, {
-    "instance_name": name,
-    "dry_run": dry_run,
-    "no_remember": no_remember,
-    })
+    return ({}, {
+      "instance_name": self.items[0],
+      "force": self.useForce(),
+      "dry_run": self.dryRun(),
+      "no_remember": bool(self._checkIntVariable("no_remember")),
+      })
 
 
-class R_2_instances_name_shutdown(baserlib.R_Generic):
+class R_2_instances_name_shutdown(baserlib.OpcodeResource):
   """/2/instances/[instance_name]/shutdown resource.
 
   Implements an instance shutdown.
 
   """
-  def PUT(self):
-    """Shutdown an instance.
+  PUT_OPCODE = opcodes.OpInstanceShutdown
 
-    @return: a job id
+  def GetPutOpInput(self):
+    """Shutdown an instance.
 
     """
-    no_remember = bool(self._checkIntVariable("no_remember"))
-    op = _ParseShutdownInstanceRequest(self.items[0], self.request_body,
-                                       bool(self.dryRun()), no_remember)
-
-    return baserlib.SubmitJob([op])
+    return (self.request_body, {
+      "instance_name": self.items[0],
+      "no_remember": bool(self._checkIntVariable("no_remember")),
+      "dry_run": self.dryRun(),
+      })
 
 
 def _ParseInstanceReinstallRequest(name, data):
@@ -953,12 +952,14 @@ def _ParseInstanceReinstallRequest(name, data):
   return ops
 
 
-class R_2_instances_name_reinstall(baserlib.R_Generic):
+class R_2_instances_name_reinstall(baserlib.OpcodeResource):
   """/2/instances/[instance_name]/reinstall resource.
 
   Implements an instance reinstall.
 
   """
+  POST_OPCODE = opcodes.OpInstanceReinstall
+
   def POST(self):
     """Reinstall an instance.
 
@@ -983,283 +984,219 @@ class R_2_instances_name_reinstall(baserlib.R_Generic):
 
     ops = _ParseInstanceReinstallRequest(self.items[0], body)
 
-    return baserlib.SubmitJob(ops)
-
-
-def _ParseInstanceReplaceDisksRequest(name, data):
-  """Parses a request for an instance export.
+    return self.SubmitJob(ops)
 
-  @rtype: L{opcodes.OpInstanceReplaceDisks}
-  @return: Instance export opcode
 
-  """
-  override = {
-    "instance_name": name,
-    }
-
-  # Parse disks
-  try:
-    raw_disks = data.pop("disks")
-  except KeyError:
-    pass
-  else:
-    if raw_disks:
-      if ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
-        data["disks"] = raw_disks
-      else:
-        # Backwards compatibility for strings of the format "1, 2, 3"
-        try:
-          data["disks"] = [int(part) for part in raw_disks.split(",")]
-        except (TypeError, ValueError), err:
-          raise http.HttpBadRequest("Invalid disk index passed: %s" % str(err))
-
-  return baserlib.FillOpcode(opcodes.OpInstanceReplaceDisks, data, override)
-
-
-class R_2_instances_name_replace_disks(baserlib.R_Generic):
+class R_2_instances_name_replace_disks(baserlib.OpcodeResource):
   """/2/instances/[instance_name]/replace-disks resource.
 
   """
-  def POST(self):
+  POST_OPCODE = opcodes.OpInstanceReplaceDisks
+
+  def GetPostOpInput(self):
     """Replaces disks on an instance.
 
     """
+    static = {
+      "instance_name": self.items[0],
+      }
+
     if self.request_body:
-      body = self.request_body
+      data = self.request_body
     elif self.queryargs:
       # Legacy interface, do not modify/extend
-      body = {
+      data = {
         "remote_node": self._checkStringVariable("remote_node", default=None),
         "mode": self._checkStringVariable("mode", default=None),
         "disks": self._checkStringVariable("disks", default=None),
         "iallocator": self._checkStringVariable("iallocator", default=None),
         }
     else:
-      body = {}
+      data = {}
 
-    op = _ParseInstanceReplaceDisksRequest(self.items[0], body)
+    # Parse disks
+    try:
+      raw_disks = data.pop("disks")
+    except KeyError:
+      pass
+    else:
+      if raw_disks:
+        if ht.TListOf(ht.TInt)(raw_disks): # pylint: disable=E1102
+          data["disks"] = raw_disks
+        else:
+          # Backwards compatibility for strings of the format "1, 2, 3"
+          try:
+            data["disks"] = [int(part) for part in raw_disks.split(",")]
+          except (TypeError, ValueError), err:
+            raise http.HttpBadRequest("Invalid disk index passed: %s" % err)
 
-    return baserlib.SubmitJob([op])
+    return (data, static)
 
 
-class R_2_instances_name_activate_disks(baserlib.R_Generic):
+class R_2_instances_name_activate_disks(baserlib.OpcodeResource):
   """/2/instances/[instance_name]/activate-disks resource.
 
   """
-  def PUT(self):
+  PUT_OPCODE = opcodes.OpInstanceActivateDisks
+
+  def GetPutOpInput(self):
     """Activate disks for an instance.
 
     The URI might contain ignore_size to ignore current recorded size.
 
     """
-    instance_name = self.items[0]
-    ignore_size = bool(self._checkIntVariable("ignore_size"))
-
-    op = opcodes.OpInstanceActivateDisks(instance_name=instance_name,
-                                         ignore_size=ignore_size)
-
-    return baserlib.SubmitJob([op])
+    return ({}, {
+      "instance_name": self.items[0],
+      "ignore_size": bool(self._checkIntVariable("ignore_size")),
+      })
 
 
-class R_2_instances_name_deactivate_disks(baserlib.R_Generic):
+class R_2_instances_name_deactivate_disks(baserlib.OpcodeResource):
   """/2/instances/[instance_name]/deactivate-disks resource.
 
   """
-  def PUT(self):
+  PUT_OPCODE = opcodes.OpInstanceDeactivateDisks
+
+  def GetPutOpInput(self):
     """Deactivate disks for an instance.
 
     """
-    instance_name = self.items[0]
-
-    op = opcodes.OpInstanceDeactivateDisks(instance_name=instance_name)
-
-    return baserlib.SubmitJob([op])
+    return ({}, {
+      "instance_name": self.items[0],
+      })
 
 
-class R_2_instances_name_prepare_export(baserlib.R_Generic):
-  """/2/instances/[instance_name]/prepare-export resource.
+class R_2_instances_name_recreate_disks(baserlib.OpcodeResource):
+  """/2/instances/[instance_name]/recreate-disks resource.
 
   """
-  def PUT(self):
-    """Prepares an export for an instance.
+  POST_OPCODE = opcodes.OpInstanceRecreateDisks
 
-    @return: a job id
+  def GetPostOpInput(self):
+    """Recreate disks for an instance.
 
     """
-    instance_name = self.items[0]
-    mode = self._checkStringVariable("mode")
-
-    op = opcodes.OpBackupPrepare(instance_name=instance_name,
-                                 mode=mode)
-
-    return baserlib.SubmitJob([op])
-
+    return ({}, {
+      "instance_name": self.items[0],
+      })
 
-def _ParseExportInstanceRequest(name, data):
-  """Parses a request for an instance export.
 
-  @rtype: L{opcodes.OpBackupExport}
-  @return: Instance export opcode
+class R_2_instances_name_prepare_export(baserlib.OpcodeResource):
+  """/2/instances/[instance_name]/prepare-export resource.
 
   """
-  # Rename "destination" to "target_node"
-  try:
-    data["target_node"] = data.pop("destination")
-  except KeyError:
-    pass
+  PUT_OPCODE = opcodes.OpBackupPrepare
 
-  return baserlib.FillOpcode(opcodes.OpBackupExport, data, {
-    "instance_name": name,
-    })
+  def GetPutOpInput(self):
+    """Prepares an export for an instance.
+
+    """
+    return ({}, {
+      "instance_name": self.items[0],
+      "mode": self._checkStringVariable("mode"),
+      })
 
 
-class R_2_instances_name_export(baserlib.R_Generic):
+class R_2_instances_name_export(baserlib.OpcodeResource):
   """/2/instances/[instance_name]/export resource.
 
   """
-  def PUT(self):
-    """Exports an instance.
+  PUT_OPCODE = opcodes.OpBackupExport
+  PUT_RENAME = {
+    "destination": "target_node",
+    }
 
-    @return: a job id
+  def GetPutOpInput(self):
+    """Exports an instance.
 
     """
-    if not isinstance(self.request_body, dict):
-      raise http.HttpBadRequest("Invalid body contents, not a dictionary")
-
-    op = _ParseExportInstanceRequest(self.items[0], self.request_body)
-
-    return baserlib.SubmitJob([op])
-
-
-def _ParseMigrateInstanceRequest(name, data):
-  """Parses a request for an instance migration.
-
-  @rtype: L{opcodes.OpInstanceMigrate}
-  @return: Instance migration opcode
-
-  """
-  return baserlib.FillOpcode(opcodes.OpInstanceMigrate, data, {
-    "instance_name": name,
-    })
+    return (self.request_body, {
+      "instance_name": self.items[0],
+      })
 
 
-class R_2_instances_name_migrate(baserlib.R_Generic):
+class R_2_instances_name_migrate(baserlib.OpcodeResource):
   """/2/instances/[instance_name]/migrate resource.
 
   """
-  def PUT(self):
-    """Migrates an instance.
+  PUT_OPCODE = opcodes.OpInstanceMigrate
 
-    @return: a job id
+  def GetPutOpInput(self):
+    """Migrates an instance.
 
     """
-    baserlib.CheckType(self.request_body, dict, "Body contents")
-
-    op = _ParseMigrateInstanceRequest(self.items[0], self.request_body)
-
-    return baserlib.SubmitJob([op])
+    return (self.request_body, {
+      "instance_name": self.items[0],
+      })
 
 
-class R_2_instances_name_failover(baserlib.R_Generic):
+class R_2_instances_name_failover(baserlib.OpcodeResource):
   """/2/instances/[instance_name]/failover resource.
 
   """
-  def PUT(self):
-    """Does a failover of an instance.
+  PUT_OPCODE = opcodes.OpInstanceFailover
 
-    @return: a job id
+  def GetPutOpInput(self):
+    """Does a failover of an instance.
 
     """
-    baserlib.CheckType(self.request_body, dict, "Body contents")
-
-    op = baserlib.FillOpcode(opcodes.OpInstanceFailover, self.request_body, {
+    return (self.request_body, {
       "instance_name": self.items[0],
       })
 
-    return baserlib.SubmitJob([op])
 
-
-def _ParseRenameInstanceRequest(name, data):
-  """Parses a request for renaming an instance.
-
-  @rtype: L{opcodes.OpInstanceRename}
-  @return: Instance rename opcode
-
-  """
-  return baserlib.FillOpcode(opcodes.OpInstanceRename, data, {
-    "instance_name": name,
-    })
-
-
-class R_2_instances_name_rename(baserlib.R_Generic):
+class R_2_instances_name_rename(baserlib.OpcodeResource):
   """/2/instances/[instance_name]/rename resource.
 
   """
-  def PUT(self):
-    """Changes the name of an instance.
+  PUT_OPCODE = opcodes.OpInstanceRename
 
-    @return: a job id
+  def GetPutOpInput(self):
+    """Changes the name of an instance.
 
     """
-    baserlib.CheckType(self.request_body, dict, "Body contents")
-
-    op = _ParseRenameInstanceRequest(self.items[0], self.request_body)
-
-    return baserlib.SubmitJob([op])
-
-
-def _ParseModifyInstanceRequest(name, data):
-  """Parses a request for modifying an instance.
-
-  @rtype: L{opcodes.OpInstanceSetParams}
-  @return: Instance modify opcode
-
-  """
-  return baserlib.FillOpcode(opcodes.OpInstanceSetParams, data, {
-    "instance_name": name,
-    })
+    return (self.request_body, {
+      "instance_name": self.items[0],
+      })
 
 
-class R_2_instances_name_modify(baserlib.R_Generic):
+class R_2_instances_name_modify(baserlib.OpcodeResource):
   """/2/instances/[instance_name]/modify resource.
 
   """
-  def PUT(self):
-    """Changes some parameters of an instance.
+  PUT_OPCODE = opcodes.OpInstanceSetParams
 
-    @return: a job id
+  def GetPutOpInput(self):
+    """Changes parameters of an instance.
 
     """
-    baserlib.CheckType(self.request_body, dict, "Body contents")
-
-    op = _ParseModifyInstanceRequest(self.items[0], self.request_body)
-
-    return baserlib.SubmitJob([op])
+    return (self.request_body, {
+      "instance_name": self.items[0],
+      })
 
 
-class R_2_instances_name_disk_grow(baserlib.R_Generic):
+class R_2_instances_name_disk_grow(baserlib.OpcodeResource):
   """/2/instances/[instance_name]/disk/[disk_index]/grow resource.
 
   """
-  def POST(self):
-    """Increases the size of an instance disk.
+  POST_OPCODE = opcodes.OpInstanceGrowDisk
 
-    @return: a job id
+  def GetPostOpInput(self):
+    """Increases the size of an instance disk.
 
     """
-    op = baserlib.FillOpcode(opcodes.OpInstanceGrowDisk, self.request_body, {
+    return (self.request_body, {
       "instance_name": self.items[0],
       "disk": int(self.items[1]),
       })
 
-    return baserlib.SubmitJob([op])
 
-
-class R_2_instances_name_console(baserlib.R_Generic):
+class R_2_instances_name_console(baserlib.ResourceBase):
   """/2/instances/[instance_name]/console resource.
 
   """
   GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
+  GET_OPCODE = opcodes.OpInstanceConsole
 
   def GET(self):
     """Request information for connecting to instance's console.
@@ -1268,7 +1205,7 @@ class R_2_instances_name_console(baserlib.R_Generic):
              L{objects.InstanceConsole}
 
     """
-    client = baserlib.GetClient()
+    client = self.GetClient()
 
     ((console, ), ) = client.QueryInstances([self.items[0]], ["console"], False)
 
@@ -1298,15 +1235,17 @@ def _SplitQueryFields(fields):
   return [i.strip() for i in fields.split(",")]
 
 
-class R_2_query(baserlib.R_Generic):
+class R_2_query(baserlib.ResourceBase):
   """/2/query/[resource] resource.
 
   """
   # Results might contain sensitive information
   GET_ACCESS = [rapi.RAPI_ACCESS_WRITE]
+  GET_OPCODE = opcodes.OpQuery
+  PUT_OPCODE = opcodes.OpQuery
 
-  def _Query(self, fields, filter_):
-    return baserlib.GetClient().Query(self.items[0], fields, filter_).ToDict()
+  def _Query(self, fields, qfilter):
+    return self.GetClient().Query(self.items[0], fields, qfilter).ToDict()
 
   def GET(self):
     """Returns resource information.
@@ -1331,13 +1270,20 @@ class R_2_query(baserlib.R_Generic):
     except KeyError:
       fields = _GetQueryFields(self.queryargs)
 
-    return self._Query(fields, self.request_body.get("filter", None))
+    qfilter = body.get("qfilter", None)
+    # TODO: remove this after 2.7
+    if qfilter is None:
+      qfilter = body.get("filter", None)
+
+    return self._Query(fields, qfilter)
 
 
-class R_2_query_fields(baserlib.R_Generic):
+class R_2_query_fields(baserlib.ResourceBase):
   """/2/query/[resource]/fields resource.
 
   """
+  GET_OPCODE = opcodes.OpQueryFields
+
   def GET(self):
     """Retrieves list of available fields for a resource.
 
@@ -1351,25 +1297,28 @@ class R_2_query_fields(baserlib.R_Generic):
     else:
       fields = _SplitQueryFields(raw_fields[0])
 
-    return baserlib.GetClient().QueryFields(self.items[0], fields).ToDict()
+    return self.GetClient().QueryFields(self.items[0], fields).ToDict()
 
 
-class _R_Tags(baserlib.R_Generic):
-  """ Quasiclass for tagging resources
+class _R_Tags(baserlib.OpcodeResource):
+  """Quasiclass for tagging resources.
 
   Manages tags. When inheriting this class you must define the
   TAG_LEVEL for it.
 
   """
   TAG_LEVEL = None
+  GET_OPCODE = opcodes.OpTagsGet
+  PUT_OPCODE = opcodes.OpTagsSet
+  DELETE_OPCODE = opcodes.OpTagsDel
 
-  def __init__(self, items, queryargs, req):
+  def __init__(self, items, queryargs, req, **kwargs):
     """A tag resource constructor.
 
     We have to override the default to sort out cluster naming case.
 
     """
-    baserlib.R_Generic.__init__(self, items, queryargs, req)
+    baserlib.OpcodeResource.__init__(self, items, queryargs, req, **kwargs)
 
     if self.TAG_LEVEL == constants.TAG_CLUSTER:
       self.name = None
@@ -1382,25 +1331,49 @@ class _R_Tags(baserlib.R_Generic):
     Example: ["tag1", "tag2", "tag3"]
 
     """
-    # pylint: disable=W0212
-    return baserlib._Tags_GET(self.TAG_LEVEL, name=self.name)
+    kind = self.TAG_LEVEL
+
+    if kind in (constants.TAG_INSTANCE,
+                constants.TAG_NODEGROUP,
+                constants.TAG_NODE):
+      if not self.name:
+        raise http.HttpBadRequest("Missing name on tag request")
+
+      cl = self.GetClient()
+      if kind == constants.TAG_INSTANCE:
+        fn = cl.QueryInstances
+      elif kind == constants.TAG_NODEGROUP:
+        fn = cl.QueryGroups
+      else:
+        fn = cl.QueryNodes
+      result = fn(names=[self.name], fields=["tags"], use_locking=False)
+      if not result or not result[0]:
+        raise http.HttpBadGateway("Invalid response from tag query")
+      tags = result[0][0]
 
-  def PUT(self):
+    elif kind == constants.TAG_CLUSTER:
+      assert not self.name
+      # TODO: Use query API?
+      ssc = ssconf.SimpleStore()
+      tags = ssc.GetClusterTags()
+
+    return list(tags)
+
+  def GetPutOpInput(self):
     """Add a set of tags.
 
     The request as a list of strings should be PUT to this URI. And
     you'll have back a job id.
 
     """
-    # pylint: disable=W0212
-    if "tag" not in self.queryargs:
-      raise http.HttpBadRequest("Please specify tag(s) to add using the"
-                                " the 'tag' parameter")
-    return baserlib._Tags_PUT(self.TAG_LEVEL,
-                              self.queryargs["tag"], name=self.name,
-                              dry_run=bool(self.dryRun()))
+    return ({}, {
+      "kind": self.TAG_LEVEL,
+      "name": self.name,
+      "tags": self.queryargs.get("tag", []),
+      "dry_run": self.dryRun(),
+      })
 
-  def DELETE(self):
+  def GetDeleteOpInput(self):
     """Delete a tag.
 
     In order to delete a set of tags, the DELETE
@@ -1408,15 +1381,8 @@ class _R_Tags(baserlib.R_Generic):
     /tags?tag=[tag]&tag=[tag]
 
     """
-    # pylint: disable=W0212
-    if "tag" not in self.queryargs:
-      # no we not gonna delete all tags
-      raise http.HttpBadRequest("Cannot delete all tags - please specify"
-                                " tag(s) using the 'tag' parameter")
-    return baserlib._Tags_DELETE(self.TAG_LEVEL,
-                                 self.queryargs["tag"],
-                                 name=self.name,
-                                 dry_run=bool(self.dryRun()))
+    # Re-use code
+    return self.GetPutOpInput()
 
 
 class R_2_instances_name_tags(_R_Tags):
diff --git a/lib/rapi/testutils.py b/lib/rapi/testutils.py
new file mode 100644 (file)
index 0000000..f960381
--- /dev/null
@@ -0,0 +1,371 @@
+#
+#
+
+# Copyright (C) 2012 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.
+
+
+"""Remote API test utilities.
+
+"""
+
+import logging
+import re
+import mimetools
+import base64
+import pycurl
+from cStringIO import StringIO
+
+from ganeti import errors
+from ganeti import opcodes
+from ganeti import http
+from ganeti import server
+from ganeti import utils
+from ganeti import compat
+from ganeti import luxi
+from ganeti import rapi
+
+import ganeti.http.server # pylint: disable=W0611
+import ganeti.server.rapi
+import ganeti.rapi.client
+
+
+_URI_RE = re.compile(r"https://(?P<host>.*):(?P<port>\d+)(?P<path>/.*)")
+
+
+class VerificationError(Exception):
+  """Dedicated error class for test utilities.
+
+  This class is used to hide all of Ganeti's internal exception, so that
+  external users of these utilities don't have to integrate Ganeti's exception
+  hierarchy.
+
+  """
+
+
+def _GetOpById(op_id):
+  """Tries to get an opcode class based on its C{OP_ID}.
+
+  """
+  try:
+    return opcodes.OP_MAPPING[op_id]
+  except KeyError:
+    raise VerificationError("Unknown opcode ID '%s'" % op_id)
+
+
+def _HideInternalErrors(fn):
+  """Hides Ganeti-internal exceptions, see L{VerificationError}.
+
+  """
+  def wrapper(*args, **kwargs):
+    try:
+      return fn(*args, **kwargs)
+    except (errors.GenericError, rapi.client.GanetiApiError), err:
+      raise VerificationError("Unhandled Ganeti error: %s" % err)
+
+  return wrapper
+
+
+@_HideInternalErrors
+def VerifyOpInput(op_id, data):
+  """Verifies opcode parameters according to their definition.
+
+  @type op_id: string
+  @param op_id: Opcode ID (C{OP_ID} attribute), e.g. C{OP_CLUSTER_VERIFY}
+  @type data: dict
+  @param data: Opcode parameter values
+  @raise VerificationError: Parameter verification failed
+
+  """
+  op_cls = _GetOpById(op_id)
+
+  try:
+    op = op_cls(**data) # pylint: disable=W0142
+  except TypeError, err:
+    raise VerificationError("Unable to create opcode instance: %s" % err)
+
+  try:
+    op.Validate(False)
+  except errors.OpPrereqError, err:
+    raise VerificationError("Parameter validation for opcode '%s' failed: %s" %
+                            (op_id, err))
+
+
+@_HideInternalErrors
+def VerifyOpResult(op_id, result):
+  """Verifies opcode results used in tests (e.g. in a mock).
+
+  @type op_id: string
+  @param op_id: Opcode ID (C{OP_ID} attribute), e.g. C{OP_CLUSTER_VERIFY}
+  @param result: Mocked opcode result
+  @raise VerificationError: Return value verification failed
+
+  """
+  resultcheck_fn = _GetOpById(op_id).OP_RESULT
+
+  if not resultcheck_fn:
+    logging.warning("Opcode '%s' has no result type definition", op_id)
+  elif not resultcheck_fn(result):
+    raise VerificationError("Given result does not match result description"
+                            " for opcode '%s': %s" % (op_id, resultcheck_fn))
+
+
+def _GetPathFromUri(uri):
+  """Gets the path and query from a URI.
+
+  """
+  match = _URI_RE.match(uri)
+  if match:
+    return match.groupdict()["path"]
+  else:
+    return None
+
+
+class FakeCurl:
+  """Fake cURL object.
+
+  """
+  def __init__(self, handler):
+    """Initialize this class
+
+    @param handler: Request handler instance
+
+    """
+    self._handler = handler
+    self._opts = {}
+    self._info = {}
+
+  def setopt(self, opt, value):
+    self._opts[opt] = value
+
+  def getopt(self, opt):
+    return self._opts.get(opt)
+
+  def unsetopt(self, opt):
+    self._opts.pop(opt, None)
+
+  def getinfo(self, info):
+    return self._info[info]
+
+  def perform(self):
+    method = self._opts[pycurl.CUSTOMREQUEST]
+    url = self._opts[pycurl.URL]
+    request_body = self._opts[pycurl.POSTFIELDS]
+    writefn = self._opts[pycurl.WRITEFUNCTION]
+
+    if pycurl.HTTPHEADER in self._opts:
+      baseheaders = "\n".join(self._opts[pycurl.HTTPHEADER])
+    else:
+      baseheaders = ""
+
+    headers = mimetools.Message(StringIO(baseheaders), 0)
+
+    if request_body:
+      headers[http.HTTP_CONTENT_LENGTH] = str(len(request_body))
+
+    if self._opts.get(pycurl.HTTPAUTH, 0) & pycurl.HTTPAUTH_BASIC:
+      try:
+        userpwd = self._opts[pycurl.USERPWD]
+      except KeyError:
+        raise errors.ProgrammerError("Basic authentication requires username"
+                                     " and password")
+
+      headers[http.HTTP_AUTHORIZATION] = \
+        "%s %s" % (http.auth.HTTP_BASIC_AUTH, base64.b64encode(userpwd))
+
+    path = _GetPathFromUri(url)
+    (code, resp_body) = \
+      self._handler.FetchResponse(path, method, headers, request_body)
+
+    self._info[pycurl.RESPONSE_CODE] = code
+    if resp_body is not None:
+      writefn(resp_body)
+
+
+class _RapiMock:
+  """Mocking out the RAPI server parts.
+
+  """
+  def __init__(self, user_fn, luxi_client):
+    """Initialize this class.
+
+    @type user_fn: callable
+    @param user_fn: Function to authentication username
+    @param luxi_client: A LUXI client implementation
+
+    """
+    self.handler = \
+      server.rapi.RemoteApiHandler(user_fn, _client_cls=luxi_client)
+
+  def FetchResponse(self, path, method, headers, request_body):
+    """This is a callback method used to fetch a response.
+
+    This method is called by the FakeCurl.perform method
+
+    @type path: string
+    @param path: Requested path
+    @type method: string
+    @param method: HTTP method
+    @type request_body: string
+    @param request_body: Request body
+    @type headers: mimetools.Message
+    @param headers: Request headers
+    @return: Tuple containing status code and response body
+
+    """
+    req_msg = http.HttpMessage()
+    req_msg.start_line = \
+      http.HttpClientToServerStartLine(method, path, http.HTTP_1_0)
+    req_msg.headers = headers
+    req_msg.body = request_body
+
+    (_, _, _, resp_msg) = \
+      http.server.HttpResponder(self.handler)(lambda: (req_msg, None))
+
+    return (resp_msg.start_line.code, resp_msg.body)
+
+
+class _TestLuxiTransport:
+  """Mocked LUXI transport.
+
+  Raises L{errors.RapiTestResult} for all method calls, no matter the
+  arguments.
+
+  """
+  def __init__(self, record_fn, address, timeouts=None): # pylint: disable=W0613
+    """Initializes this class.
+
+    """
+    self._record_fn = record_fn
+
+  def Close(self):
+    pass
+
+  def Call(self, data):
+    """Calls LUXI method.
+
+    In this test class the method is not actually called, but added to a list
+    of called methods and then an exception (L{errors.RapiTestResult}) is
+    raised. There is no return value.
+
+    """
+    (method, _, _) = luxi.ParseRequest(data)
+
+    # Take a note of called method
+    self._record_fn(method)
+
+    # Everything went fine until here, so let's abort the test
+    raise errors.RapiTestResult
+
+
+class _LuxiCallRecorder:
+  """Records all called LUXI client methods.
+
+  """
+  def __init__(self):
+    """Initializes this class.
+
+    """
+    self._called = set()
+
+  def Record(self, name):
+    """Records a called function name.
+
+    """
+    self._called.add(name)
+
+  def CalledNames(self):
+    """Returns a list of called LUXI methods.
+
+    """
+    return self._called
+
+  def __call__(self):
+    """Creates an instrumented LUXI client.
+
+    The LUXI client will record all method calls (use L{CalledNames} to
+    retrieve them).
+
+    """
+    return luxi.Client(transport=compat.partial(_TestLuxiTransport,
+                                                self.Record))
+
+
+def _TestWrapper(fn, *args, **kwargs):
+  """Wrapper for ignoring L{errors.RapiTestResult}.
+
+  """
+  try:
+    return fn(*args, **kwargs)
+  except errors.RapiTestResult:
+    # Everything was fine up to the point of sending a LUXI request
+    return NotImplemented
+
+
+class InputTestClient:
+  """Test version of RAPI client.
+
+  Instances of this class can be used to test input arguments for RAPI client
+  calls. See L{rapi.client.GanetiRapiClient} for available methods and their
+  arguments. Functions can return C{NotImplemented} if all arguments are
+  acceptable, but a LUXI request would be necessary to provide an actual return
+  value. In case of an error, L{VerificationError} is raised.
+
+  @see: An example on how to use this class can be found in
+    C{doc/examples/rapi_testutils.py}
+
+  """
+  def __init__(self):
+    """Initializes this class.
+
+    """
+    username = utils.GenerateSecret()
+    password = utils.GenerateSecret()
+
+    def user_fn(wanted):
+      """Called to verify user credentials given in HTTP request.
+
+      """
+      assert username == wanted
+      return http.auth.PasswordFileUser(username, password,
+                                        [rapi.RAPI_ACCESS_WRITE])
+
+    self._lcr = _LuxiCallRecorder()
+
+    # Create a mock RAPI server
+    handler = _RapiMock(user_fn, self._lcr)
+
+    self._client = \
+      rapi.client.GanetiRapiClient("master.example.com",
+                                   username=username, password=password,
+                                   curl_factory=lambda: FakeCurl(handler))
+
+  def _GetLuxiCalls(self):
+    """Returns the names of all called LUXI client functions.
+
+    """
+    return self._lcr.CalledNames()
+
+  def __getattr__(self, name):
+    """Finds method by name.
+
+    The method is wrapped using L{_TestWrapper} to produce the actual test
+    result.
+
+    """
+    return _HideInternalErrors(compat.partial(_TestWrapper,
+                                              getattr(self._client, name)))
index 8c96eb5..136532f 100644 (file)
@@ -30,7 +30,6 @@
 # if they need to start using instance attributes
 # R0904: Too many public methods
 
-import os
 import logging
 import zlib
 import base64
@@ -46,6 +45,11 @@ from ganeti import errors
 from ganeti import netutils
 from ganeti import ssconf
 from ganeti import runtime
+from ganeti import compat
+from ganeti import rpc_defs
+
+# Special module generated at build time
+from ganeti import _generated_rpc
 
 # pylint has a bug here, doesn't see this import
 import ganeti.http.client  # pylint: disable=W0611
@@ -67,15 +71,8 @@ _TMO_SLOW = 3600 # one hour
 _TMO_4HRS = 4 * 3600
 _TMO_1DAY = 86400
 
-# Timeout table that will be built later by decorators
-# Guidelines for choosing timeouts:
-# - call used during watcher: timeout -> 1min, _TMO_URGENT
-# - trivial (but be sure it is trivial) (e.g. reading a file): 5min, _TMO_FAST
-# - other calls: 15 min, _TMO_NORMAL
-# - special calls (instance add, etc.): either _TMO_SLOW (1h) or huge timeouts
-
-_TIMEOUTS = {
-}
+#: Special value to describe an offline host
+_OFFLINE = object()
 
 
 def Init():
@@ -120,49 +117,6 @@ def _ConfigRpcCurl(curl):
   curl.setopt(pycurl.CONNECTTIMEOUT, _RPC_CONNECT_TIMEOUT)
 
 
-# Aliasing this module avoids the following warning by epydoc: "Warning: No
-# information available for ganeti.rpc._RpcThreadLocal's base threading.local"
-_threading = threading
-
-
-class _RpcThreadLocal(_threading.local):
-  def GetHttpClientPool(self):
-    """Returns a per-thread HTTP client pool.
-
-    @rtype: L{http.client.HttpClientPool}
-
-    """
-    try:
-      pool = self.hcp
-    except AttributeError:
-      pool = http.client.HttpClientPool(_ConfigRpcCurl)
-      self.hcp = pool
-
-    return pool
-
-
-# Remove module alias (see above)
-del _threading
-
-
-_thread_local = _RpcThreadLocal()
-
-
-def _RpcTimeout(secs):
-  """Timeout decorator.
-
-  When applied to a rpc call_* function, it updates the global timeout
-  table with the given function/timeout.
-
-  """
-  def decorator(f):
-    name = f.__name__
-    assert name.startswith("call_")
-    _TIMEOUTS[name[len("call_"):]] = secs
-    return f
-  return decorator
-
-
 def RunWithRPC(fn):
   """RPC-wrapper decorator.
 
@@ -180,6 +134,26 @@ def RunWithRPC(fn):
   return wrapper
 
 
+def _Compress(data):
+  """Compresses a string for transport over RPC.
+
+  Small amounts of data are not compressed.
+
+  @type data: str
+  @param data: Data
+  @rtype: tuple
+  @return: Encoded data to send
+
+  """
+  # Small amounts of data are not compressed
+  if len(data) < 512:
+    return (constants.RPC_ENCODING_NONE, data)
+
+  # Compress with zlib and encode in base64
+  return (constants.RPC_ENCODING_ZLIB_BASE64,
+          base64.b64encode(zlib.compress(data, 3)))
+
+
 class RpcResult(object):
   """RPC Result class.
 
@@ -265,1333 +239,601 @@ class RpcResult(object):
     raise ec(*args) # pylint: disable=W0142
 
 
-def _AddressLookup(node_list,
-                   ssc=ssconf.SimpleStore,
-                   nslookup_fn=netutils.Hostname.GetIP):
+def _SsconfResolver(ssconf_ips, node_list, _,
+                    ssc=ssconf.SimpleStore,
+                    nslookup_fn=netutils.Hostname.GetIP):
   """Return addresses for given node names.
 
+  @type ssconf_ips: bool
+  @param ssconf_ips: Use the ssconf IPs
   @type node_list: list
   @param node_list: List of node names
   @type ssc: class
   @param ssc: SimpleStore class that is used to obtain node->ip mappings
   @type nslookup_fn: callable
   @param nslookup_fn: function use to do NS lookup
-  @rtype: list of addresses and/or None's
-  @returns: List of corresponding addresses, if found
+  @rtype: list of tuple; (string, string)
+  @return: List of tuples containing node name and IP address
 
   """
   ss = ssc()
-  iplist = ss.GetNodePrimaryIPList()
   family = ss.GetPrimaryIPFamily()
-  addresses = []
-  ipmap = dict(entry.split() for entry in iplist)
-  for node in node_list:
-    address = ipmap.get(node)
-    if address is None:
-      address = nslookup_fn(node, family=family)
-    addresses.append(address)
 
-  return addresses
+  if ssconf_ips:
+    iplist = ss.GetNodePrimaryIPList()
+    ipmap = dict(entry.split() for entry in iplist)
+  else:
+    ipmap = {}
 
+  result = []
+  for node in node_list:
+    ip = ipmap.get(node)
+    if ip is None:
+      ip = nslookup_fn(node, family=family)
+    result.append((node, ip))
 
-class Client:
-  """RPC Client class.
+  return result
 
-  This class, given a (remote) method name, a list of parameters and a
-  list of nodes, will contact (in parallel) all nodes, and return a
-  dict of results (key: node name, value: result).
 
-  One current bug is that generic failure is still signaled by
-  'False' result, which is not good. This overloading of values can
-  cause bugs.
-
-  """
-  def __init__(self, procedure, body, port, address_lookup_fn=_AddressLookup):
-    assert procedure in _TIMEOUTS, ("New RPC call not declared in the"
-                                    " timeouts table")
-    self.procedure = procedure
-    self.body = body
-    self.port = port
-    self._request = {}
-    self._address_lookup_fn = address_lookup_fn
-
-  def ConnectList(self, node_list, address_list=None, read_timeout=None):
-    """Add a list of nodes to the target nodes.
-
-    @type node_list: list
-    @param node_list: the list of node names to connect
-    @type address_list: list or None
-    @keyword address_list: either None or a list with node addresses,
-        which must have the same length as the node list
-    @type read_timeout: int
-    @param read_timeout: overwrites default timeout for operation
+class _StaticResolver:
+  def __init__(self, addresses):
+    """Initializes this class.
 
     """
-    if address_list is None:
-      # Always use IP address instead of node name
-      address_list = self._address_lookup_fn(node_list)
-
-    assert len(node_list) == len(address_list), \
-           "Name and address lists must have the same length"
+    self._addresses = addresses
 
-    for node, address in zip(node_list, address_list):
-      self.ConnectNode(node, address, read_timeout=read_timeout)
-
-  def ConnectNode(self, name, address=None, read_timeout=None):
-    """Add a node to the target list.
-
-    @type name: str
-    @param name: the node name
-    @type address: str
-    @param address: the node address, if known
-    @type read_timeout: int
-    @param read_timeout: overwrites default timeout for operation
+  def __call__(self, hosts, _):
+    """Returns static addresses for hosts.
 
     """
-    if address is None:
-      # Always use IP address instead of node name
-      address = self._address_lookup_fn([name])[0]
-
-    assert(address is not None)
+    assert len(hosts) == len(self._addresses)
+    return zip(hosts, self._addresses)
 
-    if read_timeout is None:
-      read_timeout = _TIMEOUTS[self.procedure]
 
-    self._request[name] = \
-      http.client.HttpClientRequest(str(address), self.port,
-                                    http.HTTP_PUT, str("/%s" % self.procedure),
-                                    headers=_RPC_CLIENT_HEADERS,
-                                    post_data=str(self.body),
-                                    read_timeout=read_timeout)
+def _CheckConfigNode(name, node, accept_offline_node):
+  """Checks if a node is online.
 
-  def GetResults(self, http_pool=None):
-    """Call nodes and return results.
-
-    @rtype: list
-    @return: List of RPC results
-
-    """
-    if not http_pool:
-      http_pool = http.client.HttpClientPool(_ConfigRpcCurl)
-
-    http_pool.ProcessRequests(self._request.values())
-
-    results = {}
-
-    for name, req in self._request.iteritems():
-      if req.success and req.resp_status_code == http.HTTP_OK:
-        results[name] = RpcResult(data=serializer.LoadJson(req.resp_body),
-                                  node=name, call=self.procedure)
-        continue
-
-      # TODO: Better error reporting
-      if req.error:
-        msg = req.error
-      else:
-        msg = req.resp_body
-
-      logging.error("RPC error in %s from node %s: %s",
-                    self.procedure, name, msg)
-      results[name] = RpcResult(data=msg, failed=True, node=name,
-                                call=self.procedure)
-
-    return results
-
-
-def _EncodeImportExportIO(ieio, ieioargs):
-  """Encodes import/export I/O information.
+  @type name: string
+  @param name: Node name
+  @type node: L{objects.Node} or None
+  @param node: Node object
 
   """
-  if ieio == constants.IEIO_RAW_DISK:
-    assert len(ieioargs) == 1
-    return (ieioargs[0].ToDict(), )
+  if node is None:
+    # Depend on DNS for name resolution
+    ip = name
+  elif node.offline and not accept_offline_node:
+    ip = _OFFLINE
+  else:
+    ip = node.primary_ip
+  return (name, ip)
 
-  if ieio == constants.IEIO_SCRIPT:
-    assert len(ieioargs) == 2
-    return (ieioargs[0].ToDict(), ieioargs[1])
 
-  return ieioargs
+def _NodeConfigResolver(single_node_fn, all_nodes_fn, hosts, opts):
+  """Calculate node addresses using configuration.
 
+  """
+  accept_offline_node = (opts is rpc_defs.ACCEPT_OFFLINE_NODE)
 
-class RpcRunner(object):
-  """RPC runner class"""
+  assert accept_offline_node or opts is None, "Unknown option"
 
-  def __init__(self, cfg):
-    """Initialized the rpc runner.
+  # Special case for single-host lookups
+  if len(hosts) == 1:
+    (name, ) = hosts
+    return [_CheckConfigNode(name, single_node_fn(name), accept_offline_node)]
+  else:
+    all_nodes = all_nodes_fn()
+    return [_CheckConfigNode(name, all_nodes.get(name, None),
+                             accept_offline_node)
+            for name in hosts]
 
-    @type cfg:  C{config.ConfigWriter}
-    @param cfg: the configuration object that will be used to get data
-                about the cluster
 
-    """
-    self._cfg = cfg
-    self.port = netutils.GetDaemonPort(constants.NODED)
-
-  def _InstDict(self, instance, hvp=None, bep=None, osp=None):
-    """Convert the given instance to a dict.
+class _RpcProcessor:
+  def __init__(self, resolver, port, lock_monitor_cb=None):
+    """Initializes this class.
 
-    This is done via the instance's ToDict() method and additionally
-    we fill the hvparams with the cluster defaults.
-
-    @type instance: L{objects.Instance}
-    @param instance: an Instance object
-    @type hvp: dict or None
-    @param hvp: a dictionary with overridden hypervisor parameters
-    @type bep: dict or None
-    @param bep: a dictionary with overridden backend parameters
-    @type osp: dict or None
-    @param osp: a dictionary with overridden os parameters
-    @rtype: dict
-    @return: the instance dict, with the hvparams filled with the
-        cluster defaults
+    @param resolver: callable accepting a list of hostnames, returning a list
+      of tuples containing name and IP address (IP address can be the name or
+      the special value L{_OFFLINE} to mark offline machines)
+    @type port: int
+    @param port: TCP port
+    @param lock_monitor_cb: Callable for registering with lock monitor
 
     """
-    idict = instance.ToDict()
-    cluster = self._cfg.GetClusterInfo()
-    idict["hvparams"] = cluster.FillHV(instance)
-    if hvp is not None:
-      idict["hvparams"].update(hvp)
-    idict["beparams"] = cluster.FillBE(instance)
-    if bep is not None:
-      idict["beparams"].update(bep)
-    idict["osparams"] = cluster.SimpleFillOS(instance.os, instance.osparams)
-    if osp is not None:
-      idict["osparams"].update(osp)
-    for nic in idict["nics"]:
-      nic['nicparams'] = objects.FillDict(
-        cluster.nicparams[constants.PP_DEFAULT],
-        nic['nicparams'])
-    return idict
+    self._resolver = resolver
+    self._port = port
+    self._lock_monitor_cb = lock_monitor_cb
 
-  def _ConnectList(self, client, node_list, call, read_timeout=None):
-    """Helper for computing node addresses.
+  @staticmethod
+  def _PrepareRequests(hosts, port, procedure, body, read_timeout):
+    """Prepares requests by sorting offline hosts into separate list.
 
-    @type client: L{ganeti.rpc.Client}
-    @param client: a C{Client} instance
-    @type node_list: list
-    @param node_list: the node list we should connect
-    @type call: string
-    @param call: the name of the remote procedure call, for filling in
-        correctly any eventual offline nodes' results
-    @type read_timeout: int
-    @param read_timeout: overwrites the default read timeout for the
-        given operation
+    @type body: dict
+    @param body: a dictionary with per-host body data
 
     """
-    all_nodes = self._cfg.GetAllNodesInfo()
-    name_list = []
-    addr_list = []
-    skip_dict = {}
-    for node in node_list:
-      if node in all_nodes:
-        if all_nodes[node].offline:
-          skip_dict[node] = RpcResult(node=node, offline=True, call=call)
-          continue
-        val = all_nodes[node].primary_ip
+    results = {}
+    requests = {}
+
+    assert isinstance(body, dict)
+    assert len(body) == len(hosts)
+    assert compat.all(isinstance(v, str) for v in body.values())
+    assert frozenset(map(compat.fst, hosts)) == frozenset(body.keys()), \
+        "%s != %s" % (hosts, body.keys())
+
+    for (name, ip) in hosts:
+      if ip is _OFFLINE:
+        # Node is marked as offline
+        results[name] = RpcResult(node=name, offline=True, call=procedure)
       else:
-        val = None
-      addr_list.append(val)
-      name_list.append(node)
-    if name_list:
-      client.ConnectList(name_list, address_list=addr_list,
-                         read_timeout=read_timeout)
-    return skip_dict
-
-  def _ConnectNode(self, client, node, call, read_timeout=None):
-    """Helper for computing one node's address.
-
-    @type client: L{ganeti.rpc.Client}
-    @param client: a C{Client} instance
-    @type node: str
-    @param node: the node we should connect
-    @type call: string
-    @param call: the name of the remote procedure call, for filling in
-        correctly any eventual offline nodes' results
-    @type read_timeout: int
-    @param read_timeout: overwrites the default read timeout for the
-        given operation
-
-    """
-    node_info = self._cfg.GetNodeInfo(node)
-    if node_info is not None:
-      if node_info.offline:
-        return RpcResult(node=node, offline=True, call=call)
-      addr = node_info.primary_ip
-    else:
-      addr = None
-    client.ConnectNode(node, address=addr, read_timeout=read_timeout)
-
-  def _MultiNodeCall(self, node_list, procedure, args, read_timeout=None):
-    """Helper for making a multi-node call
-
-    """
-    body = serializer.DumpJson(args, indent=False)
-    c = Client(procedure, body, self.port)
-    skip_dict = self._ConnectList(c, node_list, procedure,
-                                  read_timeout=read_timeout)
-    skip_dict.update(c.GetResults())
-    return skip_dict
-
-  @classmethod
-  def _StaticMultiNodeCall(cls, node_list, procedure, args,
-                           address_list=None, read_timeout=None):
-    """Helper for making a multi-node static call
-
-    """
-    body = serializer.DumpJson(args, indent=False)
-    c = Client(procedure, body, netutils.GetDaemonPort(constants.NODED))
-    c.ConnectList(node_list, address_list=address_list,
-                  read_timeout=read_timeout)
-    return c.GetResults()
-
-  def _SingleNodeCall(self, node, procedure, args, read_timeout=None):
-    """Helper for making a single-node call
+        requests[name] = \
+          http.client.HttpClientRequest(str(ip), port,
+                                        http.HTTP_POST, str("/%s" % procedure),
+                                        headers=_RPC_CLIENT_HEADERS,
+                                        post_data=body[name],
+                                        read_timeout=read_timeout,
+                                        nicename="%s/%s" % (name, procedure),
+                                        curl_config_fn=_ConfigRpcCurl)
 
-    """
-    body = serializer.DumpJson(args, indent=False)
-    c = Client(procedure, body, self.port)
-    result = self._ConnectNode(c, node, procedure, read_timeout=read_timeout)
-    if result is None:
-      # we did connect, node is not offline
-      result = c.GetResults()[node]
-    return result
-
-  @classmethod
-  def _StaticSingleNodeCall(cls, node, procedure, args, read_timeout=None):
-    """Helper for making a single-node static call
-
-    """
-    body = serializer.DumpJson(args, indent=False)
-    c = Client(procedure, body, netutils.GetDaemonPort(constants.NODED))
-    c.ConnectNode(node, read_timeout=read_timeout)
-    return c.GetResults()[node]
+    return (results, requests)
 
   @staticmethod
-  def _Compress(data):
-    """Compresses a string for transport over RPC.
-
-    Small amounts of data are not compressed.
-
-    @type data: str
-    @param data: Data
-    @rtype: tuple
-    @return: Encoded data to send
-
-    """
-    # Small amounts of data are not compressed
-    if len(data) < 512:
-      return (constants.RPC_ENCODING_NONE, data)
-
-    # Compress with zlib and encode in base64
-    return (constants.RPC_ENCODING_ZLIB_BASE64,
-            base64.b64encode(zlib.compress(data, 3)))
-
-  #
-  # Begin RPC calls
-  #
-
-  @_RpcTimeout(_TMO_URGENT)
-  def call_bdev_sizes(self, node_list, devices):
-    """Gets the sizes of requested block devices present on a node
-
-    This is a multi-node call.
-
-    """
-    return self._MultiNodeCall(node_list, "bdev_sizes", [devices])
-
-  @_RpcTimeout(_TMO_URGENT)
-  def call_lv_list(self, node_list, vg_name):
-    """Gets the logical volumes present in a given volume group.
-
-    This is a multi-node call.
-
-    """
-    return self._MultiNodeCall(node_list, "lv_list", [vg_name])
-
-  @_RpcTimeout(_TMO_URGENT)
-  def call_vg_list(self, node_list):
-    """Gets the volume group list.
-
-    This is a multi-node call.
-
-    """
-    return self._MultiNodeCall(node_list, "vg_list", [])
-
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_storage_list(self, node_list, su_name, su_args, name, fields):
-    """Get list of storage units.
-
-    This is a multi-node call.
-
-    """
-    return self._MultiNodeCall(node_list, "storage_list",
-                               [su_name, su_args, name, fields])
-
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_storage_modify(self, node, su_name, su_args, name, changes):
-    """Modify a storage unit.
-
-    This is a single-node call.
-
-    """
-    return self._SingleNodeCall(node, "storage_modify",
-                                [su_name, su_args, name, changes])
-
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_storage_execute(self, node, su_name, su_args, name, op):
-    """Executes an operation on a storage unit.
-
-    This is a single-node call.
-
-    """
-    return self._SingleNodeCall(node, "storage_execute",
-                                [su_name, su_args, name, op])
-
-  @_RpcTimeout(_TMO_URGENT)
-  def call_bridges_exist(self, node, bridges_list):
-    """Checks if a node has all the bridges given.
-
-    This method checks if all bridges given in the bridges_list are
-    present on the remote node, so that an instance that uses interfaces
-    on those bridges can be started.
-
-    This is a single-node call.
-
-    """
-    return self._SingleNodeCall(node, "bridges_exist", [bridges_list])
-
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_instance_start(self, node, instance, hvp, bep, startup_paused):
-    """Starts an instance.
-
-    This is a single-node call.
-
-    """
-    idict = self._InstDict(instance, hvp=hvp, bep=bep)
-    return self._SingleNodeCall(node, "instance_start", [idict, startup_paused])
-
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_instance_shutdown(self, node, instance, timeout):
-    """Stops an instance.
-
-    This is a single-node call.
-
-    """
-    return self._SingleNodeCall(node, "instance_shutdown",
-                                [self._InstDict(instance), timeout])
-
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_migration_info(self, node, instance):
-    """Gather the information necessary to prepare an instance migration.
-
-    This is a single-node call.
-
-    @type node: string
-    @param node: the node on which the instance is currently running
-    @type instance: C{objects.Instance}
-    @param instance: the instance definition
-
-    """
-    return self._SingleNodeCall(node, "migration_info",
-                                [self._InstDict(instance)])
-
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_accept_instance(self, node, instance, info, target):
-    """Prepare a node to accept an instance.
-
-    This is a single-node call.
-
-    @type node: string
-    @param node: the target node for the migration
-    @type instance: C{objects.Instance}
-    @param instance: the instance definition
-    @type info: opaque/hypervisor specific (string/data)
-    @param info: result for the call_migration_info call
-    @type target: string
-    @param target: target hostname (usually ip address) (on the node itself)
-
-    """
-    return self._SingleNodeCall(node, "accept_instance",
-                                [self._InstDict(instance), info, target])
-
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_finalize_migration(self, node, instance, info, success):
-    """Finalize any target-node migration specific operation.
-
-    This is called both in case of a successful migration and in case of error
-    (in which case it should abort the migration).
-
-    This is a single-node call.
-
-    @type node: string
-    @param node: the target node for the migration
-    @type instance: C{objects.Instance}
-    @param instance: the instance definition
-    @type info: opaque/hypervisor specific (string/data)
-    @param info: result for the call_migration_info call
-    @type success: boolean
-    @param success: whether the migration was a success or a failure
-
-    """
-    return self._SingleNodeCall(node, "finalize_migration",
-                                [self._InstDict(instance), info, success])
-
-  @_RpcTimeout(_TMO_SLOW)
-  def call_instance_migrate(self, node, instance, target, live):
-    """Migrate an instance.
-
-    This is a single-node call.
-
-    @type node: string
-    @param node: the node on which the instance is currently running
-    @type instance: C{objects.Instance}
-    @param instance: the instance definition
-    @type target: string
-    @param target: the target node name
-    @type live: boolean
-    @param live: whether the migration should be done live or not (the
-        interpretation of this parameter is left to the hypervisor)
-
-    """
-    return self._SingleNodeCall(node, "instance_migrate",
-                                [self._InstDict(instance), target, live])
-
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_instance_reboot(self, node, inst, reboot_type, shutdown_timeout):
-    """Reboots an instance.
-
-    This is a single-node call.
-
-    """
-    return self._SingleNodeCall(node, "instance_reboot",
-                                [self._InstDict(inst), reboot_type,
-                                 shutdown_timeout])
-
-  @_RpcTimeout(_TMO_1DAY)
-  def call_instance_os_add(self, node, inst, reinstall, debug, osparams=None):
-    """Installs an OS on the given instance.
-
-    This is a single-node call.
-
-    """
-    return self._SingleNodeCall(node, "instance_os_add",
-                                [self._InstDict(inst, osp=osparams),
-                                 reinstall, debug])
-
-  @_RpcTimeout(_TMO_SLOW)
-  def call_instance_run_rename(self, node, inst, old_name, debug):
-    """Run the OS rename script for an instance.
-
-    This is a single-node call.
-
-    """
-    return self._SingleNodeCall(node, "instance_run_rename",
-                                [self._InstDict(inst), old_name, debug])
-
-  @_RpcTimeout(_TMO_URGENT)
-  def call_instance_info(self, node, instance, hname):
-    """Returns information about a single instance.
-
-    This is a single-node call.
-
-    @type node: list
-    @param node: the list of nodes to query
-    @type instance: string
-    @param instance: the instance name
-    @type hname: string
-    @param hname: the hypervisor type of the instance
-
-    """
-    return self._SingleNodeCall(node, "instance_info", [instance, hname])
-
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_instance_migratable(self, node, instance):
-    """Checks whether the given instance can be migrated.
-
-    This is a single-node call.
-
-    @param node: the node to query
-    @type instance: L{objects.Instance}
-    @param instance: the instance to check
-
-
-    """
-    return self._SingleNodeCall(node, "instance_migratable",
-                                [self._InstDict(instance)])
-
-  @_RpcTimeout(_TMO_URGENT)
-  def call_all_instances_info(self, node_list, hypervisor_list):
-    """Returns information about all instances on the given nodes.
-
-    This is a multi-node call.
-
-    @type node_list: list
-    @param node_list: the list of nodes to query
-    @type hypervisor_list: list
-    @param hypervisor_list: the hypervisors to query for instances
-
-    """
-    return self._MultiNodeCall(node_list, "all_instances_info",
-                               [hypervisor_list])
-
-  @_RpcTimeout(_TMO_URGENT)
-  def call_instance_list(self, node_list, hypervisor_list):
-    """Returns the list of running instances on a given node.
-
-    This is a multi-node call.
-
-    @type node_list: list
-    @param node_list: the list of nodes to query
-    @type hypervisor_list: list
-    @param hypervisor_list: the hypervisors to query for instances
-
-    """
-    return self._MultiNodeCall(node_list, "instance_list", [hypervisor_list])
-
-  @_RpcTimeout(_TMO_FAST)
-  def call_node_tcp_ping(self, node, source, target, port, timeout,
-                         live_port_needed):
-    """Do a TcpPing on the remote node
-
-    This is a single-node call.
-
-    """
-    return self._SingleNodeCall(node, "node_tcp_ping",
-                                [source, target, port, timeout,
-                                 live_port_needed])
-
-  @_RpcTimeout(_TMO_FAST)
-  def call_node_has_ip_address(self, node, address):
-    """Checks if a node has the given IP address.
-
-    This is a single-node call.
-
-    """
-    return self._SingleNodeCall(node, "node_has_ip_address", [address])
-
-  @_RpcTimeout(_TMO_URGENT)
-  def call_node_info(self, node_list, vg_name, hypervisor_type):
-    """Return node information.
-
-    This will return memory information and volume group size and free
-    space.
-
-    This is a multi-node call.
-
-    @type node_list: list
-    @param node_list: the list of nodes to query
-    @type vg_name: C{string}
-    @param vg_name: the name of the volume group to ask for disk space
-        information
-    @type hypervisor_type: C{str}
-    @param hypervisor_type: the name of the hypervisor to ask for
-        memory information
-
-    """
-    return self._MultiNodeCall(node_list, "node_info",
-                               [vg_name, hypervisor_type])
-
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_etc_hosts_modify(self, node, mode, name, ip):
-    """Modify hosts file with name
-
-    @type node: string
-    @param node: The node to call
-    @type mode: string
-    @param mode: The mode to operate. Currently "add" or "remove"
-    @type name: string
-    @param name: The host name to be modified
-    @type ip: string
-    @param ip: The ip of the entry (just valid if mode is "add")
-
-    """
-    return self._SingleNodeCall(node, "etc_hosts_modify", [mode, name, ip])
-
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_node_verify(self, node_list, checkdict, cluster_name):
-    """Request verification of given parameters.
-
-    This is a multi-node call.
-
-    """
-    return self._MultiNodeCall(node_list, "node_verify",
-                               [checkdict, cluster_name])
-
-  @classmethod
-  @_RpcTimeout(_TMO_FAST)
-  def call_node_start_master(cls, node, start_daemons, no_voting):
-    """Tells a node to activate itself as a master.
-
-    This is a single-node call.
-
-    """
-    return cls._StaticSingleNodeCall(node, "node_start_master",
-                                     [start_daemons, no_voting])
-
-  @classmethod
-  @_RpcTimeout(_TMO_FAST)
-  def call_node_stop_master(cls, node, stop_daemons):
-    """Tells a node to demote itself from master status.
-
-    This is a single-node call.
+  def _CombineResults(results, requests, procedure):
+    """Combines pre-computed results for offline hosts with actual call results.
 
     """
-    return cls._StaticSingleNodeCall(node, "node_stop_master", [stop_daemons])
-
-  @classmethod
-  @_RpcTimeout(_TMO_URGENT)
-  def call_master_info(cls, node_list):
-    """Query master info.
-
-    This is a multi-node call.
-
-    """
-    # TODO: should this method query down nodes?
-    return cls._StaticMultiNodeCall(node_list, "master_info", [])
-
-  @classmethod
-  @_RpcTimeout(_TMO_URGENT)
-  def call_version(cls, node_list):
-    """Query node version.
-
-    This is a multi-node call.
-
-    """
-    return cls._StaticMultiNodeCall(node_list, "version", [])
-
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_blockdev_create(self, node, bdev, size, owner, on_primary, info):
-    """Request creation of a given block device.
-
-    This is a single-node call.
-
-    """
-    return self._SingleNodeCall(node, "blockdev_create",
-                                [bdev.ToDict(), size, owner, on_primary, info])
-
-  @_RpcTimeout(_TMO_SLOW)
-  def call_blockdev_wipe(self, node, bdev, offset, size):
-    """Request wipe at given offset with given size of a block device.
-
-    This is a single-node call.
-
-    """
-    return self._SingleNodeCall(node, "blockdev_wipe",
-                                [bdev.ToDict(), offset, size])
-
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_blockdev_remove(self, node, bdev):
-    """Request removal of a given block device.
-
-    This is a single-node call.
-
-    """
-    return self._SingleNodeCall(node, "blockdev_remove", [bdev.ToDict()])
-
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_blockdev_rename(self, node, devlist):
-    """Request rename of the given block devices.
-
-    This is a single-node call.
-
-    """
-    return self._SingleNodeCall(node, "blockdev_rename",
-                                [(d.ToDict(), uid) for d, uid in devlist])
-
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_blockdev_pause_resume_sync(self, node, disks, pause):
-    """Request a pause/resume of given block device.
-
-    This is a single-node call.
-
-    """
-    return self._SingleNodeCall(node, "blockdev_pause_resume_sync",
-                                [[bdev.ToDict() for bdev in disks], pause])
-
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_blockdev_assemble(self, node, disk, owner, on_primary, idx):
-    """Request assembling of a given block device.
-
-    This is a single-node call.
-
-    """
-    return self._SingleNodeCall(node, "blockdev_assemble",
-                                [disk.ToDict(), owner, on_primary, idx])
-
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_blockdev_shutdown(self, node, disk):
-    """Request shutdown of a given block device.
-
-    This is a single-node call.
-
-    """
-    return self._SingleNodeCall(node, "blockdev_shutdown", [disk.ToDict()])
-
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_blockdev_addchildren(self, node, bdev, ndevs):
-    """Request adding a list of children to a (mirroring) device.
-
-    This is a single-node call.
-
-    """
-    return self._SingleNodeCall(node, "blockdev_addchildren",
-                                [bdev.ToDict(),
-                                 [disk.ToDict() for disk in ndevs]])
-
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_blockdev_removechildren(self, node, bdev, ndevs):
-    """Request removing a list of children from a (mirroring) device.
-
-    This is a single-node call.
-
-    """
-    return self._SingleNodeCall(node, "blockdev_removechildren",
-                                [bdev.ToDict(),
-                                 [disk.ToDict() for disk in ndevs]])
-
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_blockdev_getmirrorstatus(self, node, disks):
-    """Request status of a (mirroring) device.
-
-    This is a single-node call.
-
-    """
-    result = self._SingleNodeCall(node, "blockdev_getmirrorstatus",
-                                  [dsk.ToDict() for dsk in disks])
-    if not result.fail_msg:
-      result.payload = [objects.BlockDevStatus.FromDict(i)
-                        for i in result.payload]
-    return result
-
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_blockdev_getmirrorstatus_multi(self, node_list, node_disks):
-    """Request status of (mirroring) devices from multiple nodes.
-
-    This is a multi-node call.
-
-    """
-    result = self._MultiNodeCall(node_list, "blockdev_getmirrorstatus_multi",
-                                 [dict((name, [dsk.ToDict() for dsk in disks])
-                                       for name, disks in node_disks.items())])
-    for nres in result.values():
-      if nres.fail_msg:
-        continue
-
-      for idx, (success, status) in enumerate(nres.payload):
-        if success:
-          nres.payload[idx] = (success, objects.BlockDevStatus.FromDict(status))
-
-    return result
-
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_blockdev_find(self, node, disk):
-    """Request identification of a given block device.
-
-    This is a single-node call.
-
-    """
-    result = self._SingleNodeCall(node, "blockdev_find", [disk.ToDict()])
-    if not result.fail_msg and result.payload is not None:
-      result.payload = objects.BlockDevStatus.FromDict(result.payload)
-    return result
-
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_blockdev_close(self, node, instance_name, disks):
-    """Closes the given block devices.
-
-    This is a single-node call.
-
-    """
-    params = [instance_name, [cf.ToDict() for cf in disks]]
-    return self._SingleNodeCall(node, "blockdev_close", params)
+    for name, req in requests.items():
+      if req.success and req.resp_status_code == http.HTTP_OK:
+        host_result = RpcResult(data=serializer.LoadJson(req.resp_body),
+                                node=name, call=procedure)
+      else:
+        # TODO: Better error reporting
+        if req.error:
+          msg = req.error
+        else:
+          msg = req.resp_body
 
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_blockdev_getsize(self, node, disks):
-    """Returns the size of the given disks.
+        logging.error("RPC error in %s on node %s: %s", procedure, name, msg)
+        host_result = RpcResult(data=msg, failed=True, node=name,
+                                call=procedure)
 
-    This is a single-node call.
+      results[name] = host_result
 
-    """
-    params = [[cf.ToDict() for cf in disks]]
-    return self._SingleNodeCall(node, "blockdev_getsize", params)
+    return results
 
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_drbd_disconnect_net(self, node_list, nodes_ip, disks):
-    """Disconnects the network of the given drbd devices.
+  def __call__(self, hosts, procedure, body, read_timeout, resolver_opts,
+               _req_process_fn=None):
+    """Makes an RPC request to a number of nodes.
 
-    This is a multi-node call.
+    @type hosts: sequence
+    @param hosts: Hostnames
+    @type procedure: string
+    @param procedure: Request path
+    @type body: dictionary
+    @param body: dictionary with request bodies per host
+    @type read_timeout: int or None
+    @param read_timeout: Read timeout for request
 
     """
-    return self._MultiNodeCall(node_list, "drbd_disconnect_net",
-                               [nodes_ip, [cf.ToDict() for cf in disks]])
+    assert read_timeout is not None, \
+      "Missing RPC read timeout for procedure '%s'" % procedure
 
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_drbd_attach_net(self, node_list, nodes_ip,
-                           disks, instance_name, multimaster):
-    """Disconnects the given drbd devices.
+    if _req_process_fn is None:
+      _req_process_fn = http.client.ProcessRequests
 
-    This is a multi-node call.
+    (results, requests) = \
+      self._PrepareRequests(self._resolver(hosts, resolver_opts), self._port,
+                            procedure, body, read_timeout)
 
-    """
-    return self._MultiNodeCall(node_list, "drbd_attach_net",
-                               [nodes_ip, [cf.ToDict() for cf in disks],
-                                instance_name, multimaster])
-
-  @_RpcTimeout(_TMO_SLOW)
-  def call_drbd_wait_sync(self, node_list, nodes_ip, disks):
-    """Waits for the synchronization of drbd devices is complete.
+    _req_process_fn(requests.values(), lock_monitor_cb=self._lock_monitor_cb)
 
-    This is a multi-node call.
+    assert not frozenset(results).intersection(requests)
 
-    """
-    return self._MultiNodeCall(node_list, "drbd_wait_sync",
-                               [nodes_ip, [cf.ToDict() for cf in disks]])
+    return self._CombineResults(results, requests, procedure)
 
-  @_RpcTimeout(_TMO_URGENT)
-  def call_drbd_helper(self, node_list):
-    """Gets drbd helper.
 
-    This is a multi-node call.
+class _RpcClientBase:
+  def __init__(self, resolver, encoder_fn, lock_monitor_cb=None,
+               _req_process_fn=None):
+    """Initializes this class.
 
     """
-    return self._MultiNodeCall(node_list, "drbd_helper", [])
+    proc = _RpcProcessor(resolver,
+                         netutils.GetDaemonPort(constants.NODED),
+                         lock_monitor_cb=lock_monitor_cb)
+    self._proc = compat.partial(proc, _req_process_fn=_req_process_fn)
+    self._encoder = compat.partial(self._EncodeArg, encoder_fn)
 
-  @classmethod
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_upload_file(cls, node_list, file_name, address_list=None):
-    """Upload a file.
-
-    The node will refuse the operation in case the file is not on the
-    approved file list.
-
-    This is a multi-node call.
-
-    @type node_list: list
-    @param node_list: the list of node names to upload to
-    @type file_name: str
-    @param file_name: the filename to upload
-    @type address_list: list or None
-    @keyword address_list: an optional list of node addresses, in order
-        to optimize the RPC speed
-
-    """
-    file_contents = utils.ReadFile(file_name)
-    data = cls._Compress(file_contents)
-    st = os.stat(file_name)
-    getents = runtime.GetEnts()
-    params = [file_name, data, st.st_mode, getents.LookupUid(st.st_uid),
-              getents.LookupGid(st.st_gid), st.st_atime, st.st_mtime]
-    return cls._StaticMultiNodeCall(node_list, "upload_file", params,
-                                    address_list=address_list)
-
-  @classmethod
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_write_ssconf_files(cls, node_list, values):
-    """Write ssconf files.
-
-    This is a multi-node call.
+  @staticmethod
+  def _EncodeArg(encoder_fn, (argkind, value)):
+    """Encode argument.
 
     """
-    return cls._StaticMultiNodeCall(node_list, "write_ssconf_files", [values])
-
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_run_oob(self, node, oob_program, command, remote_node, timeout):
-    """Runs OOB.
+    if argkind is None:
+      return value
+    else:
+      return encoder_fn(argkind)(value)
 
-    This is a single-node call.
+  def _Call(self, cdef, node_list, args):
+    """Entry point for automatically generated RPC wrappers.
 
     """
-    return self._SingleNodeCall(node, "run_oob", [oob_program, command,
-                                                  remote_node, timeout])
-
-  @_RpcTimeout(_TMO_FAST)
-  def call_os_diagnose(self, node_list):
-    """Request a diagnose of OS definitions.
-
-    This is a multi-node call.
+    (procedure, _, resolver_opts, timeout, argdefs,
+     prep_fn, postproc_fn, _) = cdef
 
-    """
-    return self._MultiNodeCall(node_list, "os_diagnose", [])
+    if callable(timeout):
+      read_timeout = timeout(args)
+    else:
+      read_timeout = timeout
 
-  @_RpcTimeout(_TMO_FAST)
-  def call_os_get(self, node, name):
-    """Returns an OS definition.
+    if callable(resolver_opts):
+      req_resolver_opts = resolver_opts(args)
+    else:
+      req_resolver_opts = resolver_opts
 
-    This is a single-node call.
+    if len(args) != len(argdefs):
+      raise errors.ProgrammerError("Number of passed arguments doesn't match")
 
-    """
-    result = self._SingleNodeCall(node, "os_get", [name])
-    if not result.fail_msg and isinstance(result.payload, dict):
-      result.payload = objects.OS.FromDict(result.payload)
-    return result
+    enc_args = map(self._encoder, zip(map(compat.snd, argdefs), args))
+    if prep_fn is None:
+      # for a no-op prep_fn, we serialise the body once, and then we
+      # reuse it in the dictionary values
+      body = serializer.DumpJson(enc_args)
+      pnbody = dict((n, body) for n in node_list)
+    else:
+      # for a custom prep_fn, we pass the encoded arguments and the
+      # node name to the prep_fn, and we serialise its return value
+      assert callable(prep_fn)
+      pnbody = dict((n, serializer.DumpJson(prep_fn(n, enc_args)))
+                    for n in node_list)
+
+    result = self._proc(node_list, procedure, pnbody, read_timeout,
+                        req_resolver_opts)
+
+    if postproc_fn:
+      return dict(map(lambda (key, value): (key, postproc_fn(value)),
+                      result.items()))
+    else:
+      return result
 
-  @_RpcTimeout(_TMO_FAST)
-  def call_os_validate(self, required, nodes, name, checks, params):
-    """Run a validation routine for a given OS.
 
-    This is a multi-node call.
+def _ObjectToDict(value):
+  """Converts an object to a dictionary.
 
-    """
-    return self._MultiNodeCall(nodes, "os_validate",
-                               [required, name, checks, params])
+  @note: See L{objects}.
 
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_hooks_runner(self, node_list, hpath, phase, env):
-    """Call the hooks runner.
+  """
+  return value.ToDict()
 
-    Args:
-      - op: the OpCode instance
-      - env: a dictionary with the environment
 
-    This is a multi-node call.
+def _ObjectListToDict(value):
+  """Converts a list of L{objects} to dictionaries.
 
-    """
-    params = [hpath, phase, env]
-    return self._MultiNodeCall(node_list, "hooks_runner", params)
+  """
+  return map(_ObjectToDict, value)
 
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_iallocator_runner(self, node, name, idata):
-    """Call an iallocator on a remote node
 
-    Args:
-      - name: the iallocator name
-      - input: the json-encoded input string
+def _EncodeNodeToDiskDict(value):
+  """Encodes a dictionary with node name as key and disk objects as values.
 
-    This is a single-node call.
+  """
+  return dict((name, _ObjectListToDict(disks))
+              for name, disks in value.items())
 
-    """
-    return self._SingleNodeCall(node, "iallocator_runner", [name, idata])
 
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_blockdev_grow(self, node, cf_bdev, amount, dryrun):
-    """Request a snapshot of the given block device.
+def _PrepareFileUpload(getents_fn, filename):
+  """Loads a file and prepares it for an upload to nodes.
 
-    This is a single-node call.
+  """
+  statcb = utils.FileStatHelper()
+  data = _Compress(utils.ReadFile(filename, preread=statcb))
+  st = statcb.st
 
-    """
-    return self._SingleNodeCall(node, "blockdev_grow",
-                                [cf_bdev.ToDict(), amount, dryrun])
+  if getents_fn is None:
+    getents_fn = runtime.GetEnts
 
-  @_RpcTimeout(_TMO_1DAY)
-  def call_blockdev_export(self, node, cf_bdev,
-                           dest_node, dest_path, cluster_name):
-    """Export a given disk to another node.
+  getents = getents_fn()
 
-    This is a single-node call.
+  return [filename, data, st.st_mode, getents.LookupUid(st.st_uid),
+          getents.LookupGid(st.st_gid), st.st_atime, st.st_mtime]
 
-    """
-    return self._SingleNodeCall(node, "blockdev_export",
-                                [cf_bdev.ToDict(), dest_node, dest_path,
-                                 cluster_name])
 
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_blockdev_snapshot(self, node, cf_bdev):
-    """Request a snapshot of the given block device.
+def _PrepareFinalizeExportDisks(snap_disks):
+  """Encodes disks for finalizing export.
 
-    This is a single-node call.
+  """
+  flat_disks = []
 
-    """
-    return self._SingleNodeCall(node, "blockdev_snapshot", [cf_bdev.ToDict()])
+  for disk in snap_disks:
+    if isinstance(disk, bool):
+      flat_disks.append(disk)
+    else:
+      flat_disks.append(disk.ToDict())
 
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_finalize_export(self, node, instance, snap_disks):
-    """Request the completion of an export operation.
+  return flat_disks
 
-    This writes the export config file, etc.
 
-    This is a single-node call.
+def _EncodeImportExportIO((ieio, ieioargs)):
+  """Encodes import/export I/O information.
 
-    """
-    flat_disks = []
-    for disk in snap_disks:
-      if isinstance(disk, bool):
-        flat_disks.append(disk)
-      else:
-        flat_disks.append(disk.ToDict())
+  """
+  if ieio == constants.IEIO_RAW_DISK:
+    assert len(ieioargs) == 1
+    return (ieio, (ieioargs[0].ToDict(), ))
 
-    return self._SingleNodeCall(node, "finalize_export",
-                                [self._InstDict(instance), flat_disks])
+  if ieio == constants.IEIO_SCRIPT:
+    assert len(ieioargs) == 2
+    return (ieio, (ieioargs[0].ToDict(), ieioargs[1]))
 
-  @_RpcTimeout(_TMO_FAST)
-  def call_export_info(self, node, path):
-    """Queries the export information in a given path.
+  return (ieio, ieioargs)
 
-    This is a single-node call.
 
-    """
-    return self._SingleNodeCall(node, "export_info", [path])
+def _EncodeBlockdevRename(value):
+  """Encodes information for renaming block devices.
 
-  @_RpcTimeout(_TMO_FAST)
-  def call_export_list(self, node_list):
-    """Gets the stored exports list.
+  """
+  return [(d.ToDict(), uid) for d, uid in value]
 
-    This is a multi-node call.
 
-    """
-    return self._MultiNodeCall(node_list, "export_list", [])
+def _AnnotateDParamsDRBD(disk, (drbd_params, data_params, meta_params)):
+  """Annotates just DRBD disks layouts.
 
-  @_RpcTimeout(_TMO_FAST)
-  def call_export_remove(self, node, export):
-    """Requests removal of a given export.
+  """
+  assert disk.dev_type == constants.LD_DRBD8
 
-    This is a single-node call.
+  disk.params = objects.FillDict(drbd_params, disk.params)
+  (dev_data, dev_meta) = disk.children
+  dev_data.params = objects.FillDict(data_params, dev_data.params)
+  dev_meta.params = objects.FillDict(meta_params, dev_meta.params)
 
-    """
-    return self._SingleNodeCall(node, "export_remove", [export])
+  return disk
 
-  @classmethod
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_node_leave_cluster(cls, node, modify_ssh_setup):
-    """Requests a node to clean the cluster information it has.
 
-    This will remove the configuration information from the ganeti data
-    dir.
+def _AnnotateDParamsGeneric(disk, (params, )):
+  """Generic disk parameter annotation routine.
 
-    This is a single-node call.
+  """
+  assert disk.dev_type != constants.LD_DRBD8
 
-    """
-    return cls._StaticSingleNodeCall(node, "node_leave_cluster",
-                                     [modify_ssh_setup])
+  disk.params = objects.FillDict(params, disk.params)
 
-  @_RpcTimeout(_TMO_FAST)
-  def call_node_volumes(self, node_list):
-    """Gets all volumes on node(s).
+  return disk
 
-    This is a multi-node call.
 
-    """
-    return self._MultiNodeCall(node_list, "node_volumes", [])
+def AnnotateDiskParams(template, disks, disk_params):
+  """Annotates the disk objects with the disk parameters.
 
-  @_RpcTimeout(_TMO_FAST)
-  def call_node_demote_from_mc(self, node):
-    """Demote a node from the master candidate role.
+  @param template: The disk template used
+  @param disks: The list of disks objects to annotate
+  @param disk_params: The disk paramaters for annotation
+  @returns: A list of disk objects annotated
 
-    This is a single-node call.
+  """
+  ld_params = objects.Disk.ComputeLDParams(template, disk_params)
 
-    """
-    return self._SingleNodeCall(node, "node_demote_from_mc", [])
+  if template == constants.DT_DRBD8:
+    annotation_fn = _AnnotateDParamsDRBD
+  elif template == constants.DT_DISKLESS:
+    annotation_fn = lambda disk, _: disk
+  else:
+    annotation_fn = _AnnotateDParamsGeneric
 
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_node_powercycle(self, node, hypervisor):
-    """Tries to powercycle a node.
+  new_disks = []
+  for disk in disks:
+    new_disks.append(annotation_fn(disk.Copy(), ld_params))
 
-    This is a single-node call.
+  return new_disks
 
-    """
-    return self._SingleNodeCall(node, "node_powercycle", [hypervisor])
 
-  @_RpcTimeout(None)
-  def call_test_delay(self, node_list, duration):
-    """Sleep for a fixed time on given node(s).
+#: Generic encoders
+_ENCODERS = {
+  rpc_defs.ED_OBJECT_DICT: _ObjectToDict,
+  rpc_defs.ED_OBJECT_DICT_LIST: _ObjectListToDict,
+  rpc_defs.ED_NODE_TO_DISK_DICT: _EncodeNodeToDiskDict,
+  rpc_defs.ED_COMPRESS: _Compress,
+  rpc_defs.ED_FINALIZE_EXPORT_DISKS: _PrepareFinalizeExportDisks,
+  rpc_defs.ED_IMPEXP_IO: _EncodeImportExportIO,
+  rpc_defs.ED_BLOCKDEV_RENAME: _EncodeBlockdevRename,
+  }
 
-    This is a multi-node call.
 
-    """
-    return self._MultiNodeCall(node_list, "test_delay", [duration],
-                               read_timeout=int(duration + 5))
+class RpcRunner(_RpcClientBase,
+                _generated_rpc.RpcClientDefault,
+                _generated_rpc.RpcClientBootstrap,
+                _generated_rpc.RpcClientDnsOnly,
+                _generated_rpc.RpcClientConfig):
+  """RPC runner class.
 
-  @_RpcTimeout(_TMO_FAST)
-  def call_file_storage_dir_create(self, node, file_storage_dir):
-    """Create the given file storage directory.
+  """
+  def __init__(self, cfg, lock_monitor_cb, _req_process_fn=None, _getents=None):
+    """Initialized the RPC runner.
 
-    This is a single-node call.
+    @type cfg: L{config.ConfigWriter}
+    @param cfg: Configuration
+    @type lock_monitor_cb: callable
+    @param lock_monitor_cb: Lock monitor callback
 
     """
-    return self._SingleNodeCall(node, "file_storage_dir_create",
-                                [file_storage_dir])
-
-  @_RpcTimeout(_TMO_FAST)
-  def call_file_storage_dir_remove(self, node, file_storage_dir):
-    """Remove the given file storage directory.
+    self._cfg = cfg
 
-    This is a single-node call.
+    encoders = _ENCODERS.copy()
+
+    encoders.update({
+      # Encoders requiring configuration object
+      rpc_defs.ED_INST_DICT: self._InstDict,
+      rpc_defs.ED_INST_DICT_HVP_BEP: self._InstDictHvpBep,
+      rpc_defs.ED_INST_DICT_OSP_DP: self._InstDictOspDp,
+
+      # Encoders annotating disk parameters
+      rpc_defs.ED_DISKS_DICT_DP: self._DisksDictDP,
+      rpc_defs.ED_SINGLE_DISK_DICT_DP: self._SingleDiskDictDP,
+
+      # Encoders with special requirements
+      rpc_defs.ED_FILE_DETAILS: compat.partial(_PrepareFileUpload, _getents),
+      })
+
+    # Resolver using configuration
+    resolver = compat.partial(_NodeConfigResolver, cfg.GetNodeInfo,
+                              cfg.GetAllNodesInfo)
+
+    # Pylint doesn't recognize multiple inheritance properly, see
+    # <http://www.logilab.org/ticket/36586> and
+    # <http://www.logilab.org/ticket/35642>
+    # pylint: disable=W0233
+    _RpcClientBase.__init__(self, resolver, encoders.get,
+                            lock_monitor_cb=lock_monitor_cb,
+                            _req_process_fn=_req_process_fn)
+    _generated_rpc.RpcClientConfig.__init__(self)
+    _generated_rpc.RpcClientBootstrap.__init__(self)
+    _generated_rpc.RpcClientDnsOnly.__init__(self)
+    _generated_rpc.RpcClientDefault.__init__(self)
 
-    """
-    return self._SingleNodeCall(node, "file_storage_dir_remove",
-                                [file_storage_dir])
+  def _InstDict(self, instance, hvp=None, bep=None, osp=None):
+    """Convert the given instance to a dict.
 
-  @_RpcTimeout(_TMO_FAST)
-  def call_file_storage_dir_rename(self, node, old_file_storage_dir,
-                                   new_file_storage_dir):
-    """Rename file storage directory.
+    This is done via the instance's ToDict() method and additionally
+    we fill the hvparams with the cluster defaults.
 
-    This is a single-node call.
+    @type instance: L{objects.Instance}
+    @param instance: an Instance object
+    @type hvp: dict or None
+    @param hvp: a dictionary with overridden hypervisor parameters
+    @type bep: dict or None
+    @param bep: a dictionary with overridden backend parameters
+    @type osp: dict or None
+    @param osp: a dictionary with overridden os parameters
+    @rtype: dict
+    @return: the instance dict, with the hvparams filled with the
+        cluster defaults
 
     """
-    return self._SingleNodeCall(node, "file_storage_dir_rename",
-                                [old_file_storage_dir, new_file_storage_dir])
-
-  @classmethod
-  @_RpcTimeout(_TMO_URGENT)
-  def call_jobqueue_update(cls, node_list, address_list, file_name, content):
-    """Update job queue.
+    idict = instance.ToDict()
+    cluster = self._cfg.GetClusterInfo()
+    idict["hvparams"] = cluster.FillHV(instance)
+    if hvp is not None:
+      idict["hvparams"].update(hvp)
+    idict["beparams"] = cluster.FillBE(instance)
+    if bep is not None:
+      idict["beparams"].update(bep)
+    idict["osparams"] = cluster.SimpleFillOS(instance.os, instance.osparams)
+    if osp is not None:
+      idict["osparams"].update(osp)
+    for nic in idict["nics"]:
+      nic["nicparams"] = objects.FillDict(
+        cluster.nicparams[constants.PP_DEFAULT],
+        nic["nicparams"])
+    return idict
 
-    This is a multi-node call.
+  def _InstDictHvpBep(self, (instance, hvp, bep)):
+    """Wrapper for L{_InstDict}.
 
     """
-    return cls._StaticMultiNodeCall(node_list, "jobqueue_update",
-                                    [file_name, cls._Compress(content)],
-                                    address_list=address_list)
+    return self._InstDict(instance, hvp=hvp, bep=bep)
 
-  @classmethod
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_jobqueue_purge(cls, node):
-    """Purge job queue.
-
-    This is a single-node call.
+  def _InstDictOspDp(self, (instance, osparams)):
+    """Wrapper for L{_InstDict}.
 
     """
-    return cls._StaticSingleNodeCall(node, "jobqueue_purge", [])
-
-  @classmethod
-  @_RpcTimeout(_TMO_URGENT)
-  def call_jobqueue_rename(cls, node_list, address_list, rename):
-    """Rename a job queue file.
+    updated_inst = self._InstDict(instance, osp=osparams)
+    updated_inst["disks"] = self._DisksDictDP((instance.disks, instance))
+    return updated_inst
 
-    This is a multi-node call.
+  def _DisksDictDP(self, (disks, instance)):
+    """Wrapper for L{AnnotateDiskParams}.
 
     """
-    return cls._StaticMultiNodeCall(node_list, "jobqueue_rename", rename,
-                                    address_list=address_list)
-
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_hypervisor_validate_params(self, node_list, hvname, hvparams):
-    """Validate the hypervisor params.
-
-    This is a multi-node call.
+    diskparams = self._cfg.GetInstanceDiskParams(instance)
+    return [disk.ToDict()
+            for disk in AnnotateDiskParams(instance.disk_template,
+                                           disks, diskparams)]
 
-    @type node_list: list
-    @param node_list: the list of nodes to query
-    @type hvname: string
-    @param hvname: the hypervisor name
-    @type hvparams: dict
-    @param hvparams: the hypervisor parameters to be validated
+  def _SingleDiskDictDP(self, (disk, instance)):
+    """Wrapper for L{AnnotateDiskParams}.
 
     """
-    cluster = self._cfg.GetClusterInfo()
-    hv_full = objects.FillDict(cluster.hvparams.get(hvname, {}), hvparams)
-    return self._MultiNodeCall(node_list, "hypervisor_validate_params",
-                               [hvname, hv_full])
+    (anno_disk,) = self._DisksDictDP(([disk], instance))
+    return anno_disk
 
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_x509_cert_create(self, node, validity):
-    """Creates a new X509 certificate for SSL/TLS.
 
-    This is a single-node call.
+class JobQueueRunner(_RpcClientBase, _generated_rpc.RpcClientJobQueue):
+  """RPC wrappers for job queue.
 
-    @type validity: int
-    @param validity: Validity in seconds
+  """
+  def __init__(self, context, address_list):
+    """Initializes this class.
 
     """
-    return self._SingleNodeCall(node, "x509_cert_create", [validity])
-
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_x509_cert_remove(self, node, name):
-    """Removes a X509 certificate.
-
-    This is a single-node call.
-
-    @type name: string
-    @param name: Certificate name
+    if address_list is None:
+      resolver = compat.partial(_SsconfResolver, True)
+    else:
+      # Caller provided an address list
+      resolver = _StaticResolver(address_list)
 
-    """
-    return self._SingleNodeCall(node, "x509_cert_remove", [name])
+    _RpcClientBase.__init__(self, resolver, _ENCODERS.get,
+                            lock_monitor_cb=context.glm.AddToLockMonitor)
+    _generated_rpc.RpcClientJobQueue.__init__(self)
 
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_import_start(self, node, opts, instance, component,
-                        dest, dest_args):
-    """Starts a listener for an import.
 
-    This is a single-node call.
+class BootstrapRunner(_RpcClientBase,
+                      _generated_rpc.RpcClientBootstrap,
+                      _generated_rpc.RpcClientDnsOnly):
+  """RPC wrappers for bootstrapping.
 
-    @type node: string
-    @param node: Node name
-    @type instance: C{objects.Instance}
-    @param instance: Instance object
-    @type component: string
-    @param component: which part of the instance is being imported
+  """
+  def __init__(self):
+    """Initializes this class.
 
     """
-    return self._SingleNodeCall(node, "import_start",
-                                [opts.ToDict(),
-                                 self._InstDict(instance), component, dest,
-                                 _EncodeImportExportIO(dest, dest_args)])
+    # Pylint doesn't recognize multiple inheritance properly, see
+    # <http://www.logilab.org/ticket/36586> and
+    # <http://www.logilab.org/ticket/35642>
+    # pylint: disable=W0233
+    _RpcClientBase.__init__(self, compat.partial(_SsconfResolver, True),
+                            _ENCODERS.get)
+    _generated_rpc.RpcClientBootstrap.__init__(self)
+    _generated_rpc.RpcClientDnsOnly.__init__(self)
 
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_export_start(self, node, opts, host, port,
-                        instance, component, source, source_args):
-    """Starts an export daemon.
 
-    This is a single-node call.
+class DnsOnlyRunner(_RpcClientBase, _generated_rpc.RpcClientDnsOnly):
+  """RPC wrappers for calls using only DNS.
 
-    @type node: string
-    @param node: Node name
-    @type instance: C{objects.Instance}
-    @param instance: Instance object
-    @type component: string
-    @param component: which part of the instance is being imported
-
-    """
-    return self._SingleNodeCall(node, "export_start",
-                                [opts.ToDict(), host, port,
-                                 self._InstDict(instance),
-                                 component, source,
-                                 _EncodeImportExportIO(source, source_args)])
-
-  @_RpcTimeout(_TMO_FAST)
-  def call_impexp_status(self, node, names):
-    """Gets the status of an import or export.
-
-    This is a single-node call.
-
-    @type node: string
-    @param node: Node name
-    @type names: List of strings
-    @param names: Import/export names
-    @rtype: List of L{objects.ImportExportStatus} instances
-    @return: Returns a list of the state of each named import/export or None if
-             a status couldn't be retrieved
+  """
+  def __init__(self):
+    """Initialize this class.
 
     """
-    result = self._SingleNodeCall(node, "impexp_status", [names])
-
-    if not result.fail_msg:
-      decoded = []
-
-      for i in result.payload:
-        if i is None:
-          decoded.append(None)
-          continue
-        decoded.append(objects.ImportExportStatus.FromDict(i))
-
-      result.payload = decoded
+    _RpcClientBase.__init__(self, compat.partial(_SsconfResolver, False),
+                            _ENCODERS.get)
+    _generated_rpc.RpcClientDnsOnly.__init__(self)
 
-    return result
 
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_impexp_abort(self, node, name):
-    """Aborts an import or export.
+class ConfigRunner(_RpcClientBase, _generated_rpc.RpcClientConfig):
+  """RPC wrappers for L{config}.
 
-    This is a single-node call.
-
-    @type node: string
-    @param node: Node name
-    @type name: string
-    @param name: Import/export name
+  """
+  def __init__(self, context, address_list, _req_process_fn=None,
+               _getents=None):
+    """Initializes this class.
 
     """
-    return self._SingleNodeCall(node, "impexp_abort", [name])
+    if context:
+      lock_monitor_cb = context.glm.AddToLockMonitor
+    else:
+      lock_monitor_cb = None
 
-  @_RpcTimeout(_TMO_NORMAL)
-  def call_impexp_cleanup(self, node, name):
-    """Cleans up after an import or export.
+    if address_list is None:
+      resolver = compat.partial(_SsconfResolver, True)
+    else:
+      # Caller provided an address list
+      resolver = _StaticResolver(address_list)
 
-    This is a single-node call.
+    encoders = _ENCODERS.copy()
 
-    @type node: string
-    @param node: Node name
-    @type name: string
-    @param name: Import/export name
+    encoders.update({
+      rpc_defs.ED_FILE_DETAILS: compat.partial(_PrepareFileUpload, _getents),
+      })
 
-    """
-    return self._SingleNodeCall(node, "impexp_cleanup", [name])
+    _RpcClientBase.__init__(self, resolver, encoders.get,
+                            lock_monitor_cb=lock_monitor_cb,
+                            _req_process_fn=_req_process_fn)
+    _generated_rpc.RpcClientConfig.__init__(self)
diff --git a/lib/rpc_defs.py b/lib/rpc_defs.py
new file mode 100644 (file)
index 0000000..2e8841b
--- /dev/null
@@ -0,0 +1,561 @@
+#
+#
+
+# Copyright (C) 2006, 2007, 2008, 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.
+
+"""RPC definitions for communication between master and node daemons.
+
+RPC definition fields:
+
+  - Name as string
+  - L{SINGLE} for single-node calls, L{MULTI} for multi-node
+  - Name resolver option(s), can be callable receiving all arguments in a tuple
+  - Timeout (e.g. L{TMO_NORMAL}), or callback receiving all arguments in a
+    tuple to calculate timeout
+  - List of arguments as tuples
+
+    - Name as string
+    - Argument kind used for encoding/decoding
+    - Description for docstring (can be C{None})
+
+  - Custom body encoder (e.g. for preparing per-node bodies)
+  - Return value wrapper (e.g. for deserializing into L{objects}-based objects)
+  - Short call description for docstring
+
+"""
+
+from ganeti import utils
+from ganeti import objects
+
+
+# Guidelines for choosing timeouts:
+# - call used during watcher: timeout of 1min, _TMO_URGENT
+# - trivial (but be sure it is trivial) (e.g. reading a file): 5min, _TMO_FAST
+# - other calls: 15 min, _TMO_NORMAL
+# - special calls (instance add, etc.): either _TMO_SLOW (1h) or huge timeouts
+TMO_URGENT = 60 # one minute
+TMO_FAST = 5 * 60 # five minutes
+TMO_NORMAL = 15 * 60 # 15 minutes
+TMO_SLOW = 3600 # one hour
+TMO_4HRS = 4 * 3600
+TMO_1DAY = 86400
+
+SINGLE = "single-node"
+MULTI = "multi-node"
+
+ACCEPT_OFFLINE_NODE = object()
+
+# Constants for encoding/decoding
+(ED_OBJECT_DICT,
+ ED_OBJECT_DICT_LIST,
+ ED_INST_DICT,
+ ED_INST_DICT_HVP_BEP,
+ ED_NODE_TO_DISK_DICT,
+ ED_INST_DICT_OSP_DP,
+ ED_IMPEXP_IO,
+ ED_FILE_DETAILS,
+ ED_FINALIZE_EXPORT_DISKS,
+ ED_COMPRESS,
+ ED_BLOCKDEV_RENAME,
+ ED_DISKS_DICT_DP,
+ ED_SINGLE_DISK_DICT_DP) = range(1, 14)
+
+
+def _Prepare(calls):
+  """Converts list of calls to dictionary.
+
+  """
+  return utils.SequenceToDict(calls)
+
+
+def _MigrationStatusPostProc(result):
+  """Post-processor for L{rpc.RpcRunner.call_instance_get_migration_status}.
+
+  """
+  if not result.fail_msg and result.payload is not None:
+    result.payload = objects.MigrationStatus.FromDict(result.payload)
+  return result
+
+
+def _BlockdevFindPostProc(result):
+  """Post-processor for L{rpc.RpcRunner.call_blockdev_find}.
+
+  """
+  if not result.fail_msg and result.payload is not None:
+    result.payload = objects.BlockDevStatus.FromDict(result.payload)
+  return result
+
+
+def _BlockdevGetMirrorStatusPostProc(result):
+  """Post-processor for L{rpc.RpcRunner.call_blockdev_getmirrorstatus}.
+
+  """
+  if not result.fail_msg:
+    result.payload = map(objects.BlockDevStatus.FromDict, result.payload)
+  return result
+
+
+def _BlockdevGetMirrorStatusMultiPreProc(node, args):
+  """Prepares the appropriate node values for blockdev_getmirrorstatus_multi.
+
+  """
+  # there should be only one argument to this RPC, already holding a
+  # node->disks dictionary, we just need to extract the value for the
+  # current node
+  assert len(args) == 1
+  return [args[0][node]]
+
+
+def _BlockdevGetMirrorStatusMultiPostProc(result):
+  """Post-processor for L{rpc.RpcRunner.call_blockdev_getmirrorstatus_multi}.
+
+  """
+  if not result.fail_msg:
+    for idx, (success, status) in enumerate(result.payload):
+      if success:
+        result.payload[idx] = (success, objects.BlockDevStatus.FromDict(status))
+
+  return result
+
+
+def _OsGetPostProc(result):
+  """Post-processor for L{rpc.RpcRunner.call_os_get}.
+
+  """
+  if not result.fail_msg and isinstance(result.payload, dict):
+    result.payload = objects.OS.FromDict(result.payload)
+  return result
+
+
+def _ImpExpStatusPostProc(result):
+  """Post-processor for import/export status.
+
+  @rtype: Payload containing list of L{objects.ImportExportStatus} instances
+  @return: Returns a list of the state of each named import/export or None if
+           a status couldn't be retrieved
+
+  """
+  if not result.fail_msg:
+    decoded = []
+
+    for i in result.payload:
+      if i is None:
+        decoded.append(None)
+        continue
+      decoded.append(objects.ImportExportStatus.FromDict(i))
+
+    result.payload = decoded
+
+  return result
+
+
+def _TestDelayTimeout((duration, )):
+  """Calculate timeout for "test_delay" RPC.
+
+  """
+  return int(duration + 5)
+
+
+_FILE_STORAGE_CALLS = [
+  ("file_storage_dir_create", SINGLE, None, TMO_FAST, [
+    ("file_storage_dir", None, "File storage directory"),
+    ], None, None, "Create the given file storage directory"),
+  ("file_storage_dir_remove", SINGLE, None, TMO_FAST, [
+    ("file_storage_dir", None, "File storage directory"),
+    ], None, None, "Remove the given file storage directory"),
+  ("file_storage_dir_rename", SINGLE, None, TMO_FAST, [
+    ("old_file_storage_dir", None, "Old name"),
+    ("new_file_storage_dir", None, "New name"),
+    ], None, None, "Rename file storage directory"),
+  ]
+
+_STORAGE_CALLS = [
+  ("storage_list", MULTI, None, TMO_NORMAL, [
+    ("su_name", None, None),
+    ("su_args", None, None),
+    ("name", None, None),
+    ("fields", None, None),
+    ], None, None, "Get list of storage units"),
+  ("storage_modify", SINGLE, None, TMO_NORMAL, [
+    ("su_name", None, None),
+    ("su_args", None, None),
+    ("name", None, None),
+    ("changes", None, None),
+    ], None, None, "Modify a storage unit"),
+  ("storage_execute", SINGLE, None, TMO_NORMAL, [
+    ("su_name", None, None),
+    ("su_args", None, None),
+    ("name", None, None),
+    ("op", None, None),
+    ], None, None, "Executes an operation on a storage unit"),
+  ]
+
+_INSTANCE_CALLS = [
+  ("instance_info", SINGLE, None, TMO_URGENT, [
+    ("instance", None, "Instance name"),
+    ("hname", None, "Hypervisor type"),
+    ], None, None, "Returns information about a single instance"),
+  ("all_instances_info", MULTI, None, TMO_URGENT, [
+    ("hypervisor_list", None, "Hypervisors to query for instances"),
+    ], None, None,
+   "Returns information about all instances on the given nodes"),
+  ("instance_list", MULTI, None, TMO_URGENT, [
+    ("hypervisor_list", None, "Hypervisors to query for instances"),
+    ], None, None, "Returns the list of running instances on the given nodes"),
+  ("instance_reboot", SINGLE, None, TMO_NORMAL, [
+    ("inst", ED_INST_DICT, "Instance object"),
+    ("reboot_type", None, None),
+    ("shutdown_timeout", None, None),
+    ], None, None, "Returns the list of running instances on the given nodes"),
+  ("instance_shutdown", SINGLE, None, TMO_NORMAL, [
+    ("instance", ED_INST_DICT, "Instance object"),
+    ("timeout", None, None),
+    ], None, None, "Stops an instance"),
+  ("instance_balloon_memory", SINGLE, None, TMO_NORMAL, [
+    ("instance", ED_INST_DICT, "Instance object"),
+    ("memory", None, None),
+    ], None, None, "Modify the amount of an instance's runtime memory"),
+  ("instance_run_rename", SINGLE, None, TMO_SLOW, [
+    ("instance", ED_INST_DICT, "Instance object"),
+    ("old_name", None, None),
+    ("debug", None, None),
+    ], None, None, "Run the OS rename script for an instance"),
+  ("instance_migratable", SINGLE, None, TMO_NORMAL, [
+    ("instance", ED_INST_DICT, "Instance object"),
+    ], None, None, "Checks whether the given instance can be migrated"),
+  ("migration_info", SINGLE, None, TMO_NORMAL, [
+    ("instance", ED_INST_DICT, "Instance object"),
+    ], None, None,
+    "Gather the information necessary to prepare an instance migration"),
+  ("accept_instance", SINGLE, None, TMO_NORMAL, [
+    ("instance", ED_INST_DICT, "Instance object"),
+    ("info", None, "Result for the call_migration_info call"),
+    ("target", None, "Target hostname (usually an IP address)"),
+    ], None, None, "Prepare a node to accept an instance"),
+  ("instance_finalize_migration_dst", SINGLE, None, TMO_NORMAL, [
+    ("instance", ED_INST_DICT, "Instance object"),
+    ("info", None, "Result for the call_migration_info call"),
+    ("success", None, "Whether the migration was a success or failure"),
+    ], None, None, "Finalize any target-node migration specific operation"),
+  ("instance_migrate", SINGLE, None, TMO_SLOW, [
+    ("instance", ED_INST_DICT, "Instance object"),
+    ("target", None, "Target node name"),
+    ("live", None, "Whether the migration should be done live or not"),
+    ], None, None, "Migrate an instance"),
+  ("instance_finalize_migration_src", SINGLE, None, TMO_SLOW, [
+    ("instance", ED_INST_DICT, "Instance object"),
+    ("success", None, "Whether the migration succeeded or not"),
+    ("live", None, "Whether the user requested a live migration or not"),
+    ], None, None, "Finalize the instance migration on the source node"),
+  ("instance_get_migration_status", SINGLE, None, TMO_SLOW, [
+    ("instance", ED_INST_DICT, "Instance object"),
+    ], None, _MigrationStatusPostProc, "Report migration status"),
+  ("instance_start", SINGLE, None, TMO_NORMAL, [
+    ("instance_hvp_bep", ED_INST_DICT_HVP_BEP, None),
+    ("startup_paused", None, None),
+    ], None, None, "Starts an instance"),
+  ("instance_os_add", SINGLE, None, TMO_1DAY, [
+    ("instance_osp", ED_INST_DICT_OSP_DP, None),
+    ("reinstall", None, None),
+    ("debug", None, None),
+    ], None, None, "Starts an instance"),
+  ]
+
+_IMPEXP_CALLS = [
+  ("import_start", SINGLE, None, TMO_NORMAL, [
+    ("opts", ED_OBJECT_DICT, None),
+    ("instance", ED_INST_DICT, None),
+    ("component", None, None),
+    ("dest", ED_IMPEXP_IO, "Import destination"),
+    ], None, None, "Starts an import daemon"),
+  ("export_start", SINGLE, None, TMO_NORMAL, [
+    ("opts", ED_OBJECT_DICT, None),
+    ("host", None, None),
+    ("port", None, None),
+    ("instance", ED_INST_DICT, None),
+    ("component", None, None),
+    ("source", ED_IMPEXP_IO, "Export source"),
+    ], None, None, "Starts an export daemon"),
+  ("impexp_status", SINGLE, None, TMO_FAST, [
+    ("names", None, "Import/export names"),
+    ], None, _ImpExpStatusPostProc, "Gets the status of an import or export"),
+  ("impexp_abort", SINGLE, None, TMO_NORMAL, [
+    ("name", None, "Import/export name"),
+    ], None, None, "Aborts an import or export"),
+  ("impexp_cleanup", SINGLE, None, TMO_NORMAL, [
+    ("name", None, "Import/export name"),
+    ], None, None, "Cleans up after an import or export"),
+  ("export_info", SINGLE, None, TMO_FAST, [
+    ("path", None, None),
+    ], None, None, "Queries the export information in a given path"),
+  ("finalize_export", SINGLE, None, TMO_NORMAL, [
+    ("instance", ED_INST_DICT, None),
+    ("snap_disks", ED_FINALIZE_EXPORT_DISKS, None),
+    ], None, None, "Request the completion of an export operation"),
+  ("export_list", MULTI, None, TMO_FAST, [], None, None,
+   "Gets the stored exports list"),
+  ("export_remove", SINGLE, None, TMO_FAST, [
+    ("export", None, None),
+    ], None, None, "Requests removal of a given export"),
+  ]
+
+_X509_CALLS = [
+  ("x509_cert_create", SINGLE, None, TMO_NORMAL, [
+    ("validity", None, "Validity in seconds"),
+    ], None, None, "Creates a new X509 certificate for SSL/TLS"),
+  ("x509_cert_remove", SINGLE, None, TMO_NORMAL, [
+    ("name", None, "Certificate name"),
+    ], None, None, "Removes a X509 certificate"),
+  ]
+
+_BLOCKDEV_CALLS = [
+  ("bdev_sizes", MULTI, None, TMO_URGENT, [
+    ("devices", None, None),
+    ], None, None,
+   "Gets the sizes of requested block devices present on a node"),
+  ("blockdev_create", SINGLE, None, TMO_NORMAL, [
+    ("bdev", ED_OBJECT_DICT, None),
+    ("size", None, None),
+    ("owner", None, None),
+    ("on_primary", None, None),
+    ("info", None, None),
+    ], None, None, "Request creation of a given block device"),
+  ("blockdev_wipe", SINGLE, None, TMO_SLOW, [
+    ("bdev", ED_SINGLE_DISK_DICT_DP, None),
+    ("offset", None, None),
+    ("size", None, None),
+    ], None, None,
+    "Request wipe at given offset with given size of a block device"),
+  ("blockdev_remove", SINGLE, None, TMO_NORMAL, [
+    ("bdev", ED_OBJECT_DICT, None),
+    ], None, None, "Request removal of a given block device"),
+  ("blockdev_pause_resume_sync", SINGLE, None, TMO_NORMAL, [
+    ("disks", ED_DISKS_DICT_DP, None),
+    ("pause", None, None),
+    ], None, None, "Request a pause/resume of given block device"),
+  ("blockdev_assemble", SINGLE, None, TMO_NORMAL, [
+    ("disk", ED_SINGLE_DISK_DICT_DP, None),
+    ("owner", None, None),
+    ("on_primary", None, None),
+    ("idx", None, None),
+    ], None, None, "Request assembling of a given block device"),
+  ("blockdev_shutdown", SINGLE, None, TMO_NORMAL, [
+    ("disk", ED_SINGLE_DISK_DICT_DP, None),
+    ], None, None, "Request shutdown of a given block device"),
+  ("blockdev_addchildren", SINGLE, None, TMO_NORMAL, [
+    ("bdev", ED_SINGLE_DISK_DICT_DP, None),
+    ("ndevs", ED_OBJECT_DICT_LIST, None),
+    ], None, None,
+   "Request adding a list of children to a (mirroring) device"),
+  ("blockdev_removechildren", SINGLE, None, TMO_NORMAL, [
+    ("bdev", ED_OBJECT_DICT, None),
+    ("ndevs", ED_OBJECT_DICT_LIST, None),
+    ], None, None,
+   "Request removing a list of children from a (mirroring) device"),
+  ("blockdev_close", SINGLE, None, TMO_NORMAL, [
+    ("instance_name", None, None),
+    ("disks", ED_OBJECT_DICT_LIST, None),
+    ], None, None, "Closes the given block devices"),
+  ("blockdev_getsize", SINGLE, None, TMO_NORMAL, [
+    ("disks", ED_OBJECT_DICT_LIST, None),
+    ], None, None, "Returns the size of the given disks"),
+  ("drbd_disconnect_net", MULTI, None, TMO_NORMAL, [
+    ("nodes_ip", None, None),
+    ("disks", ED_OBJECT_DICT_LIST, None),
+    ], None, None, "Disconnects the network of the given drbd devices"),
+  ("drbd_attach_net", MULTI, None, TMO_NORMAL, [
+    ("nodes_ip", None, None),
+    ("disks", ED_DISKS_DICT_DP, None),
+    ("instance_name", None, None),
+    ("multimaster", None, None),
+    ], None, None, "Connects the given DRBD devices"),
+  ("drbd_wait_sync", MULTI, None, TMO_SLOW, [
+    ("nodes_ip", None, None),
+    ("disks", ED_DISKS_DICT_DP, None),
+    ], None, None,
+   "Waits for the synchronization of drbd devices is complete"),
+  ("blockdev_grow", SINGLE, None, TMO_NORMAL, [
+    ("cf_bdev", ED_SINGLE_DISK_DICT_DP, None),
+    ("amount", None, None),
+    ("dryrun", None, None),
+    ], None, None, "Request a snapshot of the given block device"),
+  ("blockdev_export", SINGLE, None, TMO_1DAY, [
+    ("cf_bdev", ED_SINGLE_DISK_DICT_DP, None),
+    ("dest_node", None, None),
+    ("dest_path", None, None),
+    ("cluster_name", None, None),
+    ], None, None, "Export a given disk to another node"),
+  ("blockdev_snapshot", SINGLE, None, TMO_NORMAL, [
+    ("cf_bdev", ED_SINGLE_DISK_DICT_DP, None),
+    ], None, None, "Export a given disk to another node"),
+  ("blockdev_rename", SINGLE, None, TMO_NORMAL, [
+    ("devlist", ED_BLOCKDEV_RENAME, None),
+    ], None, None, "Request rename of the given block devices"),
+  ("blockdev_find", SINGLE, None, TMO_NORMAL, [
+    ("disk", ED_OBJECT_DICT, None),
+    ], None, _BlockdevFindPostProc,
+    "Request identification of a given block device"),
+  ("blockdev_getmirrorstatus", SINGLE, None, TMO_NORMAL, [
+    ("disks", ED_DISKS_DICT_DP, None),
+    ], None, _BlockdevGetMirrorStatusPostProc,
+    "Request status of a (mirroring) device"),
+  ("blockdev_getmirrorstatus_multi", MULTI, None, TMO_NORMAL, [
+    ("node_disks", ED_NODE_TO_DISK_DICT, None),
+    ], _BlockdevGetMirrorStatusMultiPreProc,
+   _BlockdevGetMirrorStatusMultiPostProc,
+    "Request status of (mirroring) devices from multiple nodes"),
+  ]
+
+_OS_CALLS = [
+  ("os_diagnose", MULTI, None, TMO_FAST, [], None, None,
+   "Request a diagnose of OS definitions"),
+  ("os_validate", MULTI, None, TMO_FAST, [
+    ("required", None, None),
+    ("name", None, None),
+    ("checks", None, None),
+    ("params", None, None),
+    ], None, None, "Run a validation routine for a given OS"),
+  ("os_get", SINGLE, None, TMO_FAST, [
+    ("name", None, None),
+    ], None, _OsGetPostProc, "Returns an OS definition"),
+  ]
+
+_NODE_CALLS = [
+  ("node_has_ip_address", SINGLE, None, TMO_FAST, [
+    ("address", None, "IP address"),
+    ], None, None, "Checks if a node has the given IP address"),
+  ("node_info", MULTI, None, TMO_URGENT, [
+    ("vg_names", None,
+     "Names of the volume groups to ask for disk space information"),
+    ("hv_names", None,
+     "Names of the hypervisors to ask for node information"),
+    ], None, None, "Return node information"),
+  ("node_verify", MULTI, None, TMO_NORMAL, [
+    ("checkdict", None, None),
+    ("cluster_name", None, None),
+    ], None, None, "Request verification of given parameters"),
+  ("node_volumes", MULTI, None, TMO_FAST, [], None, None,
+   "Gets all volumes on node(s)"),
+  ("node_demote_from_mc", SINGLE, None, TMO_FAST, [], None, None,
+   "Demote a node from the master candidate role"),
+  ("node_powercycle", SINGLE, ACCEPT_OFFLINE_NODE, TMO_NORMAL, [
+    ("hypervisor", None, "Hypervisor type"),
+    ], None, None, "Tries to powercycle a node"),
+  ]
+
+_MISC_CALLS = [
+  ("lv_list", MULTI, None, TMO_URGENT, [
+    ("vg_name", None, None),
+    ], None, None, "Gets the logical volumes present in a given volume group"),
+  ("vg_list", MULTI, None, TMO_URGENT, [], None, None,
+   "Gets the volume group list"),
+  ("bridges_exist", SINGLE, None, TMO_URGENT, [
+    ("bridges_list", None, "Bridges which must be present on remote node"),
+    ], None, None, "Checks if a node has all the bridges given"),
+  ("etc_hosts_modify", SINGLE, None, TMO_NORMAL, [
+    ("mode", None,
+     "Mode to operate; currently L{constants.ETC_HOSTS_ADD} or"
+     " L{constants.ETC_HOSTS_REMOVE}"),
+    ("name", None, "Hostname to be modified"),
+    ("ip", None, "IP address (L{constants.ETC_HOSTS_ADD} only)"),
+    ], None, None, "Modify hosts file with name"),
+  ("drbd_helper", MULTI, None, TMO_URGENT, [], None, None, "Gets DRBD helper"),
+  ("run_oob", SINGLE, None, TMO_NORMAL, [
+    ("oob_program", None, None),
+    ("command", None, None),
+    ("remote_node", None, None),
+    ("timeout", None, None),
+    ], None, None, "Runs out-of-band command"),
+  ("hooks_runner", MULTI, None, TMO_NORMAL, [
+    ("hpath", None, None),
+    ("phase", None, None),
+    ("env", None, None),
+    ], None, None, "Call the hooks runner"),
+  ("iallocator_runner", SINGLE, None, TMO_NORMAL, [
+    ("name", None, "Iallocator name"),
+    ("idata", None, "JSON-encoded input string"),
+    ], None, None, "Call an iallocator on a remote node"),
+  ("test_delay", MULTI, None, _TestDelayTimeout, [
+    ("duration", None, None),
+    ], None, None, "Sleep for a fixed time on given node(s)"),
+  ("hypervisor_validate_params", MULTI, None, TMO_NORMAL, [
+    ("hvname", None, "Hypervisor name"),
+    ("hvfull", None, "Parameters to be validated"),
+    ], None, None, "Validate hypervisor params"),
+  ]
+
+CALLS = {
+  "RpcClientDefault": \
+    _Prepare(_IMPEXP_CALLS + _X509_CALLS + _OS_CALLS + _NODE_CALLS +
+             _FILE_STORAGE_CALLS + _MISC_CALLS + _INSTANCE_CALLS +
+             _BLOCKDEV_CALLS + _STORAGE_CALLS),
+  "RpcClientJobQueue": _Prepare([
+    ("jobqueue_update", MULTI, None, TMO_URGENT, [
+      ("file_name", None, None),
+      ("content", ED_COMPRESS, None),
+      ], None, None, "Update job queue file"),
+    ("jobqueue_purge", SINGLE, None, TMO_NORMAL, [], None, None,
+     "Purge job queue"),
+    ("jobqueue_rename", MULTI, None, TMO_URGENT, [
+      ("rename", None, None),
+      ], None, None, "Rename job queue file"),
+    ]),
+  "RpcClientBootstrap": _Prepare([
+    ("node_start_master_daemons", SINGLE, None, TMO_FAST, [
+      ("no_voting", None, None),
+      ], None, None, "Starts master daemons on a node"),
+    ("node_activate_master_ip", SINGLE, None, TMO_FAST, [
+      ("master_params", ED_OBJECT_DICT, "Network parameters of the master"),
+      ("use_external_mip_script", None,
+       "Whether to use the user-provided master IP address setup script"),
+      ], None, None,
+      "Activates master IP on a node"),
+    ("node_stop_master", SINGLE, None, TMO_FAST, [], None, None,
+     "Deactivates master IP and stops master daemons on a node"),
+    ("node_deactivate_master_ip", SINGLE, None, TMO_FAST, [
+      ("master_params", ED_OBJECT_DICT, "Network parameters of the master"),
+      ("use_external_mip_script", None,
+       "Whether to use the user-provided master IP address setup script"),
+      ], None, None,
+     "Deactivates master IP on a node"),
+    ("node_change_master_netmask", SINGLE, None, TMO_FAST, [
+      ("old_netmask", None, "The old value of the netmask"),
+      ("netmask", None, "The new value of the netmask"),
+      ("master_ip", None, "The master IP"),
+      ("master_netdev", None, "The master network device"),
+      ], None, None, "Change master IP netmask"),
+    ("node_leave_cluster", SINGLE, None, TMO_NORMAL, [
+      ("modify_ssh_setup", None, None),
+      ], None, None,
+     "Requests a node to clean the cluster information it has"),
+    ("master_info", MULTI, None, TMO_URGENT, [], None, None,
+     "Query master info"),
+    ]),
+  "RpcClientDnsOnly": _Prepare([
+    ("version", MULTI, ACCEPT_OFFLINE_NODE, TMO_URGENT, [], None, None,
+     "Query node version"),
+    ]),
+  "RpcClientConfig": _Prepare([
+    ("upload_file", MULTI, None, TMO_NORMAL, [
+      ("file_name", ED_FILE_DETAILS, None),
+      ], None, None, "Upload a file"),
+    ("write_ssconf_files", MULTI, None, TMO_NORMAL, [
+      ("values", None, None),
+      ], None, None, "Write ssconf files"),
+    ]),
+  }
index 9e478ec..883b2e5 100644 (file)
@@ -26,6 +26,7 @@
 import grp
 import pwd
 import threading
+import platform
 
 from ganeti import constants
 from ganeti import errors
@@ -35,6 +36,9 @@ from ganeti import utils
 _priv = None
 _priv_lock = threading.Lock()
 
+#: Architecture information
+_arch = None
+
 
 def GetUid(user, _getpwnam):
   """Retrieve the uid from the database.
@@ -74,9 +78,9 @@ class GetentResolver:
   @ivar rapi_uid: The resolved uid of the rapi user
   @ivar rapi_gid: The resolved gid of the rapi group
   @ivar noded_uid: The resolved uid of the noded user
-
   @ivar daemons_gid: The resolved gid of the daemons group
   @ivar admin_gid: The resolved gid of the admin group
+
   """
   def __init__(self, _getpwnam=pwd.getpwnam, _getgrnam=grp.getgrnam):
     """Initialize the resolver.
@@ -171,7 +175,7 @@ def GetEnts(resolver=GetentResolver):
   """Singleton wrapper around resolver instance.
 
   As this method is accessed by multiple threads at the same time
-  we need to take thread-safty carefully
+  we need to take thread-safety carefully.
 
   """
   # We need to use the global keyword here
@@ -187,3 +191,35 @@ def GetEnts(resolver=GetentResolver):
       _priv_lock.release()
 
   return _priv
+
+
+def InitArchInfo():
+  """Initialize architecture information.
+
+  We can assume this information never changes during the lifetime of a
+  process, therefore the information can easily be cached.
+
+  @note: This function uses C{platform.architecture} to retrieve the Python
+    binary architecture and does so by forking to run C{file} (see Python
+    documentation for more information). Therefore it must not be used in a
+    multi-threaded environment.
+
+  """
+  global _arch # pylint: disable=W0603
+
+  if _arch is not None:
+    raise errors.ProgrammerError("Architecture information can only be"
+                                 " initialized once")
+
+  _arch = (platform.architecture()[0], platform.machine())
+
+
+def GetArchInfo():
+  """Returns previsouly initialized architecture information.
+
+  """
+  if _arch is None:
+    raise errors.ProgrammerError("Architecture information hasn't been"
+                                 " initialized")
+
+  return _arch
index ff27261..cbc11fa 100644 (file)
@@ -29,56 +29,32 @@ backend (currently json).
 # C0103: Invalid name, since pylint doesn't see that Dump points to a
 # function and not a constant
 
-import simplejson
 import re
 
+# Python 2.6 and above contain a JSON module based on simplejson. Unfortunately
+# the standard library version is significantly slower than the external
+# module. While it should be better from at least Python 3.2 on (see Python
+# issue 7451), for now Ganeti needs to work well with older Python versions
+# too.
+import simplejson
+
 from ganeti import errors
 from ganeti import utils
 
 
-_JSON_INDENT = 2
-
 _RE_EOLSP = re.compile("[ \t]+$", re.MULTILINE)
 
 
-def _GetJsonDumpers(_encoder_class=simplejson.JSONEncoder):
-  """Returns two JSON functions to serialize data.
-
-  @rtype: (callable, callable)
-  @return: The function to generate a compact form of JSON and another one to
-           generate a more readable, indented form of JSON (if supported)
-
-  """
-  plain_encoder = _encoder_class(sort_keys=True)
-
-  # Check whether the simplejson module supports indentation
-  try:
-    indent_encoder = _encoder_class(indent=_JSON_INDENT, sort_keys=True)
-  except TypeError:
-    # Indentation not supported
-    indent_encoder = plain_encoder
-
-  return (plain_encoder.encode, indent_encoder.encode)
-
-
-(_DumpJson, _DumpJsonIndent) = _GetJsonDumpers()
-
-
-def DumpJson(data, indent=True):
+def DumpJson(data):
   """Serialize a given object.
 
   @param data: the data to serialize
-  @param indent: whether to indent output (depends on simplejson version)
-
   @return: the string representation of data
 
   """
-  if indent:
-    fn = _DumpJsonIndent
-  else:
-    fn = _DumpJson
+  encoded = simplejson.dumps(data)
 
-  txt = _RE_EOLSP.sub("", fn(data))
+  txt = _RE_EOLSP.sub("", encoded)
   if not txt.endswith("\n"):
     txt += "\n"
 
@@ -106,7 +82,7 @@ def DumpSignedJson(data, key, salt=None, key_selector=None):
   @return: the string representation of data signed by the hmac key
 
   """
-  txt = DumpJson(data, indent=False)
+  txt = DumpJson(data)
   if salt is None:
     salt = ""
   signed_dict = {
@@ -121,7 +97,7 @@ def DumpSignedJson(data, key, salt=None, key_selector=None):
 
   signed_dict["hmac"] = utils.Sha1Hmac(key, txt, salt=salt + key_selector)
 
-  return DumpJson(signed_dict, indent=False)
+  return DumpJson(signed_dict)
 
 
 def LoadSignedJson(txt, key):
index 27bf561..71ddb90 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007, 2010, 2011 Google Inc.
+# Copyright (C) 2006, 2007, 2010, 2011, 2012 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
@@ -57,6 +57,7 @@ from ganeti import bootstrap
 from ganeti import netutils
 from ganeti import objects
 from ganeti import query
+from ganeti import runtime
 
 
 CLIENT_REQUEST_WORKERS = 16
@@ -125,6 +126,63 @@ class MasterClientHandler(daemon.AsyncTerminatedMessageStream):
     self.server.request_workers.AddTask((self.server, message, self))
 
 
+class _MasterShutdownCheck:
+  """Logic for master daemon shutdown.
+
+  """
+  #: How long to wait between checks
+  _CHECK_INTERVAL = 5.0
+
+  #: How long to wait after all jobs are done (e.g. to give clients time to
+  #: retrieve the job status)
+  _SHUTDOWN_LINGER = 5.0
+
+  def __init__(self):
+    """Initializes this class.
+
+    """
+    self._had_active_jobs = None
+    self._linger_timeout = None
+
+  def __call__(self, jq_prepare_result):
+    """Determines if master daemon is ready for shutdown.
+
+    @param jq_prepare_result: Result of L{jqueue.JobQueue.PrepareShutdown}
+    @rtype: None or number
+    @return: None if master daemon is ready, timeout if the check must be
+             repeated
+
+    """
+    if jq_prepare_result:
+      # Check again shortly
+      logging.info("Job queue has been notified for shutdown but is still"
+                   " busy; next check in %s seconds", self._CHECK_INTERVAL)
+      self._had_active_jobs = True
+      return self._CHECK_INTERVAL
+
+    if not self._had_active_jobs:
+      # Can shut down as there were no active jobs on the first check
+      return None
+
+    # No jobs are running anymore, but maybe some clients want to collect some
+    # information. Give them a short amount of time.
+    if self._linger_timeout is None:
+      self._linger_timeout = utils.RunningTimeout(self._SHUTDOWN_LINGER, True)
+
+    remaining = self._linger_timeout.Remaining()
+
+    logging.info("Job queue no longer busy; shutting down master daemon"
+                 " in %s seconds", remaining)
+
+    # TODO: Should the master daemon socket be closed at this point? Doing so
+    # wouldn't affect existing connections.
+
+    if remaining < 0:
+      return None
+    else:
+      return remaining
+
+
 class MasterServer(daemon.AsyncStreamServer):
   """Master Server.
 
@@ -134,11 +192,9 @@ class MasterServer(daemon.AsyncStreamServer):
   """
   family = socket.AF_UNIX
 
-  def __init__(self, mainloop, address, uid, gid):
+  def __init__(self, address, uid, gid):
     """MasterServer constructor
 
-    @type mainloop: ganeti.daemon.Mainloop
-    @param mainloop: Mainloop used to poll for I/O events
     @param address: the unix socket address to bind the MasterServer to
     @param uid: The uid of the owner of the socket
     @param gid: The gid of the owner of the socket
@@ -150,13 +206,14 @@ class MasterServer(daemon.AsyncStreamServer):
     os.chown(temp_name, uid, gid)
     os.rename(temp_name, address)
 
-    self.mainloop = mainloop
     self.awaker = daemon.AsyncAwaker()
 
     # We'll only start threads once we've forked.
     self.context = None
     self.request_workers = None
 
+    self._shutdown_check = None
+
   def handle_connection(self, connected_socket, client_address):
     # TODO: add connection count and limit the number of open connections to a
     # maximum number to avoid breaking for lack of file descriptors or memory.
@@ -168,6 +225,15 @@ class MasterServer(daemon.AsyncStreamServer):
                                                  CLIENT_REQUEST_WORKERS,
                                                  ClientRequestWorker)
 
+  def WaitForShutdown(self):
+    """Prepares server for shutdown.
+
+    """
+    if self._shutdown_check is None:
+      self._shutdown_check = _MasterShutdownCheck()
+
+    return self._shutdown_check(self.context.jobqueue.PrepareShutdown())
+
   def server_cleanup(self):
     """Cleanup the server.
 
@@ -190,35 +256,41 @@ class ClientOps:
     self.server = server
 
   def handle_request(self, method, args): # pylint: disable=R0911
-    queue = self.server.context.jobqueue
+    context = self.server.context
+    queue = context.jobqueue
 
     # TODO: Parameter validation
+    if not isinstance(args, (tuple, list)):
+      logging.info("Received invalid arguments of type '%s'", type(args))
+      raise ValueError("Invalid arguments type '%s'" % type(args))
 
     # TODO: Rewrite to not exit in each 'if/elif' branch
 
     if method == luxi.REQ_SUBMIT_JOB:
       logging.info("Received new job")
-      ops = [opcodes.OpCode.LoadOpCode(state) for state in args]
+      (job_def, ) = args
+      ops = [opcodes.OpCode.LoadOpCode(state) for state in job_def]
       return queue.SubmitJob(ops)
 
-    if method == luxi.REQ_SUBMIT_MANY_JOBS:
+    elif method == luxi.REQ_SUBMIT_MANY_JOBS:
       logging.info("Received multiple jobs")
+      (job_defs, ) = args
       jobs = []
-      for ops in args:
+      for ops in job_defs:
         jobs.append([opcodes.OpCode.LoadOpCode(state) for state in ops])
       return queue.SubmitManyJobs(jobs)
 
     elif method == luxi.REQ_CANCEL_JOB:
-      job_id = args
+      (job_id, ) = args
       logging.info("Received job cancel request for %s", job_id)
       return queue.CancelJob(job_id)
 
     elif method == luxi.REQ_ARCHIVE_JOB:
-      job_id = args
+      (job_id, ) = args
       logging.info("Received job archive request for %s", job_id)
       return queue.ArchiveJob(job_id)
 
-    elif method == luxi.REQ_AUTOARCHIVE_JOBS:
+    elif method == luxi.REQ_AUTO_ARCHIVE_JOBS:
       (age, timeout) = args
       logging.info("Received job autoarchive request for age %s, timeout %s",
                    age, timeout)
@@ -231,25 +303,28 @@ class ClientOps:
                                      prev_log_serial, timeout)
 
     elif method == luxi.REQ_QUERY:
-      req = objects.QueryRequest.FromDict(args)
+      (what, fields, qfilter) = args
 
-      if req.what in constants.QR_VIA_OP:
-        result = self._Query(opcodes.OpQuery(what=req.what, fields=req.fields,
-                                             filter=req.filter))
-      elif req.what == constants.QR_LOCK:
-        if req.filter is not None:
+      if what in constants.QR_VIA_OP:
+        result = self._Query(opcodes.OpQuery(what=what, fields=fields,
+                                             qfilter=qfilter))
+      elif what == constants.QR_LOCK:
+        if qfilter is not None:
           raise errors.OpPrereqError("Lock queries can't be filtered")
-        return self.server.context.glm.QueryLocks(req.fields)
-      elif req.what in constants.QR_VIA_LUXI:
+        return context.glm.QueryLocks(fields)
+      elif what == constants.QR_JOB:
+        return queue.QueryJobs(fields, qfilter)
+      elif what in constants.QR_VIA_LUXI:
         raise NotImplementedError
       else:
-        raise errors.OpPrereqError("Resource type '%s' unknown" % req.what,
+        raise errors.OpPrereqError("Resource type '%s' unknown" % what,
                                    errors.ECODE_INVAL)
 
       return result
 
     elif method == luxi.REQ_QUERY_FIELDS:
-      req = objects.QueryFieldsRequest.FromDict(args)
+      (what, fields) = args
+      req = objects.QueryFieldsRequest(what=what, fields=fields)
 
       try:
         fielddefs = query.ALL_FIELDS[req.what]
@@ -266,7 +341,7 @@ class ClientOps:
       else:
         msg = str(job_ids)
       logging.info("Received job query request for %s", msg)
-      return queue.QueryJobs(job_ids, fields)
+      return queue.OldStyleQueryJobs(job_ids, fields)
 
     elif method == luxi.REQ_QUERY_INSTANCES:
       (names, fields, use_locking) = args
@@ -298,7 +373,7 @@ class ClientOps:
       return self._Query(op)
 
     elif method == luxi.REQ_QUERY_EXPORTS:
-      nodes, use_locking = args
+      (nodes, use_locking) = args
       if use_locking:
         raise errors.OpPrereqError("Sync queries are not allowed",
                                    errors.ECODE_INVAL)
@@ -307,7 +382,7 @@ class ClientOps:
       return self._Query(op)
 
     elif method == luxi.REQ_QUERY_CONFIG_VALUES:
-      fields = args
+      (fields, ) = args
       logging.info("Received config values query request for %s", fields)
       op = opcodes.OpClusterConfigQuery(output_fields=fields)
       return self._Query(op)
@@ -318,20 +393,13 @@ class ClientOps:
       return self._Query(op)
 
     elif method == luxi.REQ_QUERY_TAGS:
-      kind, name = args
+      (kind, name) = args
       logging.info("Received tags query request")
-      op = opcodes.OpTagsGet(kind=kind, name=name)
+      op = opcodes.OpTagsGet(kind=kind, name=name, use_locking=False)
       return self._Query(op)
 
-    elif method == luxi.REQ_QUERY_LOCKS:
-      (fields, sync) = args
-      logging.info("Received locks query request")
-      if sync:
-        raise NotImplementedError("Synchronous queries are not implemented")
-      return self.server.context.glm.OldStyleQueryLocks(fields)
-
-    elif method == luxi.REQ_QUEUE_SET_DRAIN_FLAG:
-      drain_flag = args
+    elif method == luxi.REQ_SET_DRAIN_FLAG:
+      (drain_flag, ) = args
       logging.info("Received queue drain flag change request to %s",
                    drain_flag)
       return queue.SetDrainFlag(drain_flag)
@@ -361,7 +429,7 @@ class ClientOps:
 
     """
     # Queries don't have a job id
-    proc = mcpu.Processor(self.server.context, None)
+    proc = mcpu.Processor(self.server.context, None, enable_locks=False)
 
     # TODO: Executing an opcode using locks will acquire them in blocking mode.
     # Consider using a timeout for retries.
@@ -396,6 +464,11 @@ class GanetiContext(object):
                 self.cfg.GetNodeGroupList(),
                 self.cfg.GetInstanceList())
 
+    self.cfg.SetContext(self)
+
+    # RPC runner
+    self.rpc = rpc.RpcRunner(self.cfg, self.glm.AddToLockMonitor)
+
     # Job queue
     self.jobqueue = jqueue.JobQueue(self)
 
@@ -421,6 +494,7 @@ class GanetiContext(object):
 
     # Add the new node to the Ganeti Lock Manager
     self.glm.add(locking.LEVEL_NODE, node.name)
+    self.glm.add(locking.LEVEL_NODE_RES, node.name)
 
   def ReaddNode(self, node):
     """Updates a node that's already in the configuration
@@ -441,6 +515,7 @@ class GanetiContext(object):
 
     # Remove the node from the Ganeti Lock Manager
     self.glm.remove(locking.LEVEL_NODE, name)
+    self.glm.remove(locking.LEVEL_NODE_RES, name)
 
 
 def _SetWatcherPause(until):
@@ -523,8 +598,13 @@ def CheckAgreement():
 @rpc.RunWithRPC
 def ActivateMasterIP():
   # activate ip
-  master_node = ssconf.SimpleStore().GetMasterNode()
-  result = rpc.RpcRunner.call_node_start_master(master_node, False, False)
+  cfg = config.ConfigWriter()
+  master_params = cfg.GetMasterNetworkParameters()
+  ems = cfg.GetUseExternalMipScript()
+  runner = rpc.BootstrapRunner()
+  result = runner.call_node_activate_master_ip(master_params.name,
+                                               master_params, ems)
+
   msg = result.fail_msg
   if msg:
     logging.error("Can't activate master IP address: %s", msg)
@@ -548,6 +628,9 @@ def CheckMasterd(options, args):
                           (constants.MASTERD_USER, constants.DAEMONS_GROUP))
     sys.exit(constants.EXIT_FAILURE)
 
+  # Determine static runtime architecture information
+  runtime.InitArchInfo()
+
   # Check the configuration is sane before anything else
   try:
     config.ConfigWriter()
@@ -608,8 +691,7 @@ def PrepMasterd(options, _):
   utils.RemoveFile(constants.MASTER_SOCKET)
 
   mainloop = daemon.Mainloop()
-  master = MasterServer(mainloop, constants.MASTER_SOCKET,
-                        options.uid, options.gid)
+  master = MasterServer(constants.MASTER_SOCKET, options.uid, options.gid)
   return (mainloop, master)
 
 
@@ -623,7 +705,7 @@ def ExecMasterd(options, args, prep_data): # pylint: disable=W0613
     try:
       master.setup_queue()
       try:
-        mainloop.Run()
+        mainloop.Run(shutdown_wait_fn=master.WaitForShutdown)
       finally:
         master.server_cleanup()
     finally:
@@ -631,6 +713,8 @@ def ExecMasterd(options, args, prep_data): # pylint: disable=W0613
   finally:
     utils.RemoveFile(constants.MASTER_SOCKET)
 
+  logging.info("Clean master daemon shutdown")
+
 
 def Main():
   """Main function"""
index 1135fe9..d95680a 100644 (file)
@@ -111,8 +111,7 @@ def _DecodeImportExportIO(ieio, ieioargs):
 
 
 class MlockallRequestExecutor(http.server.HttpServerRequestExecutor):
-  """Custom Request Executor class that ensures NodeHttpServer children are
-  locked in ram.
+  """Subclass ensuring request handlers are locked in RAM.
 
   """
   def __init__(self, *args, **kwargs):
@@ -121,7 +120,7 @@ class MlockallRequestExecutor(http.server.HttpServerRequestExecutor):
     http.server.HttpServerRequestExecutor.__init__(self, *args, **kwargs)
 
 
-class NodeHttpServer(http.server.HttpServer):
+class NodeRequestHandler(http.server.HttpServerHandler):
   """The server implementation.
 
   This class holds all methods exposed over the RPC interface.
@@ -130,8 +129,8 @@ class NodeHttpServer(http.server.HttpServer):
   # too many public methods, and unused args - all methods get params
   # due to the API
   # pylint: disable=R0904,W0613
-  def __init__(self, *args, **kwargs):
-    http.server.HttpServer.__init__(self, *args, **kwargs)
+  def __init__(self):
+    http.server.HttpServerHandler.__init__(self)
     self.noded_pid = os.getpid()
 
   def HandleRequest(self, req):
@@ -170,7 +169,7 @@ class NodeHttpServer(http.server.HttpServer):
       logging.exception("Error in RPC call")
       result = (False, "Error while executing backend function: %s" % str(err))
 
-    return serializer.DumpJson(result, indent=False)
+    return serializer.DumpJson(result)
 
   # the new block devices  --------------------------
 
@@ -217,7 +216,7 @@ class NodeHttpServer(http.server.HttpServer):
     """Remove a block device.
 
     """
-    devlist = [(objects.Disk.FromDict(ds), uid) for ds, uid in params]
+    devlist = [(objects.Disk.FromDict(ds), uid) for ds, uid in params[0]]
     return backend.BlockdevRename(devlist)
 
   @staticmethod
@@ -278,7 +277,7 @@ class NodeHttpServer(http.server.HttpServer):
 
     """
     disks = [objects.Disk.FromDict(dsk_s)
-             for dsk_s in params]
+             for dsk_s in params[0]]
     return [status.ToDict()
             for status in backend.BlockdevGetmirrorstatus(disks)]
 
@@ -289,10 +288,7 @@ class NodeHttpServer(http.server.HttpServer):
     """
     (node_disks, ) = params
 
-    node_name = netutils.Hostname.GetSysName()
-
-    disks = [objects.Disk.FromDict(dsk_s)
-             for dsk_s in node_disks.get(node_name, [])]
+    disks = [objects.Disk.FromDict(dsk_s) for dsk_s in node_disks]
 
     result = []
 
@@ -580,13 +576,13 @@ class NodeHttpServer(http.server.HttpServer):
     return backend.AcceptInstance(instance, info, target)
 
   @staticmethod
-  def perspective_finalize_migration(params):
-    """Finalize the instance migration.
+  def perspective_instance_finalize_migration_dst(params):
+    """Finalize the instance migration on the destination node.
 
     """
     instance, info, success = params
     instance = objects.Instance.FromDict(instance)
-    return backend.FinalizeMigration(instance, info, success)
+    return backend.FinalizeMigrationDst(instance, info, success)
 
   @staticmethod
   def perspective_instance_migrate(params):
@@ -598,6 +594,23 @@ class NodeHttpServer(http.server.HttpServer):
     return backend.MigrateInstance(instance, target, live)
 
   @staticmethod
+  def perspective_instance_finalize_migration_src(params):
+    """Finalize the instance migration on the source node.
+
+    """
+    instance, success, live = params
+    instance = objects.Instance.FromDict(instance)
+    return backend.FinalizeMigrationSource(instance, success, live)
+
+  @staticmethod
+  def perspective_instance_get_migration_status(params):
+    """Reports migration status.
+
+    """
+    instance = objects.Instance.FromDict(params[0])
+    return backend.GetMigrationStatus(instance).ToDict()
+
+  @staticmethod
   def perspective_instance_reboot(params):
     """Reboot an instance.
 
@@ -608,6 +621,15 @@ class NodeHttpServer(http.server.HttpServer):
     return backend.InstanceReboot(instance, reboot_type, shutdown_timeout)
 
   @staticmethod
+  def perspective_instance_balloon_memory(params):
+    """Modify instance runtime memory.
+
+    """
+    instance_dict, memory = params
+    instance = objects.Instance.FromDict(instance_dict)
+    return backend.InstanceBalloonMemory(instance, memory)
+
+  @staticmethod
   def perspective_instance_info(params):
     """Query instance information.
 
@@ -639,14 +661,6 @@ class NodeHttpServer(http.server.HttpServer):
   # node --------------------------
 
   @staticmethod
-  def perspective_node_tcp_ping(params):
-    """Do a TcpPing on the remote node.
-
-    """
-    return netutils.TcpPing(params[1], params[2], timeout=params[3],
-                            live_port_needed=params[4], source=params[0])
-
-  @staticmethod
   def perspective_node_has_ip_address(params):
     """Checks if a node has the given ip address.
 
@@ -658,8 +672,8 @@ class NodeHttpServer(http.server.HttpServer):
     """Query node information.
 
     """
-    vgname, hypervisor_type = params
-    return backend.GetNodeInfo(vgname, hypervisor_type)
+    (vg_names, hv_names) = params
+    return backend.GetNodeInfo(vg_names, hv_names)
 
   @staticmethod
   def perspective_etc_hosts_modify(params):
@@ -678,18 +692,42 @@ class NodeHttpServer(http.server.HttpServer):
     return backend.VerifyNode(params[0], params[1])
 
   @staticmethod
-  def perspective_node_start_master(params):
-    """Promote this node to master status.
+  def perspective_node_start_master_daemons(params):
+    """Start the master daemons on this node.
 
     """
-    return backend.StartMaster(params[0], params[1])
+    return backend.StartMasterDaemons(params[0])
+
+  @staticmethod
+  def perspective_node_activate_master_ip(params):
+    """Activate the master IP on this node.
+
+    """
+    master_params = objects.MasterNetworkParameters.FromDict(params[0])
+    return backend.ActivateMasterIp(master_params, params[1])
+
+  @staticmethod
+  def perspective_node_deactivate_master_ip(params):
+    """Deactivate the master IP on this node.
+
+    """
+    master_params = objects.MasterNetworkParameters.FromDict(params[0])
+    return backend.DeactivateMasterIp(master_params, params[1])
 
   @staticmethod
   def perspective_node_stop_master(params):
-    """Demote this node from master status.
+    """Stops master daemons on this node.
+
+    """
+    return backend.StopMasterDaemons()
+
+  @staticmethod
+  def perspective_node_change_master_netmask(params):
+    """Change the master IP netmask.
 
     """
-    return backend.StopMaster(params[0])
+    return backend.ChangeMasterNetmask(params[0], params[1], params[2],
+                                       params[3])
 
   @staticmethod
   def perspective_node_leave_cluster(params):
@@ -737,7 +775,7 @@ class NodeHttpServer(http.server.HttpServer):
     files are accepted.
 
     """
-    return backend.UploadFile(*params)
+    return backend.UploadFile(*(params[0]))
 
   @staticmethod
   def perspective_master_info(params):
@@ -881,7 +919,7 @@ class NodeHttpServer(http.server.HttpServer):
 
     """
     # TODO: What if a file fails to rename?
-    return [backend.JobQueueRename(old, new) for old, new in params]
+    return [backend.JobQueueRename(old, new) for old, new in params[0]]
 
   # hypervisor ---------------
 
@@ -918,7 +956,7 @@ class NodeHttpServer(http.server.HttpServer):
     """Starts an import daemon.
 
     """
-    (opts_s, instance, component, dest, dest_args) = params
+    (opts_s, instance, component, (dest, dest_args)) = params
 
     opts = objects.ImportExportOptions.FromDict(opts_s)
 
@@ -934,7 +972,7 @@ class NodeHttpServer(http.server.HttpServer):
     """Starts an export daemon.
 
     """
-    (opts_s, host, port, instance, component, source, source_args) = params
+    (opts_s, host, port, instance, component, (source, source_args)) = params
 
     opts = objects.ImportExportOptions.FromDict(opts_s)
 
@@ -1012,11 +1050,15 @@ def PrepNoded(options, _):
     # startup of the whole node daemon because of this
     logging.critical("Can't init/verify the queue, proceeding anyway: %s", err)
 
+  handler = NodeRequestHandler()
+
   mainloop = daemon.Mainloop()
-  server = NodeHttpServer(mainloop, options.bind_address, options.port,
-                          ssl_params=ssl_params, ssl_verify_peer=True,
-                          request_executor_class=request_executor_class)
+  server = \
+    http.server.HttpServer(mainloop, options.bind_address, options.port,
+      handler, ssl_params=ssl_params, ssl_verify_peer=True,
+      request_executor_class=request_executor_class)
   server.Start()
+
   return (mainloop, server)
 
 
index 6a8a76a..972419c 100644 (file)
@@ -64,68 +64,40 @@ class RemoteApiRequestContext(object):
     self.body_data = None
 
 
-class JsonErrorRequestExecutor(http.server.HttpServerRequestExecutor):
-  """Custom Request Executor class that formats HTTP errors in JSON.
+class RemoteApiHandler(http.auth.HttpServerRequestAuthentication,
+                       http.server.HttpServerHandler):
+  """REST Request Handler Class.
 
   """
-  error_content_type = http.HTTP_APP_JSON
+  AUTH_REALM = "Ganeti Remote API"
 
-  def _FormatErrorMessage(self, values):
-    """Formats the body of an error message.
+  def __init__(self, user_fn, _client_cls=None):
+    """Initializes this class.
 
-    @type values: dict
-    @param values: dictionary with keys code, message and explain.
-    @rtype: string
-    @return: the body of the message
+    @type user_fn: callable
+    @param user_fn: Function receiving username as string and returning
+      L{http.auth.PasswordFileUser} or C{None} if user is not found
 
     """
-    return serializer.DumpJson(values, indent=True)
-
-
-class RemoteApiHttpServer(http.auth.HttpServerRequestAuthentication,
-                          http.server.HttpServer):
-  """REST Request Handler Class.
-
-  """
-  AUTH_REALM = "Ganeti Remote API"
-
-  def __init__(self, *args, **kwargs):
     # pylint: disable=W0233
     # it seems pylint doesn't see the second parent class there
-    http.server.HttpServer.__init__(self, *args, **kwargs)
+    http.server.HttpServerHandler.__init__(self)
     http.auth.HttpServerRequestAuthentication.__init__(self)
+    self._client_cls = _client_cls
     self._resmap = connector.Mapper()
-    self._users = None
+    self._user_fn = user_fn
 
-  def LoadUsers(self, filename):
-    """Loads a file containing users and passwords.
+  @staticmethod
+  def FormatErrorMessage(values):
+    """Formats the body of an error message.
 
-    @type filename: string
-    @param filename: Path to file
+    @type values: dict
+    @param values: dictionary with keys C{code}, C{message} and C{explain}.
+    @rtype: tuple; (string, string)
+    @return: Content-type and response body
 
     """
-    logging.info("Reading users file at %s", filename)
-    try:
-      try:
-        contents = utils.ReadFile(filename)
-      except EnvironmentError, err:
-        self._users = None
-        if err.errno == errno.ENOENT:
-          logging.warning("No users file at %s", filename)
-        else:
-          logging.warning("Error while reading %s: %s", filename, err)
-        return False
-
-      users = http.auth.ParsePasswordFile(contents)
-
-    except Exception, err: # pylint: disable=W0703
-      # We don't care about the type of exception
-      logging.error("Error while parsing %s: %s", filename, err)
-      return False
-
-    self._users = users
-
-    return True
+    return (http.HTTP_APP_JSON, serializer.DumpJson(values))
 
   def _GetRequestContext(self, req):
     """Returns the context for a request.
@@ -138,7 +110,7 @@ class RemoteApiHttpServer(http.auth.HttpServerRequestAuthentication,
                      self._resmap.getController(req.request_path)
 
       ctx = RemoteApiRequestContext()
-      ctx.handler = HandlerClass(items, args, req)
+      ctx.handler = HandlerClass(items, args, req, _client_cls=self._client_cls)
 
       method = req.request_method.upper()
       try:
@@ -177,15 +149,10 @@ class RemoteApiHttpServer(http.auth.HttpServerRequestAuthentication,
     """
     ctx = self._GetRequestContext(req)
 
-    # Check username and password
-    valid_user = False
-    if self._users:
-      user = self._users.get(username, None)
-      if user and self.VerifyBasicAuthPassword(req, username, password,
-                                               user.password):
-        valid_user = True
-
-    if not valid_user:
+    user = self._user_fn(username)
+    if not (user and
+            self.VerifyBasicAuthPassword(req, username, password,
+                                         user.password)):
       # Unknown user or password wrong
       return False
 
@@ -227,16 +194,59 @@ class RemoteApiHttpServer(http.auth.HttpServerRequestAuthentication,
       raise http.HttpGatewayTimeout()
     except luxi.ProtocolError, err:
       raise http.HttpBadGateway(str(err))
-    except:
-      method = req.request_method.upper()
-      logging.exception("Error while handling the %s request", method)
-      raise
 
     req.resp_headers[http.HTTP_CONTENT_TYPE] = http.HTTP_APP_JSON
 
     return serializer.DumpJson(result)
 
 
+class RapiUsers:
+  def __init__(self):
+    """Initializes this class.
+
+    """
+    self._users = None
+
+  def Get(self, username):
+    """Checks whether a user exists.
+
+    """
+    if self._users:
+      return self._users.get(username, None)
+    else:
+      return None
+
+  def Load(self, filename):
+    """Loads a file containing users and passwords.
+
+    @type filename: string
+    @param filename: Path to file
+
+    """
+    logging.info("Reading users file at %s", filename)
+    try:
+      try:
+        contents = utils.ReadFile(filename)
+      except EnvironmentError, err:
+        self._users = None
+        if err.errno == errno.ENOENT:
+          logging.warning("No users file at %s", filename)
+        else:
+          logging.warning("Error while reading %s: %s", filename, err)
+        return False
+
+      users = http.auth.ParsePasswordFile(contents)
+
+    except Exception, err: # pylint: disable=W0703
+      # We don't care about the type of exception
+      logging.error("Error while parsing %s: %s", filename, err)
+      return False
+
+    self._users = users
+
+    return True
+
+
 class FileEventHandler(asyncnotifier.FileEventHandlerBase):
   def __init__(self, wm, path, cb):
     """Initializes this class.
@@ -308,21 +318,21 @@ def PrepRapi(options, _):
   """Prep remote API function, executed with the PID file held.
 
   """
-
   mainloop = daemon.Mainloop()
-  server = RemoteApiHttpServer(mainloop, options.bind_address, options.port,
-                               ssl_params=options.ssl_params,
-                               ssl_verify_peer=False,
-                               request_executor_class=JsonErrorRequestExecutor)
+
+  users = RapiUsers()
+
+  handler = RemoteApiHandler(users.Get)
 
   # Setup file watcher (it'll be driven by asyncore)
   SetupFileWatcher(constants.RAPI_USERS_FILE,
-                   compat.partial(server.LoadUsers, constants.RAPI_USERS_FILE))
+                   compat.partial(users.Load, constants.RAPI_USERS_FILE))
 
-  server.LoadUsers(constants.RAPI_USERS_FILE)
+  users.Load(constants.RAPI_USERS_FILE)
 
-  # pylint: disable=E1101
-  # it seems pylint doesn't see the second parent class there
+  server = \
+    http.server.HttpServer(mainloop, options.bind_address, options.port,
+      handler, ssl_params=options.ssl_params, ssl_verify_peer=False)
   server.Start()
 
   return (mainloop, server)
index 9cc2a34..2399d81 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007, 2008, 2010 Google Inc.
+# Copyright (C) 2006, 2007, 2008, 2010, 2011, 2012 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
@@ -152,6 +152,9 @@ class SimpleConfigReader(object):
   def GetMasterNetdev(self):
     return self._config_data["cluster"]["master_netdev"]
 
+  def GetMasterNetmask(self):
+    return self._config_data["cluster"]["master_netmask"]
+
   def GetFileStorageDir(self):
     return self._config_data["cluster"]["file_storage_dir"]
 
@@ -270,7 +273,6 @@ class SimpleStore(object):
     - keys are restricted to predefined values
 
   """
-  _SS_FILEPREFIX = "ssconf_"
   _VALID_KEYS = (
     constants.SS_CLUSTER_NAME,
     constants.SS_CLUSTER_TAGS,
@@ -280,6 +282,7 @@ class SimpleStore(object):
     constants.SS_MASTER_CANDIDATES_IPS,
     constants.SS_MASTER_IP,
     constants.SS_MASTER_NETDEV,
+    constants.SS_MASTER_NETMASK,
     constants.SS_MASTER_NODE,
     constants.SS_NODE_LIST,
     constants.SS_NODE_PRIMARY_IPS,
@@ -310,7 +313,7 @@ class SimpleStore(object):
       raise errors.ProgrammerError("Invalid key requested from SSConf: '%s'"
                                    % str(key))
 
-    filename = self._cfg_dir + "/" + self._SS_FILEPREFIX + key
+    filename = self._cfg_dir + "/" + constants.SSCONF_FILEPREFIX + key
     return filename
 
   def _ReadFile(self, key, default=None):
@@ -408,6 +411,17 @@ class SimpleStore(object):
     """
     return self._ReadFile(constants.SS_MASTER_NETDEV)
 
+  def GetMasterNetmask(self):
+    """Get the master netmask.
+
+    """
+    try:
+      return self._ReadFile(constants.SS_MASTER_NETMASK)
+    except errors.ConfigurationError:
+      family = self.GetPrimaryIPFamily()
+      ipcls = netutils.IPAddress.GetClassFromIpFamily(family)
+      return ipcls.iplen
+
   def GetMasterNode(self):
     """Get the hostname of the master node for this cluster.
 
index 8cf01ae..5fdb723 100644 (file)
 
 """
 
-import errno
 import os
 import os.path
 import optparse
 import sys
-import stat
 import logging
 
 from ganeti import constants
@@ -49,79 +47,6 @@ ALL_TYPES = frozenset([
   ])
 
 
-class EnsureError(errors.GenericError):
-  """Top-level error class related to this script.
-
-  """
-
-
-def EnsurePermission(path, mode, uid=-1, gid=-1, must_exist=True,
-                     _chmod_fn=os.chmod, _chown_fn=os.chown, _stat_fn=os.stat):
-  """Ensures that given path has given mode.
-
-  @param path: The path to the file
-  @param mode: The mode of the file
-  @param uid: The uid of the owner of this file
-  @param gid: The gid of the owner of this file
-  @param must_exist: Specifies if non-existance of path will be an error
-  @param _chmod_fn: chmod function to use (unittest only)
-  @param _chown_fn: chown function to use (unittest only)
-
-  """
-  logging.debug("Checking %s", path)
-  try:
-    st = _stat_fn(path)
-
-    fmode = stat.S_IMODE(st[stat.ST_MODE])
-    if fmode != mode:
-      logging.debug("Changing mode of %s from %#o to %#o", path, fmode, mode)
-      _chmod_fn(path, mode)
-
-    if max(uid, gid) > -1:
-      fuid = st[stat.ST_UID]
-      fgid = st[stat.ST_GID]
-      if fuid != uid or fgid != gid:
-        logging.debug("Changing owner of %s from UID %s/GID %s to"
-                      " UID %s/GID %s", path, fuid, fgid, uid, gid)
-        _chown_fn(path, uid, gid)
-  except EnvironmentError, err:
-    if err.errno == errno.ENOENT:
-      if must_exist:
-        raise EnsureError("Path %s should exist, but does not" % path)
-    else:
-      raise EnsureError("Error while changing permissions on %s: %s" %
-                        (path, err))
-
-
-def EnsureDir(path, mode, uid, gid, _lstat_fn=os.lstat, _mkdir_fn=os.mkdir,
-              _ensure_fn=EnsurePermission):
-  """Ensures that given path is a dir and has given mode, uid and gid set.
-
-  @param path: The path to the file
-  @param mode: The mode of the file
-  @param uid: The uid of the owner of this file
-  @param gid: The gid of the owner of this file
-  @param _lstat_fn: Stat function to use (unittest only)
-  @param _mkdir_fn: mkdir function to use (unittest only)
-  @param _ensure_fn: ensure function to use (unittest only)
-
-  """
-  logging.debug("Checking directory %s", path)
-  try:
-    # We don't want to follow symlinks
-    st = _lstat_fn(path)
-  except EnvironmentError, err:
-    if err.errno != errno.ENOENT:
-      raise EnsureError("stat(2) on %s failed: %s" % (path, err))
-    _mkdir_fn(path)
-  else:
-    if not stat.S_ISDIR(st[stat.ST_MODE]):
-      raise EnsureError("Path %s is expected to be a directory, but isn't" %
-                        path)
-
-  _ensure_fn(path, mode, uid=uid, gid=gid)
-
-
 def RecursiveEnsure(path, uid, gid, dir_perm, file_perm):
   """Ensures permissions recursively down a directory.
 
@@ -141,11 +66,12 @@ def RecursiveEnsure(path, uid, gid, dir_perm, file_perm):
 
   for root, dirs, files in os.walk(path):
     for subdir in dirs:
-      EnsurePermission(os.path.join(root, subdir), dir_perm, uid=uid, gid=gid)
+      utils.EnforcePermission(os.path.join(root, subdir), dir_perm, uid=uid,
+                              gid=gid)
 
     for filename in files:
-      EnsurePermission(os.path.join(root, filename), file_perm, uid=uid,
-                       gid=gid)
+      utils.EnforcePermission(os.path.join(root, filename), file_perm, uid=uid,
+                              gid=gid)
 
 
 def EnsureQueueDir(path, mode, uid, gid):
@@ -159,7 +85,8 @@ def EnsureQueueDir(path, mode, uid, gid):
   """
   for filename in utils.ListVisibleFiles(path):
     if constants.JOB_FILE_RE.match(filename):
-      EnsurePermission(utils.PathJoin(path, filename), mode, uid=uid, gid=gid)
+      utils.EnforcePermission(utils.PathJoin(path, filename), mode, uid=uid,
+                              gid=gid)
 
 
 def ProcessPath(path):
@@ -176,12 +103,13 @@ def ProcessPath(path):
     # No additional parameters
     assert len(path[5:]) == 0
     if pathtype == DIR:
-      EnsureDir(pathname, mode, uid, gid)
+      utils.MakeDirWithPerm(pathname, mode, uid, gid)
     elif pathtype == QUEUE_DIR:
       EnsureQueueDir(pathname, mode, uid, gid)
   elif pathtype == FILE:
     (must_exist, ) = path[5:]
-    EnsurePermission(pathname, mode, uid=uid, gid=gid, must_exist=must_exist)
+    utils.EnforcePermission(pathname, mode, uid=uid, gid=gid,
+                            must_exist=must_exist)
 
 
 def GetPaths():
@@ -209,6 +137,10 @@ def GetPaths():
      getent.masterd_gid, False),
     (constants.RAPI_CERT_FILE, FILE, 0440, getent.rapi_uid,
      getent.masterd_gid, False),
+    (constants.SPICE_CERT_FILE, FILE, 0440, getent.noded_uid,
+     getent.masterd_gid, False),
+    (constants.SPICE_CACERT_FILE, FILE, 0440, getent.noded_uid,
+     getent.masterd_gid, False),
     (constants.NODED_CERT_FILE, FILE, 0440, getent.masterd_uid,
      getent.masterd_gid, False),
     ]
@@ -323,7 +255,7 @@ def Main():
     if opts.full_run:
       RecursiveEnsure(constants.JOB_QUEUE_ARCHIVE_DIR, getent.masterd_uid,
                       getent.masterd_gid, 0700, 0600)
-  except EnsureError, err:
+  except errors.GenericError, err:
     logging.error("An error occurred while setting permissions: %s", err)
     return constants.EXIT_FAILURE
 
index 6167794..a95cf7f 100644 (file)
@@ -32,6 +32,7 @@ import os
 import re
 import errno
 import pwd
+import time
 import itertools
 import select
 import logging
@@ -154,6 +155,46 @@ def ValidateServiceName(name):
   return name
 
 
+def _ComputeMissingKeys(key_path, options, defaults):
+  """Helper functions to compute which keys a invalid.
+
+  @param key_path: The current key path (if any)
+  @param options: The user provided options
+  @param defaults: The default dictionary
+  @return: A list of invalid keys
+
+  """
+  defaults_keys = frozenset(defaults.keys())
+  invalid = []
+  for key, value in options.items():
+    if key_path:
+      new_path = "%s/%s" % (key_path, key)
+    else:
+      new_path = key
+
+    if key not in defaults_keys:
+      invalid.append(new_path)
+    elif isinstance(value, dict):
+      invalid.extend(_ComputeMissingKeys(new_path, value, defaults[key]))
+
+  return invalid
+
+
+def VerifyDictOptions(options, defaults):
+  """Verify a dict has only keys set which also are in the defaults dict.
+
+  @param options: The user provided options
+  @param defaults: The default dictionary
+  @raise error.OpPrereqError: If one of the keys is not supported
+
+  """
+  invalid = _ComputeMissingKeys("", options, defaults)
+
+  if invalid:
+    raise errors.OpPrereqError("Provided option keys not supported: %s" %
+                               CommaJoin(invalid), errors.ECODE_INVAL)
+
+
 def ListVolumeGroups():
   """List volume groups and their size
 
@@ -255,12 +296,38 @@ def ParseCpuMask(cpu_mask):
   return cpu_list
 
 
+def ParseMultiCpuMask(cpu_mask):
+  """Parse a multiple CPU mask definition and return the list of CPU IDs.
+
+  CPU mask format: colon-separated list of comma-separated list of CPU IDs
+  or dash-separated ID ranges, with optional "all" as CPU value
+  Example: "0-2,5:all:1,5,6:2" -> [ [ 0,1,2,5 ], [ -1 ], [ 1, 5, 6 ], [ 2 ] ]
+
+  @type cpu_mask: str
+  @param cpu_mask: multiple CPU mask definition
+  @rtype: list of lists of int
+  @return: list of lists of CPU IDs
+
+  """
+  if not cpu_mask:
+    return []
+  cpu_list = []
+  for range_def in cpu_mask.split(constants.CPU_PINNING_SEP):
+    if range_def == constants.CPU_PINNING_ALL:
+      cpu_list.append([constants.CPU_PINNING_ALL_VAL, ])
+    else:
+      # Uniquify and sort the list before adding
+      cpu_list.append(sorted(set(ParseCpuMask(range_def))))
+
+  return cpu_list
+
+
 def GetHomeDir(user, default=None):
   """Try to get the homedir of the given user.
 
   The user can be passed either as a string (denoting the name) or as
   an integer (denoting the user id). If the user is not found, the
-  'default' argument is returned, which defaults to None.
+  C{default} argument is returned, which defaults to C{None}.
 
   """
   try:
@@ -566,6 +633,13 @@ def SignalHandled(signums):
   return wrap
 
 
+def TimeoutExpired(epoch, timeout, _time_fn=time.time):
+  """Checks whether a timeout has expired.
+
+  """
+  return _time_fn() > (epoch + timeout)
+
+
 class SignalWakeupFd(object):
   try:
     # This is only supported in Python 2.5 and above (some distributions
index 9182e8e..ec8ce34 100644 (file)
 
 import re
 import time
+import itertools
+
+from ganeti import compat
+from ganeti.utils import text
 
 
 _SORTER_GROUPS = 8
 _SORTER_RE = re.compile("^%s(.*)$" % (_SORTER_GROUPS * "(\D+|\d+)?"))
-_SORTER_DIGIT = re.compile("^\d+$")
 
 
 def UniqueSequence(seq):
@@ -46,6 +49,29 @@ def UniqueSequence(seq):
   return [i for i in seq if i not in seen and not seen.add(i)]
 
 
+def JoinDisjointDicts(dict_a, dict_b):
+  """Joins dictionaries with no conflicting keys.
+
+  Enforces the constraint that the two key sets must be disjoint, and then
+  merges the two dictionaries in a new dictionary that is returned to the
+  caller.
+
+  @type dict_a: dict
+  @param dict_a: the first dictionary
+  @type dict_b: dict
+  @param dict_b: the second dictionary
+  @rtype: dict
+  @return: a new dictionary containing all the key/value pairs contained in the
+  two dictionaries.
+
+  """
+  assert not (set(dict_a) & set(dict_b)), ("Duplicate keys found while joining"
+                                           " %s and %s" % (dict_a, dict_b))
+  result = dict_a.copy()
+  result.update(dict_b)
+  return result
+
+
 def FindDuplicates(seq):
   """Identifies duplicates in a list.
 
@@ -73,7 +99,7 @@ def _NiceSortTryInt(val):
   """Attempts to convert a string to an integer.
 
   """
-  if val and _SORTER_DIGIT.match(val):
+  if val and val.isdigit():
     return int(val)
   else:
     return val
@@ -125,6 +151,99 @@ def InvertDict(dict_in):
   return dict(zip(dict_in.values(), dict_in.keys()))
 
 
+def InsertAtPos(src, pos, other):
+  """Inserts C{other} at given C{pos} into C{src}.
+
+  @note: This function does not modify C{src} in place but returns a new copy
+
+  @type src: list
+  @param src: The source list in which we want insert elements
+  @type pos: int
+  @param pos: The position where we want to start insert C{other}
+  @type other: list
+  @param other: The other list to insert into C{src}
+  @return: A copy of C{src} with C{other} inserted at C{pos}
+
+  """
+  new = src[:pos]
+  new.extend(other)
+  new.extend(src[pos:])
+
+  return new
+
+
+def SequenceToDict(seq, key=compat.fst):
+  """Converts a sequence to a dictionary with duplicate detection.
+
+  @type seq: sequen
+  @param seq: Input sequence
+  @type key: callable
+  @param key: Function for retrieving dictionary key from sequence element
+  @rtype: dict
+
+  """
+  keys = map(key, seq)
+
+  duplicates = FindDuplicates(keys)
+  if duplicates:
+    raise ValueError("Duplicate keys found: %s" % text.CommaJoin(duplicates))
+
+  assert len(keys) == len(seq)
+
+  return dict(zip(keys, seq))
+
+
+def _MakeFlatToDict(data):
+  """Helper function for C{FlatToDict}.
+
+  This function is recursively called
+
+  @param data: The input data as described in C{FlatToDict}, already splitted
+  @returns: The so far converted dict
+
+  """
+  if not compat.fst(compat.fst(data)):
+    assert len(data) == 1, \
+      "not bottom most element, found %d elements, expected 1" % len(data)
+    return compat.snd(compat.fst(data))
+
+  keyfn = lambda e: compat.fst(e).pop(0)
+  return dict([(k, _MakeFlatToDict(list(g)))
+               for (k, g) in itertools.groupby(sorted(data), keyfn)])
+
+
+def FlatToDict(data, field_sep="/"):
+  """Converts a flat structure to a fully fledged dict.
+
+  It accept a list of tuples in the form::
+
+    [
+      ("foo/bar", {"key1": "data1", "key2": "data2"}),
+      ("foo/baz", {"key3" :"data3" }),
+    ]
+
+  where the first element is the key separated by C{field_sep}.
+
+  This would then return::
+
+    {
+      "foo": {
+        "bar": {"key1": "data1", "key2": "data2"},
+        "baz": {"key3" :"data3" },
+        },
+    }
+
+  @type data: list of tuple
+  @param data: Input list to convert
+  @type field_sep: str
+  @param field_sep: The separator for the first field of the tuple
+  @returns: A dict based on the input list
+
+  """
+  return _MakeFlatToDict([(keys.split(field_sep), value)
+                          for (keys, value) in data])
+
+
 class RunningTimeout(object):
   """Class to calculate remaining timeout when doing several operations.
 
index c12ad0a..ed55f92 100644 (file)
@@ -28,6 +28,7 @@ import shutil
 import tempfile
 import errno
 import time
+import stat
 
 from ganeti import errors
 from ganeti import constants
@@ -37,6 +38,58 @@ from ganeti.utils import filelock
 #: Path generating random UUID
 _RANDOM_UUID_FILE = "/proc/sys/kernel/random/uuid"
 
+#: Directory used by fsck(8) to store recovered data, usually at a file
+#: system's root directory
+_LOST_AND_FOUND = "lost+found"
+
+# Possible values for keep_perms in WriteFile()
+KP_NEVER = 0
+KP_ALWAYS = 1
+KP_IF_EXISTS = 2
+
+KEEP_PERMS_VALUES = [
+  KP_NEVER,
+  KP_ALWAYS,
+  KP_IF_EXISTS,
+  ]
+
+
+def ErrnoOrStr(err):
+  """Format an EnvironmentError exception.
+
+  If the L{err} argument has an errno attribute, it will be looked up
+  and converted into a textual C{E...} description. Otherwise the
+  string representation of the error will be returned.
+
+  @type err: L{EnvironmentError}
+  @param err: the exception to format
+
+  """
+  if hasattr(err, "errno"):
+    detail = errno.errorcode[err.errno]
+  else:
+    detail = str(err)
+  return detail
+
+
+class FileStatHelper:
+  """Helper to store file handle's C{fstat}.
+
+  Useful in combination with L{ReadFile}'s C{preread} parameter.
+
+  """
+  def __init__(self):
+    """Initializes this class.
+
+    """
+    self.st = None
+
+  def __call__(self, fh):
+    """Calls C{fstat} on file handle.
+
+    """
+    self.st = os.fstat(fh.fileno())
+
 
 def ReadFile(file_name, size=-1, preread=None):
   """Reads a file.
@@ -63,7 +116,7 @@ def WriteFile(file_name, fn=None, data=None,
               mode=None, uid=-1, gid=-1,
               atime=None, mtime=None, close=True,
               dry_run=False, backup=False,
-              prewrite=None, postwrite=None):
+              prewrite=None, postwrite=None, keep_perms=KP_NEVER):
   """(Over)write a file atomically.
 
   The file_name and either fn (a function taking one argument, the
@@ -100,6 +153,14 @@ def WriteFile(file_name, fn=None, data=None,
   @param prewrite: function to be called before writing content
   @type postwrite: callable
   @param postwrite: function to be called after writing content
+  @type keep_perms: members of L{KEEP_PERMS_VALUES}
+  @param keep_perms: if L{KP_NEVER} (default), owner, group, and mode are
+      taken from the other parameters; if L{KP_ALWAYS}, owner, group, and
+      mode are copied from the existing file; if L{KP_IF_EXISTS}, owner,
+      group, and mode are taken from the file, and if the file doesn't
+      exist, they are taken from the other parameters. It is an error to
+      pass L{KP_ALWAYS} when the file doesn't exist or when C{uid}, C{gid},
+      or C{mode} are set to non-default values.
 
   @rtype: None or int
   @return: None if the 'close' parameter evaluates to True,
@@ -119,9 +180,28 @@ def WriteFile(file_name, fn=None, data=None,
     raise errors.ProgrammerError("Both atime and mtime must be either"
                                  " set or None")
 
+  if not keep_perms in KEEP_PERMS_VALUES:
+    raise errors.ProgrammerError("Invalid value for keep_perms: %s" %
+                                 keep_perms)
+  if keep_perms == KP_ALWAYS and (uid != -1 or gid != -1 or mode is not None):
+    raise errors.ProgrammerError("When keep_perms==KP_ALWAYS, 'uid', 'gid',"
+                                 " and 'mode' cannot be set")
+
   if backup and not dry_run and os.path.isfile(file_name):
     CreateBackup(file_name)
 
+  if keep_perms == KP_ALWAYS or keep_perms == KP_IF_EXISTS:
+    # os.stat() raises an exception if the file doesn't exist
+    try:
+      file_stat = os.stat(file_name)
+      mode = stat.S_IMODE(file_stat.st_mode)
+      uid = file_stat.st_uid
+      gid = file_stat.st_gid
+    except OSError:
+      if keep_perms == KP_ALWAYS:
+        raise
+      # else: if keeep_perms == KP_IF_EXISTS it's ok if the file doesn't exist
+
   # Whether temporary file needs to be removed (e.g. if any error occurs)
   do_remove = True
 
@@ -299,6 +379,9 @@ def RenameFile(old, new, mkdir=False, mkdir_mode=0750, dir_uid=None,
                dir_gid=None):
   """Renames a file.
 
+  This just creates the very least directory if it does not exist and C{mkdir}
+  is set to true.
+
   @type old: string
   @param old: Original path
   @type new: string
@@ -322,15 +405,88 @@ def RenameFile(old, new, mkdir=False, mkdir_mode=0750, dir_uid=None,
     if mkdir and err.errno == errno.ENOENT:
       # Create directory and try again
       dir_path = os.path.dirname(new)
-      Makedirs(dir_path, mode=mkdir_mode)
-      if not (dir_uid is None or dir_gid is None):
-        os.chown(dir_path, dir_uid, dir_gid)
+      MakeDirWithPerm(dir_path, mkdir_mode, dir_uid, dir_gid)
 
       return os.rename(old, new)
 
     raise
 
 
+def EnforcePermission(path, mode, uid=None, gid=None, must_exist=True,
+                      _chmod_fn=os.chmod, _chown_fn=os.chown, _stat_fn=os.stat):
+  """Enforces that given path has given permissions.
+
+  @param path: The path to the file
+  @param mode: The mode of the file
+  @param uid: The uid of the owner of this file
+  @param gid: The gid of the owner of this file
+  @param must_exist: Specifies if non-existance of path will be an error
+  @param _chmod_fn: chmod function to use (unittest only)
+  @param _chown_fn: chown function to use (unittest only)
+
+  """
+  logging.debug("Checking %s", path)
+
+  # chown takes -1 if you want to keep one part of the ownership, however
+  # None is Python standard for that. So we remap them here.
+  if uid is None:
+    uid = -1
+  if gid is None:
+    gid = -1
+
+  try:
+    st = _stat_fn(path)
+
+    fmode = stat.S_IMODE(st[stat.ST_MODE])
+    if fmode != mode:
+      logging.debug("Changing mode of %s from %#o to %#o", path, fmode, mode)
+      _chmod_fn(path, mode)
+
+    if max(uid, gid) > -1:
+      fuid = st[stat.ST_UID]
+      fgid = st[stat.ST_GID]
+      if fuid != uid or fgid != gid:
+        logging.debug("Changing owner of %s from UID %s/GID %s to"
+                      " UID %s/GID %s", path, fuid, fgid, uid, gid)
+        _chown_fn(path, uid, gid)
+  except EnvironmentError, err:
+    if err.errno == errno.ENOENT:
+      if must_exist:
+        raise errors.GenericError("Path %s should exist, but does not" % path)
+    else:
+      raise errors.GenericError("Error while changing permissions on %s: %s" %
+                                (path, err))
+
+
+def MakeDirWithPerm(path, mode, uid, gid, _lstat_fn=os.lstat,
+                    _mkdir_fn=os.mkdir, _perm_fn=EnforcePermission):
+  """Enforces that given path is a dir and has given mode, uid and gid set.
+
+  @param path: The path to the file
+  @param mode: The mode of the file
+  @param uid: The uid of the owner of this file
+  @param gid: The gid of the owner of this file
+  @param _lstat_fn: Stat function to use (unittest only)
+  @param _mkdir_fn: mkdir function to use (unittest only)
+  @param _perm_fn: permission setter function to use (unittest only)
+
+  """
+  logging.debug("Checking directory %s", path)
+  try:
+    # We don't want to follow symlinks
+    st = _lstat_fn(path)
+  except EnvironmentError, err:
+    if err.errno != errno.ENOENT:
+      raise errors.GenericError("stat(2) on %s failed: %s" % (path, err))
+    _mkdir_fn(path)
+  else:
+    if not stat.S_ISDIR(st[stat.ST_MODE]):
+      raise errors.GenericError(("Path %s is expected to be a directory, but "
+                                 "isn't") % path)
+
+  _perm_fn(path, mode, uid=uid, gid=gid)
+
+
 def Makedirs(path, mode=0750):
   """Super-mkdir; create a leaf directory and all intermediate ones.
 
@@ -390,7 +546,7 @@ def CreateBackup(file_name):
   return backup_name
 
 
-def ListVisibleFiles(path):
+def ListVisibleFiles(path, _is_mountpoint=os.path.ismount):
   """Returns a list of visible files in a directory.
 
   @type path: str
@@ -403,8 +559,22 @@ def ListVisibleFiles(path):
   if not IsNormAbsPath(path):
     raise errors.ProgrammerError("Path passed to ListVisibleFiles is not"
                                  " absolute/normalized: '%s'" % path)
-  files = [i for i in os.listdir(path) if not i.startswith(".")]
-  return files
+
+  mountpoint = _is_mountpoint(path)
+
+  def fn(name):
+    """File name filter.
+
+    Ignores files starting with a dot (".") as by Unix convention they're
+    considered hidden. The "lost+found" directory found at the root of some
+    filesystems is also hidden.
+
+    """
+    return not (name.startswith(".") or
+                (mountpoint and name == _LOST_AND_FOUND and
+                 os.path.isdir(os.path.join(path, name))))
+
+  return filter(fn, os.listdir(path))
 
 
 def EnsureDirs(dirs):
@@ -473,6 +643,20 @@ def IsNormAbsPath(path):
   return os.path.normpath(path) == path and os.path.isabs(path)
 
 
+def IsBelowDir(root, other_path):
+  """Check whether a path is below a root dir.
+
+  This works around the nasty byte-byte comparisation of commonprefix.
+
+  """
+  if not (os.path.isabs(root) and os.path.isabs(other_path)):
+    raise ValueError("Provided paths '%s' and '%s' are not absolute" %
+                     (root, other_path))
+  prepared_root = "%s%s" % (os.path.normpath(root), os.sep)
+  return os.path.commonprefix([prepared_root,
+                               os.path.normpath(other_path)]) == prepared_root
+
+
 def PathJoin(*args):
   """Safe-join a list of path components.
 
@@ -496,10 +680,9 @@ def PathJoin(*args):
   if not IsNormAbsPath(result):
     raise ValueError("Invalid parameters to PathJoin: '%s'" % str(args))
   # check that we're still under the original prefix
-  prefix = os.path.commonprefix([root, result])
-  if prefix != root:
+  if not IsBelowDir(root, result):
     raise ValueError("Error: path joining resulted in different prefix"
-                     " (%s != %s)" % (prefix, root))
+                     " (%s != %s)" % (result, root))
   return result
 
 
@@ -593,13 +776,24 @@ def ReadPidFile(pidfile):
       logging.exception("Can't read pid file")
     return 0
 
+  return _ParsePidFileContents(raw_data)
+
+
+def _ParsePidFileContents(data):
+  """Tries to extract a process ID from a PID file's content.
+
+  @type data: string
+  @rtype: int
+  @return: Zero if nothing could be read, PID otherwise
+
+  """
   try:
-    pid = int(raw_data)
-  except (TypeError, ValueError), err:
+    pid = int(data)
+  except (TypeError, ValueError):
     logging.info("Can't parse pid file contents", exc_info=True)
     return 0
-
-  return pid
+  else:
+    return pid
 
 
 def ReadLockedPidFile(path):
@@ -726,13 +920,21 @@ def WritePidFile(pidfile):
   """
   # We don't rename nor truncate the file to not drop locks under
   # existing processes
-  fd_pidfile = os.open(pidfile, os.O_WRONLY | os.O_CREAT, 0600)
+  fd_pidfile = os.open(pidfile, os.O_RDWR | os.O_CREAT, 0600)
 
   # Lock the PID file (and fail if not possible to do so). Any code
   # wanting to send a signal to the daemon should try to lock the PID
   # file before reading it. If acquiring the lock succeeds, the daemon is
   # no longer running and the signal should not be sent.
-  filelock.LockFile(fd_pidfile)
+  try:
+    filelock.LockFile(fd_pidfile)
+  except errors.LockError:
+    msg = ["PID file '%s' is already locked by another process" % pidfile]
+    # Try to read PID file
+    pid = _ParsePidFileContents(os.read(fd_pidfile, 100))
+    if pid > 0:
+      msg.append(", PID read from file is %s" % pid)
+    raise errors.PidFileLockError("".join(msg))
 
   os.write(fd_pidfile, "%d\n" % os.getpid())
 
@@ -791,3 +993,40 @@ def NewUUID():
 
   """
   return ReadFile(_RANDOM_UUID_FILE, size=128).rstrip("\n")
+
+
+class TemporaryFileManager(object):
+  """Stores the list of files to be deleted and removes them on demand.
+
+  """
+
+  def __init__(self):
+    self._files = []
+
+  def __del__(self):
+    self.Cleanup()
+
+  def Add(self, filename):
+    """Add file to list of files to be deleted.
+
+    @type filename: string
+    @param filename: path to filename to be added
+
+    """
+    self._files.append(filename)
+
+  def Remove(self, filename):
+    """Remove file from list of files to be deleted.
+
+    @type filename: string
+    @param filename: path to filename to be deleted
+
+    """
+    self._files.remove(filename)
+
+  def Cleanup(self):
+    """Delete all files marked for deletion
+
+    """
+    while self._files:
+      RemoveFile(self._files.pop())
index ea14d68..fc010cd 100644 (file)
@@ -34,7 +34,7 @@ except ImportError:
   ctypes = None
 
 
-# Flags for mlockall() (from bits/mman.h)
+# Flags for mlockall(2) (from bits/mman.h)
 _MCL_CURRENT = 1
 _MCL_FUTURE = 2
 
@@ -42,10 +42,10 @@ _MCL_FUTURE = 2
 def Mlockall(_ctypes=ctypes):
   """Lock current process' virtual address space into RAM.
 
-  This is equivalent to the C call mlockall(MCL_CURRENT|MCL_FUTURE),
-  see mlock(2) for more details. This function requires ctypes module.
+  This is equivalent to the C call C{mlockall(MCL_CURRENT | MCL_FUTURE)}. See
+  mlockall(2) for more details. This function requires the C{ctypes} module.
 
-  @raises errors.NoCtypesError: if ctypes module is not found
+  @raises errors.NoCtypesError: If the C{ctypes} module is not found
 
   """
   if _ctypes is None:
@@ -60,11 +60,11 @@ def Mlockall(_ctypes=ctypes):
     logging.error("Cannot set memory lock, ctypes cannot load libc")
     return
 
-  # Some older version of the ctypes module don't have built-in functionality
-  # to access the errno global variable, where function error codes are stored.
-  # By declaring this variable as a pointer to an integer we can then access
-  # its value correctly, should the mlockall call fail, in order to see what
-  # the actual error code was.
+  # The ctypes module before Python 2.6 does not have built-in functionality to
+  # access the global errno global (which, depending on the libc and build
+  # options, is per thread), where function error codes are stored. Use GNU
+  # libc's way to retrieve errno(3) instead, which is to use the pointer named
+  # "__errno_location" (see errno.h and bits/errno.h).
   # pylint: disable=W0212
   libc.__errno_location.restype = _ctypes.POINTER(_ctypes.c_int)
 
index 0931ee3..5dfc621 100644 (file)
@@ -62,7 +62,8 @@ def SetEtcHostsEntry(file_name, ip, hostname, aliases):
       out.write(line)
   _write_entry(written)
 
-  io.WriteFile(file_name, data=out.getvalue(), mode=0644)
+  io.WriteFile(file_name, data=out.getvalue(), uid=0, gid=0, mode=0644,
+               keep_perms=io.KP_IF_EXISTS)
 
 
 def AddHostToEtcHosts(hostname, ip):
@@ -104,7 +105,8 @@ def RemoveEtcHostsEntry(file_name, hostname):
 
     out.write(line)
 
-  io.WriteFile(file_name, data=out.getvalue(), mode=0644)
+  io.WriteFile(file_name, data=out.getvalue(), uid=0, gid=0, mode=0644,
+               keep_perms=io.KP_IF_EXISTS)
 
 
 def RemoveHostFromEtcHosts(hostname):
index a458755..8b865d6 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007, 2010, 2011 Google Inc.
+# Copyright (C) 2006, 2007, 2010, 2011, 2012 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
@@ -192,9 +192,9 @@ def RunCmd(cmd, env=None, output=None, cwd="/", reset_env=False,
     shell = False
 
   if output:
-    logging.debug("RunCmd %s, output file '%s'", strcmd, output)
+    logging.info("RunCmd %s, output file '%s'", strcmd, output)
   else:
-    logging.debug("RunCmd %s", strcmd)
+    logging.info("RunCmd %s", strcmd)
 
   cmd_env = _BuildCmdEnvironment(env, reset_env)
 
index f369c74..e16a861 100644 (file)
@@ -43,6 +43,9 @@ _MAC_CHECK_RE = re.compile("^([0-9a-f]{2}:){5}[0-9a-f]{2}$", re.I)
 #: Shell param checker regexp
 _SHELLPARAM_REGEX = re.compile(r"^[-a-zA-Z0-9._+/:%@]+$")
 
+#: ASCII equivalent of unicode character 'HORIZONTAL ELLIPSIS' (U+2026)
+_ASCII_ELLIPSIS = "..."
+
 
 def MatchNameComponent(key, name_list, case_sensitive=True):
   """Try to match a name against a list.
@@ -268,12 +271,16 @@ class ShellWriter:
     """
     assert self._indent >= 0
 
-    self._fh.write(self._indent * self.INDENT_STR)
-
     if args:
-      self._fh.write(txt % args)
+      line = txt % args
     else:
-      self._fh.write(txt)
+      line = txt
+
+    if line:
+      # Indent only if there's something on the line
+      self._fh.write(self._indent * self.INDENT_STR)
+
+    self._fh.write(line)
 
     self._fh.write("\n")
 
@@ -405,7 +412,7 @@ def CommaJoin(names):
   return ", ".join([str(val) for val in names])
 
 
-def FormatTime(val):
+def FormatTime(val, usecs=None):
   """Formats a time value.
 
   @type val: float or None
@@ -416,9 +423,15 @@ def FormatTime(val):
   """
   if val is None or not isinstance(val, (int, float)):
     return "N/A"
+
   # these two codes works on Linux, but they are not guaranteed on all
   # platforms
-  return time.strftime("%F %T", time.localtime(val))
+  result = time.strftime("%F %T", time.localtime(val))
+
+  if usecs is not None:
+    result += ".%06d" % usecs
+
+  return result
 
 
 def FormatSeconds(secs):
@@ -552,3 +565,26 @@ def FormatOrdinal(value):
     suffix = "th"
 
   return "%s%s" % (value, suffix)
+
+
+def Truncate(text, length):
+  """Truncate string and add ellipsis if needed.
+
+  @type text: string
+  @param text: Text
+  @type length: integer
+  @param length: Desired length
+  @rtype: string
+  @return: Truncated text
+
+  """
+  assert length > len(_ASCII_ELLIPSIS)
+
+  # Serialize if necessary
+  if not isinstance(text, basestring):
+    text = str(text)
+
+  if len(text) <= length:
+    return text
+  else:
+    return text[:length - len(_ASCII_ELLIPSIS)] + _ASCII_ELLIPSIS
index 256d2f1..982971e 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007, 2010, 2011 Google Inc.
+# Copyright (C) 2006, 2007, 2010, 2011, 2012 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
@@ -22,6 +22,7 @@
 
 """
 
+import sys
 import time
 import socket
 import errno
@@ -171,7 +172,7 @@ def GetClosedTempfile(*args, **kwargs):
   return path
 
 
-def ResetTempfileModule():
+def ResetTempfileModule(_time=time.time):
   """Resets the random name generator of the tempfile module.
 
   This function should be called after C{os.fork} in the child process to
@@ -182,13 +183,20 @@ def ResetTempfileModule():
 
   """
   # pylint: disable=W0212
-  if hasattr(tempfile, "_once_lock") and hasattr(tempfile, "_name_sequence"):
-    tempfile._once_lock.acquire()
+  if ((sys.hexversion >= 0x020703F0 and sys.hexversion < 0x03000000) or
+      sys.hexversion >= 0x030203F0):
+    # Python 2.7 automatically resets the RNG on pid changes (i.e. forking)
+    return
+
+  try:
+    lock = tempfile._once_lock
+    lock.acquire()
     try:
-      # Reset random name generator
-      tempfile._name_sequence = None
+      # Re-seed random name generator
+      if tempfile._name_sequence:
+        tempfile._name_sequence.rng.seed(hash(_time()) ^ os.getpid())
     finally:
-      tempfile._once_lock.release()
-  else:
+      lock.release()
+  except AttributeError:
     logging.critical("The tempfile module misses at least one of the"
                      " '_once_lock' and '_name_sequence' attributes")
index 71ba25d..0a91f41 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007, 2010, 2011 Google Inc.
+# Copyright (C) 2006, 2007, 2010, 2011, 2012 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
@@ -180,8 +180,10 @@ def VerifyX509Certificate(cert, warn_days, error_days):
   # Depending on the pyOpenSSL version, this can just return (None, None)
   (not_before, not_after) = GetX509CertValidity(cert)
 
+  now = time.time() + constants.NODE_MAX_CLOCK_SKEW
+
   return _VerifyCertificateInner(cert.has_expired(), not_before, not_after,
-                                 time.time(), warn_days, error_days)
+                                 now, warn_days, error_days)
 
 
 def SignX509Certificate(cert, key, salt):
@@ -259,6 +261,8 @@ def GenerateSelfSignedX509Cert(common_name, validity):
   @param common_name: commonName value
   @type validity: int
   @param validity: Validity for certificate in seconds
+  @return: a tuple of strings containing the PEM-encoded private key and
+           certificate
 
   """
   # Create private and public key
@@ -292,6 +296,8 @@ def GenerateSelfSignedSslCert(filename, common_name=constants.X509_CERT_CN,
   @param common_name: commonName value
   @type validity: int
   @param validity: validity of certificate in number of days
+  @return: a tuple of strings containing the PEM-encoded private key and
+           certificate
 
   """
   # TODO: Investigate using the cluster name instead of X505_CERT_CN for
@@ -301,3 +307,4 @@ def GenerateSelfSignedSslCert(filename, common_name=constants.X509_CERT_CN,
                                                    validity * 24 * 60 * 60)
 
   utils_io.WriteFile(filename, mode=0400, data=key_pem + cert_pem)
+  return (key_pem, cert_pem)
index c84f3e8..7cc3ce3 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 Google Inc.
+# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 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
@@ -92,7 +92,8 @@ def StartNodeDaemons():
   # on master or not, try to start the node daemon
   utils.EnsureDaemon(constants.NODED)
   # start confd as well. On non candidates it will be in disabled mode.
-  utils.EnsureDaemon(constants.CONFD)
+  if constants.ENABLE_CONFD:
+    utils.EnsureDaemon(constants.CONFD)
 
 
 def RunWatcherHooks():
@@ -302,8 +303,8 @@ def _VerifyDisks(cl, uuid, nodes, instances):
       continue
 
     if inst.status in HELPLESS_STATES or _CheckForOfflineNodes(nodes, inst):
-      logging.info("Skipping instance '%s' because it is in a helpless state or"
-                   " has offline secondaries", name)
+      logging.info("Skipping instance '%s' because it is in a helpless state"
+                   " or has offline secondaries", name)
       continue
 
     job.append(opcodes.OpInstanceActivateDisks(instance_name=name))
@@ -366,7 +367,8 @@ def ParseOptions():
   parser.add_option("--wait-children", dest="wait_children",
                     action="store_true", help="Wait for child processes")
   parser.add_option("--no-wait-children", dest="wait_children",
-                    action="store_false", help="Don't wait for child processes")
+                    action="store_false",
+                    help="Don't wait for child processes")
   # See optparse documentation for why default values are not set by options
   parser.set_defaults(wait_children=True)
   options, args = parser.parse_args()
@@ -409,23 +411,6 @@ def _UpdateInstanceStatus(filename, instances):
                                   for inst in instances])
 
 
-class _StatCb:
-  """Helper to store file handle's C{fstat}.
-
-  """
-  def __init__(self):
-    """Initializes this class.
-
-    """
-    self.st = None
-
-  def __call__(self, fh):
-    """Calls C{fstat} on file handle.
-
-    """
-    self.st = os.fstat(fh.fileno())
-
-
 def _ReadInstanceStatus(filename):
   """Reads an instance status file.
 
@@ -439,7 +424,7 @@ def _ReadInstanceStatus(filename):
   """
   logging.debug("Reading per-group instance status from '%s'", filename)
 
-  statcb = _StatCb()
+  statcb = utils.FileStatHelper()
   try:
     content = utils.ReadFile(filename, preread=statcb)
   except EnvironmentError, err:
@@ -638,13 +623,13 @@ def _GetGroupData(cl, uuid):
     opcodes.OpQuery(what=constants.QR_INSTANCE,
                     fields=["name", "status", "admin_state", "snodes",
                             "pnode.group.uuid", "snodes.group.uuid"],
-                    filter=[qlang.OP_EQUAL, "pnode.group.uuid", uuid],
+                    qfilter=[qlang.OP_EQUAL, "pnode.group.uuid", uuid],
                     use_locking=True),
 
     # Get all nodes in group
     opcodes.OpQuery(what=constants.QR_NODE,
                     fields=["name", "bootid", "offline"],
-                    filter=[qlang.OP_EQUAL, "group.uuid", uuid],
+                    qfilter=[qlang.OP_EQUAL, "group.uuid", uuid],
                     use_locking=True),
     ]
 
@@ -722,7 +707,8 @@ def _GroupWatcher(opts):
     raise errors.GenericError("Node group '%s' is not known by ssconf" %
                               group_uuid)
 
-  # Group UUID has been verified and should not contain any dangerous characters
+  # Group UUID has been verified and should not contain any dangerous
+  # characters
   state_path = constants.WATCHER_GROUP_STATE_FILE % group_uuid
   inst_status_path = constants.WATCHER_GROUP_INSTANCE_STATUS_FILE % group_uuid
 
index 33be4f1..6cb2a48 100644 (file)
@@ -130,6 +130,10 @@ class NodeMaintenance(object):
     """Check node status versus cluster desired state.
 
     """
+    if not constants.ENABLE_CONFD:
+      logging.warning("Confd use not enabled, cannot do maintenance")
+      return
+
     my_name = netutils.Hostname.GetSysName()
     req = \
       confd.client.ConfdClientRequest(type=constants.CONFD_REQ_NODE_ROLE_BYNAME,
index be52c65..3e8c463 100644 (file)
@@ -116,7 +116,7 @@ class WatcherState(object):
     fd = utils.WriteFile(filename,
                          data=serialized_form,
                          prewrite=utils.LockFile, close=False)
-    self.statefile = os.fdopen(fd, 'w+')
+    self.statefile = os.fdopen(fd, "w+")
 
   def Close(self):
     """Unlock configuration file and close it.
index 4736be5..8db03c7 100644 (file)
@@ -246,6 +246,7 @@ class WorkerPool(object):
     self._last_worker_id = 0
     self._workers = []
     self._quiescing = False
+    self._active = True
 
     # Terminating workers
     self._termworkers = []
@@ -340,6 +341,28 @@ class WorkerPool(object):
     finally:
       self._lock.release()
 
+  def SetActive(self, active):
+    """Enable/disable processing of tasks.
+
+    This is different from L{Quiesce} in the sense that this function just
+    changes an internal flag and doesn't wait for the queue to be empty. Tasks
+    already being processed continue normally, but no new tasks will be
+    started. New tasks can still be added.
+
+    @type active: bool
+    @param active: Whether tasks should be processed
+
+    """
+    self._lock.acquire()
+    try:
+      self._active = active
+
+      if active:
+        # Tell all workers to continue processing
+        self._pool_to_worker.notifyAll()
+    finally:
+      self._lock.release()
+
   def _WaitForTaskUnlocked(self, worker):
     """Waits for a task for a worker.
 
@@ -351,21 +374,22 @@ class WorkerPool(object):
       return _TERMINATE
 
     # We only wait if there's no task for us.
-    if not self._tasks:
+    if not (self._active and self._tasks):
       logging.debug("Waiting for tasks")
 
-      # wait() releases the lock and sleeps until notified
-      self._pool_to_worker.wait()
+      while True:
+        # wait() releases the lock and sleeps until notified
+        self._pool_to_worker.wait()
 
-      logging.debug("Notified while waiting")
+        logging.debug("Notified while waiting")
 
-      # Were we woken up in order to terminate?
-      if self._ShouldWorkerTerminateUnlocked(worker):
-        return _TERMINATE
+        # Were we woken up in order to terminate?
+        if self._ShouldWorkerTerminateUnlocked(worker):
+          return _TERMINATE
 
-      if not self._tasks:
-        # Spurious notification, ignore
-        return None
+        # Just loop if pool is not processing tasks at this time
+        if self._active and self._tasks:
+          break
 
     # Get task from queue and tell pool about it
     try:
@@ -388,6 +412,16 @@ class WorkerPool(object):
         return True
     return False
 
+  def HasRunningTasks(self):
+    """Checks whether there's at least one task running.
+
+    """
+    self._lock.acquire()
+    try:
+      return self._HasRunningTasksUnlocked()
+    finally:
+      self._lock.release()
+
   def Quiesce(self):
     """Waits until the task queue is empty.
 
index 2ded5ba..a774f05 100644 (file)
@@ -25,12 +25,13 @@ daemon), **ganeti-masterd**(8) (master daemon), **ganeti-rapi**(8)
 
 Ganeti htools: **htools**(1) (generic binary), **hbal**(1) (cluster
 balancer), **hspace**(1) (capacity calculation), **hail**(1) (IAllocator
-plugin), **hscan**(1) (data gatherer from remote clusters).
+plugin), **hscan**(1) (data gatherer from remote clusters), **hinfo**(1)
+(cluster information printer).
 
 COPYRIGHT
 ---------
 
-Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 Google
+Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 Google
 Inc. Permission is granted to copy, distribute and/or modify 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
index b435d07..5159277 100644 (file)
@@ -9,7 +9,7 @@ ganeti-masterd - Ganeti master daemon
 Synopsis
 --------
 
-**ganeti-masterd** [-f] [-d] [--no-voting]
+**ganeti-masterd** [-f] [-d] [\--no-voting]
 
 DESCRIPTION
 -----------
index 35ba870..ed96997 100644 (file)
@@ -9,7 +9,7 @@ ganeti-rapi - Ganeti remote API daemon
 Synopsis
 --------
 
-**ganeti-rapi** [-d] [-f] [--no-ssl] [-K *SSL_KEY_FILE*] [-C
+**ganeti-rapi** [-d] [-f] [\--no-ssl] [-K *SSL_KEY_FILE*] [-C
 *SSL_CERT_FILE*]
 
 DESCRIPTION
index 6f2b692..7c61d38 100644 (file)
@@ -68,12 +68,24 @@ execute much later than the watcher submits them.
 FILES
 -----
 
-The command has a state file located at
-``@LOCALSTATEDIR@/lib/ganeti/watcher.data`` (only used on the master)
-and a log file at ``@LOCALSTATEDIR@/log/ganeti/watcher.log``. Removal
-of either file will not affect correct operation; the removal of the
-state file will just cause the restart counters for the instances to
-reset to zero.
+The command has a set of state files (one per group) located at
+``@LOCALSTATEDIR@/lib/ganeti/watcher.GROUP-UUID.data`` (only used on the
+master) and a log file at
+``@LOCALSTATEDIR@/log/ganeti/watcher.log``. Removal of either file(s)
+will not affect correct operation; the removal of the state file will
+just cause the restart counters for the instances to reset to zero, and
+mark nodes as freshly rebooted (so for example DRBD minors will be
+re-activated).
+
+In some cases, it's even desirable to reset the watcher state, for
+example after maintenance actions, or when you want to simulate the
+reboot of all nodes, so in this case, you can remove all state files::
+
+    rm -f @LOCALSTATEDIR@/lib/ganeti/watcher.*.data
+    rm -f @LOCALSTATEDIR@/lib/ganeti/watcher.*.instance-status
+    rm -f @LOCALSTATEDIR@/lib/ganeti/instance-status
+
+And then re-run the watcher.
 
 .. vim: set textwidth=72 :
 .. Local Variables:
index 3daf8b5..d31adfc 100644 (file)
@@ -99,8 +99,8 @@ vm_capable
 Node Parameters
 ~~~~~~~~~~~~~~~
 
-These parameters are node specific and can be preseeded on node-group
-and cluster level.
+The ``ndparams`` refer to node parameters. These can be set as defaults
+on cluster and node group levels, but they take effect for nodes only.
 
 Currently we support the following node parameters:
 
@@ -109,6 +109,69 @@ oob_program
     the `Ganeti Node OOB Management Framework <design-oob.rst>`_ design
     document.
 
+spindle_count
+    This should reflect the I/O performance of local attached storage
+    (e.g. for "file", "plain" and "drbd" disk templates). It doesn't
+    have to match the actual spindle count of (any eventual) mechanical
+    hard-drives, its meaning is site-local and just the relative values
+    matter.
+
+
+Hypervisor State Parameters
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Using ``--hypervisor-state`` you can set hypervisor specific states as
+pointed out in ``Ganeti Resource Model <design-resource-model.rst>``.
+
+The format is: ``hypervisor:option=value``.
+
+Currently we support the following hypervisor state values:
+
+mem_total
+  Total node memory, as discovered by this hypervisor
+mem_node
+  Memory used by, or reserved for, the node itself; note that some
+  hypervisors can report this in an authoritative way, other not
+mem_hv
+  Memory used either by the hypervisor itself or lost due to instance
+  allocation rounding; usually this cannot be precisely computed, but
+  only roughly estimated
+cpu_total
+  Total node cpu (core) count; usually this can be discovered
+  automatically
+cpu_node
+  Number of cores reserved for the node itself; this can either be
+  discovered or set manually. Only used for estimating how many VCPUs
+  are left for instances
+
+Note that currently this option is unused by Ganeti; values will be
+recorded but will not influence the Ganeti operation.
+
+
+Disk State Parameters
+~~~~~~~~~~~~~~~~~~~~~
+
+Using ``--disk-state`` you can set disk specific states as pointed out
+in ``Ganeti Resource Model <design-resource-model.rst>``.
+
+The format is: ``storage_type/identifier:option=value``. Where we
+currently just support ``lvm`` as storage type. The identifier in this
+case is the LVM volume group. By default this is ``xenvg``.
+
+Currently we support the following hypervisor state values:
+
+disk_total
+  Total disk size (usually discovered automatically)
+disk_reserved
+  Reserved disk size; this is a lower limit on the free space, if such a
+  limit is desired
+disk_overhead
+  Disk that is expected to be used by other volumes (set via
+  ``reserved_lvs``); usually should be zero
+
+Note that currently this option is unused by Ganeti; values will be
+recorded but will not influence the Ganeti operation.
+
 
 Cluster configuration
 ~~~~~~~~~~~~~~~~~~~~~
@@ -168,7 +231,8 @@ Many Ganeti commands provide the following options. The
 availability for a certain command can be checked by calling the
 command using the ``--help`` option.
 
-**gnt-...** *command* [--dry-run] [--priority {low | normal | high}]
+| **gnt-...** *command* [\--dry-run] [\--priority {low | normal | high}]
+| [\--submit]
 
 The ``--dry-run`` option can be used to check whether an operation
 would succeed.
@@ -176,6 +240,25 @@ would succeed.
 The option ``--priority`` sets the priority for opcodes submitted
 by the command.
 
+The ``--submit`` option is used to send the job to the master daemon but
+not wait for its completion. The job ID will be shown so that it can be
+examined using **gnt-job info**.
+
+Defaults
+~~~~~~~~
+
+For certain commands you can use environment variables to provide
+default command line arguments. Just assign the arguments as a string to
+the corresponding environment variable. The format of that variable
+name is **binary**_*command*. **binary** is the name of the ``gnt-*``
+script all upper case and dashes replaced by underscores, and *command*
+is the command invoked on that script.
+
+Currently supported commands are ``gnt-node list``, ``gnt-group list``
+and ``gnt-instance list``. So you can configure default command line
+flags by setting ``GNT_NODE_LIST``, ``GNT_GROUP_LIST`` and
+``GNT_INSTANCE_LIST``.
+
 Field formatting
 ----------------
 
@@ -233,6 +316,30 @@ Perl. The language is not generic. Each condition must consist of a field name
 and a value (except for boolean checks), a field can not be compared to another
 field. Keywords are case-sensitive.
 
+Examples (see below for syntax details):
+
+- List webservers::
+
+    gnt-instance list --filter 'name =* "web*.example.com"'
+
+- List instances with three or six virtual CPUs and whose primary
+  nodes reside in groups starting with the string "rack"::
+
+    gnt-instance list --filter
+      '(be/vcpus == 3 or be/vcpus == 6) and pnode.group =~ m/^rack/'
+
+- Nodes hosting primary instances::
+
+    gnt-node list --filter 'pinst_cnt != 0'
+
+- Nodes which aren't master candidates::
+
+    gnt-node list --filter 'not master_candidate'
+
+- Short version for globbing patterns::
+
+    gnt-instance list '*.site1' '*.site2'
+
 Syntax in pseudo-BNF::
 
   <quoted-string> ::= /* String quoted with single or double quotes,
@@ -255,7 +362,7 @@ Syntax in pseudo-BNF::
 
   <condition> ::=
     { /* Value comparison */
-      <field> { == | != } <value>
+      <field> { == | != | < | <= | >= | > } <value>
 
       /* Collection membership */
       | <value> [ not ] in <field>
@@ -282,6 +389,14 @@ Operators:
   Equality
 *!=*
   Inequality
+*<*
+  Less than
+*<=*
+  Less than or equal
+*>*
+  Greater than
+*>=*
+  Greater than or equal
 *=~*
   Pattern match using regular expression
 *!~*
@@ -293,9 +408,6 @@ Operators:
 *in*, *not in*
   Collection membership and negation
 
-As a shortcut globbing patterns can be specified as names, e.g.
-``gnt-instance list '*.site1' '*.site2'``.
-
 
 Common daemon functionality
 ---------------------------
index 41889fd..da56299 100644 (file)
@@ -24,8 +24,9 @@ COMMANDS
 EXPORT
 ~~~~~~
 
-**export** {-n *node*} [--shutdown-timeout=*N*] [--noshutdown]
-[--remove-instance] [--ignore-remove-failures] {*instance*}
+| **export** {-n *node*} [\--shutdown-timeout=*N*] [\--noshutdown]
+| [\--remove-instance] [\--ignore-remove-failures] [\--submit]
+| {*instance*}
 
 Exports an instance to the target node. All the instance data and
 its configuration will be exported under the
@@ -53,6 +54,9 @@ execution (and will be stored in the job log). It is recommended
 that for any non-zero exit code, the backup is considered invalid,
 and retried.
 
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
+
 Example::
 
     # gnt-backup export -n node1.example.com instance3.example.com
@@ -62,14 +66,16 @@ IMPORT
 ~~~~~~
 
 | **import**
-| {-n *node[:secondary-node]* | --iallocator *name*}
-| [--disk *N*:size=*VAL* [,vg=*VG*], [,mode=*ro|rw*]...]
-| [--net *N* [:options...] | --no-nics]
+| {-n *node[:secondary-node]* | \--iallocator *name*}
+| [\--disk *N*:size=*VAL* [,vg=*VG*], [,mode=*ro|rw*]...]
+| [\--net *N* [:options...] | \--no-nics]
 | [-B *BEPARAMS*]
 | [-H *HYPERVISOR* [: option=*value*... ]]
-| [--src-node=*source-node*] [--src-dir=*source-dir*]
+| [\--src-node=*source-node*] [\--src-dir=*source-dir*]
 | [-t [diskless | plain | drbd | file]]
-| [--identify-defaults]
+| [\--identify-defaults]
+| [\--ignore-ipolicy]
+| [\--submit]
 | {*instance*}
 
 Imports a new instance from an export residing on *source-node* in
@@ -129,16 +135,20 @@ Of these ``mode`` and ``link`` are nic parameters, and inherit their
 default at cluster level.
 
 If no network is desired for the instance, you should create a single
-empty NIC and delete it afterwards via **gnt-instance modify --net
+empty NIC and delete it afterwards via **gnt-instance modify \--net
 delete**.
 
 The ``-B`` option specifies the backend parameters for the
 instance. If no such parameters are specified, the values are
 inherited from the export. Possible parameters are:
 
-memory
-    the memory size of the instance; as usual, suffixes can be used to
-    denote the unit, otherwise the value is taken in mebibites
+maxmem
+    the maximum memory size of the instance; as usual, suffixes can be
+    used to denote the unit, otherwise the value is taken in mebibytes
+
+minmem
+    the minimum memory size of the instance; as usual, suffixes can be
+    used to denote the unit, otherwise the value is taken in mebibytes
 
 vcpus
     the number of VCPUs to assign to the instance (if this value makes
@@ -148,6 +158,11 @@ auto_balance
     whether the instance is considered in the N+1 cluster checks
     (enough redundancy in the cluster to survive a node failure)
 
+always\_failover
+    ``True`` or ``False``, whether the instance must be failed over
+    (shut down and rebooted) always or it may be migrated (briefly
+    suspended)
+
 
 The ``-t`` options specifies the disk layout type for the instance.
 If not passed, the configuration of the original instance is used.
@@ -182,6 +197,9 @@ template and specifies the remote node.
 The ``--src-dir`` option allows importing instances from a directory
 below ``@CUSTOM_EXPORT_DIR@``.
 
+If ``--ignore-ipolicy`` is given any instance policy violations occuring
+during this operation are ignored.
+
 Since many of the parameters are by default read from the exported
 instance information and used as such, the new instance will have
 all parameters explicitly specified, the opposite of a newly added
@@ -192,6 +210,9 @@ value matches the current cluster default and mark it as such
 affect the hypervisor, backend and NIC parameters, both read from
 the export file and passed in via the command line.
 
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
+
 Example for identical instance import::
 
     # gnt-backup import -n node1.example.com instance3.example.com
@@ -207,16 +228,40 @@ Explicit configuration example::
 LIST
 ~~~~
 
-**list** [--node=*NODE*]
+| **list** [\--node=*NODE*] [\--no-headers] [\--separator=*SEPARATOR*]
+| [-o *[+]FIELD,...*]
 
 Lists the exports currently available in the default directory in
 all the nodes of the current cluster, or optionally only a subset
 of them specified using the ``--node`` option (which can be used
 multiple times)
 
+The ``--no-headers`` option will skip the initial header line. The
+``--separator`` option takes an argument which denotes what will be
+used between the output fields. Both these options are to help
+scripting.
+
+The ``-o`` option takes a comma-separated list of output fields.
+The available fields and their meaning are:
+
+@QUERY_FIELDS_EXPORT@
+
+If the value of the option starts with the character ``+``, the new
+fields will be added to the default list. This allows one to quickly
+see the default list plus a few other fields, instead of retyping
+the entire list of fields.
+
 Example::
 
-    # gnt-backup list --nodes node1 --nodes node2
+    # gnt-backup list --node node1 --node node2
+
+
+LIST-FIELDS
+~~~~~~~~~~~
+
+**list-fields** [field...]
+
+Lists available fields for exports.
 
 
 REMOVE
index c821cd7..8082d14 100644 (file)
@@ -20,10 +20,17 @@ Ganeti system.
 COMMANDS
 --------
 
+ACTIVATE-MASTER-IP
+~~~~~~~~~~~~~~~~~~
+
+**activate-master-ip**
+
+Activates the master IP on the master node.
+
 ADD-TAGS
 ~~~~~~~~
 
-**add-tags** [--from *file*] {*tag*...}
+**add-tags** [\--from *file*] {*tag*...}
 
 Add tags to the cluster. If any of the tags contains invalid
 characters, the entire operation will abort.
@@ -37,7 +44,7 @@ interpreted as stdin.
 COMMAND
 ~~~~~~~
 
-**command** [-n *node*] [-g *group*] {*command*}
+**command** [-n *node*] [-g *group*] [-M] {*command*}
 
 Executes a command on all nodes. If the option ``-n`` is not given,
 the command will be executed on all nodes, otherwise it will be
@@ -51,6 +58,9 @@ group, e.g.::
 
     # gnt-cluster command -g default date
 
+The ``-M`` option can be used to prepend the node name to all output
+lines.
+
 The command is executed serially on the selected nodes. If the
 master node is present in the list, the command will be executed
 last on the master. Regarding the other nodes, the execution order
@@ -72,7 +82,7 @@ and the command which will be executed will be ``ls -l /etc``.
 COPYFILE
 ~~~~~~~~
 
-| **copyfile** [--use-replication-network] [-n *node*] [-g *group*]
+| **copyfile** [\--use-replication-network] [-n *node*] [-g *group*]
 | {*file*}
 
 Copies a file to all or to some nodes. The argument specifies the
@@ -89,21 +99,33 @@ primary/secondary IPs are different). Example::
 This will copy the file /tmp/test from the current node to the two
 named nodes.
 
+DEACTIVATE-MASTER-IP
+~~~~~~~~~~~~~~~~~~~~
+
+**deactivate-master-ip** [\--yes]
+
+Deactivates the master IP on the master node.
+
+This should be run only locally or on a connection to the node ip
+directly, as a connection to the master ip will be broken by this
+operation. Because of this risk it will require user confirmation
+unless the ``--yes`` option is passed.
+
 DESTROY
 ~~~~~~~
 
-**destroy** {--yes-do-it}
+**destroy** {\--yes-do-it}
 
 Remove all configuration files related to the cluster, so that a
 **gnt-cluster init** can be done again afterwards.
 
 Since this is a dangerous command, you are required to pass the
-argument *--yes-do-it.*
+argument *\--yes-do-it.*
 
 EPO
 ~~~
 
-**epo** [--on] [--groups|--all] [--power-delay] *arguments*
+**epo** [\--on] [\--groups|\--all] [\--power-delay] *arguments*
 
 Performs an emergency power-off on nodes given as arguments. If
 ``--groups`` is given, arguments are node groups. If ``--all`` is
@@ -131,7 +153,7 @@ Displays the current master node.
 INFO
 ~~~~
 
-**info** [--roman]
+**info** [\--roman]
 
 Shows runtime cluster information: cluster name, architecture (32
 or 64 bit), master node, node list and instance list.
@@ -144,25 +166,36 @@ INIT
 ~~~~
 
 | **init**
-| [{-s|--secondary-ip} *secondary\_ip*]
-| [--vg-name *vg-name*]
-| [--master-netdev *interface-name*]
-| [{-m|--mac-prefix} *mac-prefix*]
-| [--no-lvm-storage]
-| [--no-etc-hosts]
-| [--no-ssh-init]
-| [--file-storage-dir *dir*]
-| [--enabled-hypervisors *hypervisors*]
-| [{-H|--hypervisor-parameters} *hypervisor*:*hv-param*=*value*[,*hv-param*=*value*...]]
-| [{-B|--backend-parameters} *be-param*=*value* [,*be-param*=*value*...]]
-| [{-N|--nic-parameters} *nic-param*=*value* [,*nic-param*=*value*...]]
-| [--maintain-node-health {yes \| no}]
-| [--uid-pool *user-id pool definition*]
-| [{-I|--default-iallocator} *default instance allocator*]
-| [--primary-ip-version *version*]
-| [--prealloc-wipe-disks {yes \| no}]
-| [--node-parameters *ndparams*]
-| [{-C|--candidate-pool-size} *candidate\_pool\_size*]
+| [{-s|\--secondary-ip} *secondary\_ip*]
+| [\--vg-name *vg-name*]
+| [\--master-netdev *interface-name*]
+| [\--master-netmask *netmask*]
+| [\--use-external-mip-script {yes \| no}]
+| [{-m|\--mac-prefix} *mac-prefix*]
+| [\--no-lvm-storage]
+| [\--no-etc-hosts]
+| [\--no-ssh-init]
+| [\--file-storage-dir *dir*]
+| [\--enabled-hypervisors *hypervisors*]
+| [{-H|\--hypervisor-parameters} *hypervisor*:*hv-param*=*value*[,*hv-param*=*value*...]]
+| [{-B|\--backend-parameters} *be-param*=*value*[,*be-param*=*value*...]]
+| [{-N|\--nic-parameters} *nic-param*=*value*[,*nic-param*=*value*...]]
+| [{-D|\--disk-parameters} *disk-template*:*disk-param*=*value*[,*disk-param*=*value*...]]
+| [\--maintain-node-health {yes \| no}]
+| [\--uid-pool *user-id pool definition*]
+| [{-I|\--default-iallocator} *default instance allocator*]
+| [\--primary-ip-version *version*]
+| [\--prealloc-wipe-disks {yes \| no}]
+| [\--node-parameters *ndparams*]
+| [{-C|\--candidate-pool-size} *candidate\_pool\_size*]
+| [\--specs-cpu-count *spec-param*=*value* [,*spec-param*=*value*...]]
+| [\--specs-disk-count *spec-param*=*value* [,*spec-param*=*value*...]]
+| [\--specs-disk-size *spec-param*=*value* [,*spec-param*=*value*...]]
+| [\--specs-mem-size *spec-param*=*value* [,*spec-param*=*value*...]]
+| [\--specs-nic-count *spec-param*=*value* [,*spec-param*=*value*...]]
+| [\--ipol-disk-templates *template* [,*template*...]]
+| [\--disk-state *diskstate*]
+| [\--hypervisor-state *hvstate*]
 | {*clustername*}
 
 This commands is only run once initially on the first node of the
@@ -203,6 +236,17 @@ interface on which the master will activate its IP address. It's
 important that all nodes have this interface because you'll need it
 for a master failover.
 
+The ``--master-netmask`` option allows to specify a netmask for the
+master IP. The netmask must be specified as an integer, and will be
+interpreted as a CIDR netmask. The default value is 32 for an IPv4
+address and 128 for an IPv6 address.
+
+The ``--use-external-mip-script`` option allows to specify whether to
+use an user-supplied master IP address setup script, whose location is
+``@SYSCONFDIR@/ganeti/scripts/master-ip-setup``. If the option value is
+set to False, the default script (located at
+``@PKGLIBDIR@/tools/master-ip-setup``) will be executed.
+
 The ``-m (--mac-prefix)`` option will let you specify a three byte
 prefix under which the virtual MAC addresses of your instances will be
 generated. The prefix must be specified in the format ``XX:XX:XX`` and
@@ -289,16 +333,26 @@ vcpus
     Number of VCPUs to set for an instance by default, must be an
     integer, will be set to 1 if no specified.
 
-memory
-    Amount of memory to allocate for an instance by default, can be
-    either an integer or an integer followed by a unit (M for mebibytes
-    and G for gibibytes are supported), will be set to 128M if not
-    specified.
+maxmem
+    Maximum amount of memory to allocate for an instance by default, can
+    be either an integer or an integer followed by a unit (M for
+    mebibytes and G for gibibytes are supported), will be set to 128M if
+    not specified.
+
+minmem
+    Minimum amount of memory to allocate for an instance by default, can
+    be either an integer or an integer followed by a unit (M for
+    mebibytes and G for gibibytes are supported), will be set to 128M if
+    not specified.
 
 auto\_balance
     Value of the auto\_balance flag for instances to use by default,
     will be set to true if not specified.
 
+always\_failover
+    Default value for the ``always\_failover`` flag for instances; if
+    not set, ``False`` is used.
+
 
 The ``-N (--nic-parameters)`` option allows you to set the default nic
 parameters for the cluster. The parameter format is a comma-separated
@@ -314,11 +368,96 @@ link
     network script it is interpreted as a routing table number or
     name.
 
+The ``-D (--disk-parameters)`` option allows you to set the default disk
+template parameters at cluster level. The format used for this option is
+similar to the one use by the  ``-H`` option: the disk template name
+must be specified first, followed by a colon and by a comma-separated
+list of key-value pairs. These parameters can only be specified at
+cluster and node group level; the cluster-level parameter are inherited
+by the node group at the moment of its creation, and can be further
+modified at node group level using the **gnt-group**(8) command.
+
+The following is the list of disk parameters available for the **drbd**
+template, with measurement units specified in square brackets at the end
+of the description (when applicable):
+
+resync-rate
+    Static re-synchronization rate. [KiB/s]
+
+data-stripes
+    Number of stripes to use for data LVs.
+
+meta-stripes
+    Number of stripes to use for meta LVs.
+
+disk-barriers
+    What kind of barriers to **disable** for disks. It can either assume
+    the value "n", meaning no barrier disabled, or a non-empty string
+    containing a subset of the characters "bfd". "b" means disable disk
+    barriers, "f" means disable disk flushes, "d" disables disk drains.
+
+meta-barriers
+    Boolean value indicating whether the meta barriers should be
+    disabled (True) or not (False).
+
+metavg
+    String containing the name of the default LVM volume group for DRBD
+    metadata. By default, it is set to ``xenvg``. It can be overridden
+    during the instance creation process by using the ``metavg`` key of
+    the ``--disk`` parameter.
+
+disk-custom
+    String containing additional parameters to be appended to the
+    arguments list of ``drbdsetup disk``.
+
+net-custom
+    String containing additional parameters to be appended to the
+    arguments list of ``drbdsetup net``.
+
+dynamic-resync
+    Boolean indicating whether to use the dynamic resync speed
+    controller or not. If enabled, c-plan-ahead must be non-zero and all
+    the c-* parameters will be used by DRBD. Otherwise, the value of
+    resync-rate will be used as a static resync speed.
+
+c-plan-ahead
+    Agility factor of the dynamic resync speed controller. (the higher,
+    the slower the algorithm will adapt the resync speed). A value of 0
+    (that is the default) disables the controller. [ds]
+
+c-fill-target
+    Maximum amount of in-flight resync data for the dynamic resync speed
+    controller. [sectors]
+
+c-delay-target
+    Maximum estimated peer response latency for the dynamic resync speed
+    controller. [ds]
+
+c-min-rate
+    Minimum resync speed for the dynamic resync speed controller. [KiB/s]
+
+c-max-rate
+    Upper bound on resync speed for the dynamic resync speed controller.
+    [KiB/s]
+
+List of parameters available for the **plain** template:
+
+stripes
+    Number of stripes to use for new LVs.
+
+List of parameters available for the **rbd** template:
+
+pool
+    The RADOS cluster pool, inside which all rbd volumes will reside.
+    When a new RADOS cluster is deployed, the default pool to put rbd
+    volumes (Images in RADOS terminology) is 'rbd'.
+
 The option ``--maintain-node-health`` allows one to enable/disable
 automatic maintenance actions on nodes. Currently these include
 automatic shutdown of instances and deactivation of DRBD devices on
 offline nodes; in the future it might be extended to automatic
-removal of unknown LVM volumes, etc.
+removal of unknown LVM volumes, etc. Note that this option is only
+useful if the use of ``ganeti-confd`` was enabled at compilation.
 
 The ``--uid-pool`` option initializes the user-id pool. The
 *user-id pool definition* can contain a list of user-ids and/or a
@@ -355,6 +494,26 @@ The ``-C (--candidate-pool-size)`` option specifies the
 that the master will try to keep as master\_candidates. For more
 details about this role and other node roles, see the ganeti(7).
 
+The ``--specs-...`` and ``--ipol-disk-templates`` options specify
+instance policy on the cluster. For the ``--specs-...`` options, each
+option can have three values: ``min``, ``max`` and ``std``, which can
+also be modified on group level (except for ``std``, which is defined
+once for the entire cluster). Please note, that ``std`` values are not
+the same as defaults set by ``--beparams``, but they are used for the
+capacity calculations. The ``--ipol-disk-templates`` option takes a
+comma-separated list of disk templates.
+
+- ``--specs-cpu-count`` limits the number of VCPUs that can be used by an
+  instance.
+- ``--specs-disk-count`` limits the number of disks
+- ``--specs-disk-size`` limits the disk size for every disk used
+- ``--specs-mem-size`` limits the amount of memory available
+- ``--specs-nic-count`` sets limits on the number of NICs used
+- ``--ipol-disk-templates`` limits the allowed disk templates
+
+For details about how to use ``--hypervisor-state`` and ``--disk-state``
+have a look at **ganeti**(7).
+
 LIST-TAGS
 ~~~~~~~~~
 
@@ -365,7 +524,7 @@ List the tags of the cluster.
 MASTER-FAILOVER
 ~~~~~~~~~~~~~~~
 
-**master-failover** [--no-voting]
+**master-failover** [\--no-voting]
 
 Failover the master role to the current node.
 
@@ -395,32 +554,49 @@ be 1.
 MODIFY
 ~~~~~~
 
-| **modify**
-| [--vg-name *vg-name*]
-| [--no-lvm-storage]
-| [--enabled-hypervisors *hypervisors*]
-| [{-H|--hypervisor-parameters} *hypervisor*:*hv-param*=*value*[,*hv-param*=*value*...]]
-| [{-B|--backend-parameters} *be-param*=*value* [,*be-param*=*value*...]]
-| [{-N|--nic-parameters} *nic-param*=*value* [,*nic-param*=*value*...]]
-| [--uid-pool *user-id pool definition*]
-| [--add-uids *user-id pool definition*]
-| [--remove-uids *user-id pool definition*]
-| [{-C|--candidate-pool-size} *candidate\_pool\_size*]
-| [--maintain-node-health {yes \| no}]
-| [--prealloc-wipe-disks {yes \| no}]
-| [{-I|--default-iallocator} *default instance allocator*]
-| [--reserved-lvs=*NAMES*]
-| [--node-parameters *ndparams*]
-| [--master-netdev *interface-name*]
+| **modify** [\--submit]
+| [\--vg-name *vg-name*]
+| [\--no-lvm-storage]
+| [\--enabled-hypervisors *hypervisors*]
+| [{-H|\--hypervisor-parameters} *hypervisor*:*hv-param*=*value*[,*hv-param*=*value*...]]
+| [{-B|\--backend-parameters} *be-param*=*value*[,*be-param*=*value*...]]
+| [{-N|\--nic-parameters} *nic-param*=*value*[,*nic-param*=*value*...]]
+| [{-D|\--disk-parameters} *disk-template*:*disk-param*=*value*[,*disk-param*=*value*...]]
+| [\--uid-pool *user-id pool definition*]
+| [\--add-uids *user-id pool definition*]
+| [\--remove-uids *user-id pool definition*]
+| [{-C|\--candidate-pool-size} *candidate\_pool\_size*]
+| [\--maintain-node-health {yes \| no}]
+| [\--prealloc-wipe-disks {yes \| no}]
+| [{-I|\--default-iallocator} *default instance allocator*]
+| [\--reserved-lvs=*NAMES*]
+| [\--node-parameters *ndparams*]
+| [\--master-netdev *interface-name*]
+| [\--master-netmask *netmask*]
+| [\--use-external-mip-script {yes \| no}]
+| [\--hypervisor-state *hvstate*]
+| [\--disk-state *diskstate*]
+| [\--specs-cpu-count *spec-param*=*value* [,*spec-param*=*value*...]]
+| [\--specs-disk-count *spec-param*=*value* [,*spec-param*=*value*...]]
+| [\--specs-disk-size *spec-param*=*value* [,*spec-param*=*value*...]]
+| [\--specs-mem-size *spec-param*=*value* [,*spec-param*=*value*...]]
+| [\--specs-nic-count *spec-param*=*value* [,*spec-param*=*value*...]]
+| [\--ipol-disk-templates *template* [,*template*...]]
+
 
 Modify the options for the cluster.
 
 The ``--vg-name``, ``--no-lvm-storarge``, ``--enabled-hypervisors``,
 ``-H (--hypervisor-parameters)``, ``-B (--backend-parameters)``,
-``--nic-parameters``, ``-C (--candidate-pool-size)``,
-``--maintain-node-health``, ``--prealloc-wipe-disks``, ``--uid-pool``,
-``--node-parameters``, ``--master-netdev`` options are described in
-the **init** command.
+``-D (--disk-parameters)``, ``--nic-parameters``, ``-C
+(--candidate-pool-size)``, ``--maintain-node-health``,
+``--prealloc-wipe-disks``, ``--uid-pool``, ``--node-parameters``,
+``--master-netdev``, ``--master-netmask`` and
+``--use-external-mip-script`` options are described in the **init**
+command.
+
+The ``--hypervisor-state`` and ``--disk-state`` options are described in
+detail in **ganeti(7)**.
 
 The ``--add-uids`` and ``--remove-uids`` options can be used to
 modify the user-id pool by adding/removing a list of user-ids or
@@ -442,6 +618,12 @@ The ``-I (--default-iallocator)`` is described in the **init**
 command. To clear the default iallocator, just pass an empty string
 ('').
 
+The ``--specs-...`` and ``--ipol-disk-templates`` options are described
+in the **init** command.
+
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
+
 QUEUE
 ~~~~~
 
@@ -472,24 +654,23 @@ The ``continue`` option will let the watcher continue.
 
 The ``info`` option shows whether the watcher is currently paused.
 
-redist-conf
+REDIST-CONF
 ~~~~~~~~~~~
 
-**redist-conf** [--submit]
+**redist-conf** [\--submit]
 
 This command forces a full push of configuration files from the
 master node to the other nodes in the cluster. This is normally not
 needed, but can be run if the **verify** complains about
 configuration mismatches.
 
-The ``--submit`` option is used to send the job to the master
-daemon but not wait for its completion. The job ID will be shown so
-that it can be examined via **gnt-job info**.
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
 
 REMOVE-TAGS
 ~~~~~~~~~~~
 
-**remove-tags** [--from *file*] {*tag*...}
+**remove-tags** [\--from *file*] {*tag*...}
 
 Remove tags from the cluster. If any of the tags are not existing
 on the cluster, the entire operation will abort.
@@ -518,9 +699,11 @@ RENEW-CRYPTO
 ~~~~~~~~~~~~
 
 | **renew-crypto** [-f]
-| [--new-cluster-certificate] [--new-confd-hmac-key]
-| [--new-rapi-certificate] [--rapi-certificate *rapi-cert*]
-| [--new-cluster-domain-secret] [--cluster-domain-secret *filename*]
+| [\--new-cluster-certificate] [\--new-confd-hmac-key]
+| [\--new-rapi-certificate] [\--rapi-certificate *rapi-cert*]
+| [\--new-spice-certificate | \--spice-certificate *spice-cert*
+| \--spice-ca-certificate *spice-ca-cert*]
+| [\--new-cluster-domain-secret] [\--cluster-domain-secret *filename*]
 
 This command will stop all Ganeti daemons in the cluster and start
 them again once the new certificates and keys are replicated. The
@@ -533,6 +716,12 @@ ganeti-rapi(8)) specify ``--new-rapi-certificate``. If you want to
 use your own certificate, e.g. one signed by a certificate
 authority (CA), pass its filename to ``--rapi-certificate``.
 
+To generate a new self-signed SPICE certificate, used by SPICE
+connections to the KVM hypervisor, specify the
+``--new-spice-certificate`` option. If you want to provide a
+certificate, pass its filename to ``--spice-certificate`` and pass the
+signing CA certificate to ``--spice-ca-certificate``.
+
 ``--new-cluster-domain-secret`` generates a new, random cluster
 domain secret. ``--cluster-domain-secret`` reads the secret from a
 file. The cluster domain secret is used to sign information
@@ -551,7 +740,7 @@ arguments are given, all instances will be checked.
 
 Note that only active disks can be checked by this command; in case
 a disk cannot be activated it's advised to use
-**gnt-instance activate-disks --ignore-size ...** to force
+**gnt-instance activate-disks \--ignore-size ...** to force
 activation without regard to the current size.
 
 When the all disk sizes are consistent, the command will return no
@@ -585,7 +774,9 @@ node will be listed as /nodes/*name*, and an instance as
 VERIFY
 ~~~~~~
 
-**verify** [--no-nplus1-mem] [--node-group *nodegroup*]
+| **verify** [\--no-nplus1-mem] [\--node-group *nodegroup*]
+| [\--error-codes] [{-I|\--ignore-errors} *errorcode*]
+| [{-I|\--ignore-errors} *errorcode*...]
 
 Verify correctness of cluster configuration. This is safe with
 respect to running instances, and incurs no downtime of the
@@ -600,6 +791,39 @@ instances that live in the named group. This will not verify global
 settings, but will allow to perform verification of a group while other
 operations are ongoing in other groups.
 
+The ``--error-codes`` option outputs each error in the following
+parseable format: *ftype*:*ecode*:*edomain*:*name*:*msg*.
+These fields have the following meaning:
+
+ftype
+    Failure type. Can be *WARNING* or *ERROR*.
+
+ecode
+    Error code of the failure. See below for a list of error codes.
+
+edomain
+    Can be *cluster*, *node* or *instance*.
+
+name
+    Contains the name of the item that is affected from the failure.
+
+msg
+    Contains a descriptive error message about the error
+
+``gnt-cluster verify`` will have a non-zero exit code if at least one of
+the failures that are found are of type *ERROR*.
+
+The ``--ignore-errors`` option can be used to change this behaviour,
+because it demotes the error represented by the error code received as a
+parameter to a warning. The option must be repeated for each error that
+should be ignored (e.g.: ``-I ENODEVERSION -I ENODEORPHANLV``). The
+``--error-codes`` option can be used to determine the error code of a
+given error.
+
+List of error codes:
+
+@CONSTANTS_ECODES@
+
 VERIFY-DISKS
 ~~~~~~~~~~~~
 
index 7989631..8ac0979 100644 (file)
@@ -22,10 +22,10 @@ COMMANDS
 IALLOCATOR
 ~~~~~~~~~~
 
-**iallocator** [--debug] [--dir *DIRECTION*] {--algorithm
-*ALLOCATOR* } [--mode *MODE*] [--mem *MEMORY*] [--disks *DISKS*]
-[--disk-template *TEMPLATE*] [--nics *NICS*] [--os-type *OS*]
-[--vcpus *VCPUS*] [--tags *TAGS*] {*instance*}
+**iallocator** [\--debug] [\--dir *DIRECTION*] {\--algorithm
+*ALLOCATOR* } [\--mode *MODE*] [\--mem *MEMORY*] [\--disks *DISKS*]
+[\--disk-template *TEMPLATE*] [\--nics *NICS*] [\--os-type *OS*]
+[\--vcpus *VCPUS*] [\--tags *TAGS*] {*instance*}
 
 Executes a test run of the *iallocator* framework.
 
@@ -46,7 +46,7 @@ this framework, see the HTML or PDF documentation.
 DELAY
 ~~~~~
 
-**delay** [--debug] [--no-master] [-n *NODE*...] {*duration*}
+**delay** [\--debug] [\--no-master] [-n *NODE*...] {*duration*}
 
 Run a test opcode (a sleep) on the master and on selected nodes
 (via an RPC call). This serves no other purpose but to execute a
@@ -62,8 +62,8 @@ number.
 SUBMIT-JOB
 ~~~~~~~~~~
 
-**submit-job** [--verbose] [--timing-stats] [--job-repeat ``N``]
-[--op-repeat ``N``] {opcodes_file...}
+**submit-job** [\--verbose] [\--timing-stats] [\--job-repeat *N*]
+[\--op-repeat *N*] [\--each] {opcodes_file...}
 
 This command builds a list of opcodes from files in JSON format and
 submits a job per file to the master daemon. It can be used to test
@@ -82,6 +82,9 @@ passing the arguments N times) while op-repeat will cause N copies
 of each of the opcodes in the file to be executed (equivalent to
 each file containing N copies of the opcodes).
 
+The ``each`` option allow to submit each job separately (using ``N``
+SubmitJob LUXI requests instead of one SubmitManyJobs request).
+
 TEST-JOBQUEUE
 ~~~~~~~~~~~~~
 
@@ -93,8 +96,8 @@ failed jobs deliberately.
 LOCKS
 ~~~~~
 
-| **locks** [--no-headers] [--separator=*SEPARATOR*] [-v]
-| [-o *[+]FIELD,...*] [--interval=*SECONDS*]
+| **locks** [\--no-headers] [\--separator=*SEPARATOR*] [-v]
+| [-o *[+]FIELD,...*] [\--interval=*SECONDS*]
 
 Shows a list of locks in the master daemon.
 
index 51a2fe5..319f4fc 100644 (file)
@@ -23,9 +23,18 @@ COMMANDS
 ADD
 ~~~
 
-| **add**
-| [--node-parameters=*NDPARAMS*]
-| [--alloc-policy=*POLICY*]
+| **add** [\--submit]
+| [\--node-parameters=*NDPARAMS*]
+| [\--alloc-policy=*POLICY*]
+| [{-D|\--disk-parameters} *disk-template*:*disk-param*=*value*[,*disk-param*=*value*...]]
+| [\--specs-cpu-count *spec-param*=*value* [,*spec-param*=*value*...]]
+| [\--specs-disk-count *spec-param*=*value* [,*spec-param*=*value*...]]
+| [\--specs-disk-size *spec-param*=*value* [,*spec-param*=*value*...]]
+| [\--specs-mem-size *spec-param*=*value* [,*spec-param*=*value*...]]
+| [\--specs-nic-count *spec-param*=*value* [,*spec-param*=*value*...]]
+| [\--ipol-disk-templates *template* [,*template*...]]
+| [\--disk-state *diskstate*]
+| [\--hypervisor-state *hvstate*]
 | {*group*}
 
 Creates a new group with the given name. The node group will be
@@ -33,7 +42,8 @@ initially empty; to add nodes to it, use ``gnt-group assign-nodes``.
 
 The ``--node-parameters`` option allows you to set default node
 parameters for nodes in the group. Please see **ganeti**(7) for more
-information about supported key=value pairs.
+information about supported key=value pairs and their corresponding
+options.
 
 The ``--alloc-policy`` option allows you to set an allocation policy for
 the group at creation time. Possible values are:
@@ -52,11 +62,23 @@ preferred
     (this is the default). Note that prioritization among groups in this
     state will be deferred to the iallocator plugin that's being used.
 
+The ``-D (--disk-parameters)`` option allows you to set the disk
+parameters for the node group; please see the section about
+**gnt-cluster add** in **gnt-cluster**(8) for more information about
+disk parameters
+
+The ``--specs-...`` and ``--ipol-disk-templates`` options specify
+instance policies on the node group, and are documented in the
+**gnt-cluster**(8) man page.
+
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
+
 ASSIGN-NODES
 ~~~~~~~~~~~~
 
 | **assign-nodes**
-| [--force]
+| [\--force] [\--submit]
 | {*group*} {*node*...}
 
 Assigns one or more nodes to the specified group, moving them from their
@@ -68,32 +90,59 @@ instance is an instance with a mirrored disk template, e.g. DRBD, that
 has the primary and secondary nodes in different node groups). You can
 force the operation with ``--force``.
 
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
+
 MODIFY
 ~~~~~~
 
-| **modify**
-| [--node-parameters=*NDPARAMS*]
-| [--alloc-policy=*POLICY*]
+| **modify** [\--submit]
+| [\--node-parameters=*NDPARAMS*]
+| [\--alloc-policy=*POLICY*]
+| [\--hypervisor-state *hvstate*]
+| [{-D|\--disk-parameters} *disk-template*:*disk-param*=*value*[,*disk-param*=*value*...]]
+| [\--disk-state *diskstate*]
+| [\--specs-cpu-count *spec-param*=*value* [,*spec-param*=*value*...]]
+| [\--specs-disk-count *spec-param*=*value* [,*spec-param*=*value*...]]
+| [\--specs-disk-size *spec-param*=*value* [,*spec-param*=*value*...]]
+| [\--specs-mem-size *spec-param*=*value* [,*spec-param*=*value*...]]
+| [\--specs-nic-count *spec-param*=*value* [,*spec-param*=*value*...]]
+| [\--ipol-disk-templates *template* [,*template*...]]
 | {*group*}
 
 Modifies some parameters from the node group.
 
-The ``--node-parameters`` and ``--alloc-policy`` optiosn are documented
-in the **add** command above.
+The ``--node-parameters`` and ``--alloc-policy`` options are documented
+in the **add** command above. ``--hypervisor-state`` as well as
+``--disk-state`` are documented in detail in **ganeti**(7).
+
+The ``--node-parameters``, ``--alloc-policy``, ``-D
+(--disk-parameters)`` options are documented in the **add** command
+above.
+
+The ``--specs-...`` and ``--ipol-disk-templates`` options specify
+instance policies on the node group, and are documented in the
+**gnt-cluster**(8) man page.
+
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
 
 REMOVE
 ~~~~~~
 
-| **remove** {*group*}
+| **remove** [\--submit] {*group*}
 
 Deletes the indicated node group, which must be empty. There must always be at
 least one group, so the last group cannot be removed.
 
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
+
 LIST
 ~~~~
 
-| **list** [--no-headers] [--separator=*SEPARATOR*] [-v]
-| [-o *[+]FIELD,...*] [--filter] [group...]
+| **list** [\--no-headers] [\--separator=*SEPARATOR*] [-v]
+| [-o *[+]FIELD,...*] [\--filter] [group...]
 
 Lists all existing node groups in the cluster.
 
@@ -133,15 +182,18 @@ List available fields for node groups.
 RENAME
 ~~~~~~
 
-| **rename** {*oldname*} {*newname*}
+| **rename** [\--submit] {*oldname*} {*newname*}
 
 Renames a given group from *oldname* to *newname*.
 
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
+
 
 EVACUATE
 ~~~~~~~~
 
-**evacuate** [--iallocator *NAME*] [--to *GROUP*...] {*group*}
+**evacuate** [\--submit] [\--iallocator *NAME*] [\--to *GROUP*...] {*group*}
 
 This command will move all instances out of the given node group.
 Instances are placed in a new group by an iallocator, either given on
@@ -150,6 +202,9 @@ the command line or as a cluster default.
 If no specific destination groups are specified using ``--to``, all
 groups except the evacuated group are considered.
 
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
+
 Example::
 
     # gnt-group evacuate -I hail --to rack4 rack1
@@ -161,7 +216,7 @@ TAGS
 ADD-TAGS
 ^^^^^^^^
 
-**add-tags** [--from *file*] {*groupname*} {*tag*...}
+**add-tags** [\--from *file*] {*groupname*} {*tag*...}
 
 Add tags to the given node group. If any of the tags contains invalid
 characters, the entire operation will abort.
@@ -182,7 +237,7 @@ List the tags of the given node group.
 REMOVE-TAGS
 ^^^^^^^^^^^
 
-**remove-tags** [--from *file*] {*groupname*} {*tag*...}
+**remove-tags** [\--from *file*] {*groupname*} {*tag*...}
 
 Remove tags from the given node group. If any of the tags are not
 existing on the node, the entire operation will abort.
@@ -193,6 +248,14 @@ this case, there is not need to pass tags on the command line (if you
 do, tags from both sources will be removed). A file name of ``-`` will
 be interpreted as stdin.
 
+INFO
+~~~~
+
+**info** [group...]
+
+Shows config information for all (or given) groups.
+
+
 .. vim: set textwidth=72 :
 .. Local Variables:
 .. mode: rst
index bb2d982..2d27bfd 100644 (file)
@@ -27,18 +27,19 @@ ADD
 ^^^
 
 | **add**
-| {-t|--disk-template {diskless | file \| plain \| drbd}}
-| {--disk=*N*: {size=*VAL* \| adopt=*LV*}[,vg=*VG*][,metavg=*VG*][,mode=*ro\|rw*]
-|  \| {-s|--os-size} *SIZE*}
-| [--no-ip-check] [--no-name-check] [--no-start] [--no-install]
-| [--net=*N* [:options...] \| --no-nics]
-| [{-B|--backend-parameters} *BEPARAMS*]
-| [{-H|--hypervisor-parameters} *HYPERVISOR* [: option=*value*... ]]
-| [{-O|--os-parameters} *param*=*value*... ]
-| [--file-storage-dir *dir\_path*] [--file-driver {loop \| blktap}]
-| {{-n|--node} *node[:secondary-node]* \| {-I|--iallocator} *name*}
-| {{-o|--os-type} *os-type*}
-| [--submit]
+| {-t|\--disk-template {diskless | file \| plain \| drbd \| rbd}}
+| {\--disk=*N*: {size=*VAL* \| adopt=*LV*}[,vg=*VG*][,metavg=*VG*][,mode=*ro\|rw*]
+|  \| {-s|\--os-size} *SIZE*}
+| [\--no-ip-check] [\--no-name-check] [\--no-start] [\--no-install]
+| [\--net=*N* [:options...] \| \--no-nics]
+| [{-B|\--backend-parameters} *BEPARAMS*]
+| [{-H|\--hypervisor-parameters} *HYPERVISOR* [: option=*value*... ]]
+| [{-O|\--os-parameters} *param*=*value*... ]
+| [\--file-storage-dir *dir\_path*] [\--file-driver {loop \| blktap}]
+| {{-n|\--node} *node[:secondary-node]* \| {-I|\--iallocator} *name*}
+| {{-o|\--os-type} *os-type*}
+| [\--submit]
+| [\--ignore-ipolicy]
 | {*instance*}
 
 Creates a new instance on the specified host. The *instance* argument
@@ -61,7 +62,7 @@ reuse those volumes (instead of creating new ones) as the
 instance's disks. Ganeti will rename these volumes to the standard
 format, and (without installing the OS) will use them as-is for the
 instance. This allows migrating instances from non-managed mode
-(e.q. plain KVM with LVM) to being managed via Ganeti. Note that
+(e.g. plain KVM with LVM) to being managed via Ganeti. Please note that
 this works only for the \`plain' disk template (see below for
 template details).
 
@@ -128,9 +129,13 @@ The ``-B (--backend-parameters)`` option specifies the backend
 parameters for the instance. If no such parameters are specified, the
 values are inherited from the cluster. Possible parameters are:
 
-memory
-    the memory size of the instance; as usual, suffixes can be used to
-    denote the unit, otherwise the value is taken in mebibites
+maxmem
+    the maximum memory size of the instance; as usual, suffixes can be
+    used to denote the unit, otherwise the value is taken in mebibytes
+
+minmem
+    the minimum memory size of the instance; as usual, suffixes can be
+    used to denote the unit, otherwise the value is taken in mebibytes
 
 vcpus
     the number of VCPUs to assign to the instance (if this value makes
@@ -140,6 +145,16 @@ auto\_balance
     whether the instance is considered in the N+1 cluster checks
     (enough redundancy in the cluster to survive a node failure)
 
+always\_failover
+    ``True`` or ``False``, whether the instance must be failed over
+    (shut down and rebooted) always or it may be migrated (briefly
+    suspended)
+
+Note that before 2.6 Ganeti had a ``memory`` parameter, which was the
+only value of memory an instance could have. With the
+``maxmem``/``minmem`` change Ganeti guarantees that at least the minimum
+memory is always available for an instance, but allows more memory to be
+used (up to the maximum memory) should it be free.
 
 The ``-H (--hypervisor-parameters)`` option specified the hypervisor
 to use for the instance (must be one of the enabled hypervisors on the
@@ -171,7 +186,7 @@ boot\_order
     n
         network boot (PXE)
 
-    The default is not to set an HVM boot order which is interpreted
+    The default is not to set an HVM boot order, which is interpreted
     as 'dc'.
 
     For KVM the boot order is either "floppy", "cdrom", "disk" or
@@ -302,6 +317,76 @@ spice\_ip\_version
     this case, if the ``spice_ip_version`` parameter is not used, the
     default IP version of the cluster will be used.
 
+spice\_password\_file
+    Valid for the KVM hypervisor.
+
+    Specifies a file containing the password that must be used when
+    connecting via the SPICE protocol. If the option is not specified,
+    passwordless connections are allowed.
+
+spice\_image\_compression
+    Valid for the KVM hypervisor.
+
+    Configures the SPICE lossless image compression. Valid values are:
+
+    - auto_glz
+    - auto_lz
+    - quic
+    - glz
+    - lz
+    - off
+
+spice\_jpeg\_wan\_compression
+    Valid for the KVM hypervisor.
+
+    Configures how SPICE should use the jpeg algorithm for lossy image
+    compression on slow links. Valid values are:
+
+    - auto
+    - never
+    - always
+
+spice\_zlib\_glz\_wan\_compression
+    Valid for the KVM hypervisor.
+
+    Configures how SPICE should use the zlib-glz algorithm for lossy image
+    compression on slow links. Valid values are:
+
+    - auto
+    - never
+    - always
+
+spice\_streaming\_video
+    Valid for the KVM hypervisor.
+
+    Configures how SPICE should detect video streams. Valid values are:
+
+    - off
+    - all
+    - filter
+
+spice\_playback\_compression
+    Valid for the KVM hypervisor.
+
+    Configures whether SPICE should compress audio streams or not.
+
+spice\_use\_tls
+    Valid for the KVM hypervisor.
+
+    Specifies that the SPICE server must use TLS to encrypt all the
+    traffic with the client.
+
+spice\_tls\_ciphers
+    Valid for the KVM hypervisor.
+
+    Specifies a list of comma-separated ciphers that SPICE should use
+    for TLS connections. For the format, see man cipher(1).
+
+spice\_use\_vdagent
+    Valid for the KVM hypervisor.
+
+    Enables or disables passing mouse events via SPICE vdagent.
+
 acpi
     Valid for the Xen HVM and KVM hypervisors.
 
@@ -445,14 +530,49 @@ migration\_downtime
     versions >= 0.11.0.
 
 cpu\_mask
-    Valid for the LXC hypervisor.
+    Valid for the Xen, KVM and LXC hypervisors.
 
     The processes belonging to the given instance are only scheduled
     on the specified CPUs.
 
-    The parameter format is a comma-separated list of CPU IDs or CPU
-    ID ranges. The ranges are defined by a lower and higher boundary,
-    separated by a dash. The boundaries are inclusive.
+    The format of the mask can be given in three forms. First, the word
+    "all", which signifies the common case where all VCPUs can live on
+    any CPU, based on the hypervisor's decisions.
+
+    Second, a comma-separated list of CPU IDs or CPU ID ranges. The
+    ranges are defined by a lower and higher boundary, separated by a
+    dash, and the boundaries are inclusive. In this form, all VCPUs of
+    the instance will be mapped on the selected list of CPUs. Example:
+    ``0-2,5``, mapping all VCPUs (no matter how many) onto physical CPUs
+    0, 1, 2 and 5.
+
+    The last form is used for explicit control of VCPU-CPU pinnings. In
+    this form, the list of VCPU mappings is given as a colon (:)
+    separated list, whose elements are the possible values for the
+    second or first form above. In this form, the number of elements in
+    the colon-separated list _must_ equal the number of VCPUs of the
+    instance.
+
+    Example::
+
+      # Map the entire instance to CPUs 0-2
+      gnt-instance modify -H cpu_mask=0-2 my-inst
+
+      # 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 (backslashes for escaping the comma)
+      gnt-instance modify -H cpu_mask=all:1\\,3-5: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
 
 usb\_mouse
     Valid for the KVM hypervisor.
@@ -506,6 +626,9 @@ plain
 drbd
     Disk devices will be drbd (version 8.x) on top of lvm volumes.
 
+rbd
+    Disk devices will be rbd volumes residing inside a RADOS cluster.
+
 
 The optional second value of the ``-n (--node)`` is used for the drbd
 template type and specifies the remote node.
@@ -543,20 +666,21 @@ blktap
     better performance. Especially if you use a network file system
     (e.g. NFS) to store your instances this is the recommended choice.
 
+If ``--ignore-ipolicy`` is given any instance policy violations occuring
+during this operation are ignored.
 
-The ``--submit`` option is used to send the job to the master daemon
-but not wait for its completion. The job ID will be shown so that it
-can be examined via **gnt-job info**.
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
 
 Example::
 
-    # gnt-instance add -t file --disk 0:size=30g -B memory=512 -o debian-etch \
+    # gnt-instance add -t file --disk 0:size=30g -B maxmem=512 -o debian-etch \
       -n node1.example.com --file-storage-dir=mysubdir instance1.example.com
-    # gnt-instance add -t plain --disk 0:size=30g -B memory=512 -o debian-etch \
-      -n node1.example.com instance1.example.com
+    # gnt-instance add -t plain --disk 0:size=30g -B maxmem=1024,minmem=512 \
+      -o debian-etch -n node1.example.com instance1.example.com
     # gnt-instance add -t plain --disk 0:size=30g --disk 1:size=100g,vg=san \
-      -B memory=512 -o debian-etch -n node1.example.com instance1.example.com
-    # gnt-instance add -t drbd --disk 0:size=30g -B memory=512 -o debian-etch \
+      -B maxmem=512 -o debian-etch -n node1.example.com instance1.example.com
+    # gnt-instance add -t drbd --disk 0:size=30g -B maxmem=512 -o debian-etch \
       -n node1.example.com:node2.example.com instance2.example.com
 
 
@@ -641,7 +765,7 @@ parameters taken from the cluster defaults)::
         "iallocator": "dumb",
         "hypervisor": "xen-hvm",
         "hvparams": {"acpi": true},
-        "backend": {"memory": 512}
+        "backend": {"maxmem": 512, "minmem": 256}
       }
     }
 
@@ -655,8 +779,8 @@ follows::
 REMOVE
 ^^^^^^
 
-**remove** [--ignore-failures] [--shutdown-timeout=*N*] [--submit]
-[--force] {*instance*}
+**remove** [\--ignore-failures] [\--shutdown-timeout=*N*] [\--submit]
+[\--force] {*instance*}
 
 Remove an instance. This will remove all data from the instance and
 there is *no way back*. If you are not sure if you use an instance
@@ -673,12 +797,11 @@ before forcing the shutdown (e.g. ``xm destroy`` in Xen, killing the
 kvm process for KVM, etc.). By default two minutes are given to each
 instance to stop.
 
-The ``--submit`` option is used to send the job to the master daemon
-but not wait for its completion. The job ID will be shown so that it
-can be examined via **gnt-job info**.
-
 The ``--force`` option is used to skip the interactive confirmation.
 
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
+
 Example::
 
     # gnt-instance remove instance1.example.com
@@ -688,8 +811,8 @@ LIST
 ^^^^
 
 | **list**
-| [--no-headers] [--separator=*SEPARATOR*] [--units=*UNITS*] [-v]
-| [{-o|--output} *[+]FIELD,...*] [--filter] [instance...]
+| [\--no-headers] [\--separator=*SEPARATOR*] [\--units=*UNITS*] [-v]
+| [{-o|\--output} *[+]FIELD,...*] [\--filter] [instance...]
 
 Shows the currently configured instances with memory usage, disk
 usage, the node they are running on, and their run status.
@@ -749,7 +872,7 @@ Lists available fields for instances.
 INFO
 ^^^^
 
-**info** [-s \| --static] [--roman] {--all \| *instance*}
+**info** [-s \| \--static] [\--roman] {\--all \| *instance*}
 
 Show detailed information about the given instance(s). This is
 different from **list** as it shows detailed data about the instance's
@@ -770,15 +893,18 @@ MODIFY
 ^^^^^^
 
 | **modify**
-| [{-H|--hypervisor-parameters} *HYPERVISOR\_PARAMETERS*]
-| [{-B|--backend-parameters} *BACKEND\_PARAMETERS*]
-| [--net add*[:options]* \| --net remove \| --net *N:options*]
-| [--disk add:size=*SIZE*[,vg=*VG*][,metavg=*VG*] \| --disk remove \|
-|  --disk *N*:mode=*MODE*]
-| [{-t|--disk-template} plain | {-t|--disk-template} drbd -n *new_secondary*] [--no-wait-for-sync]
-| [--os-type=*OS* [--force-variant]]
-| [{-O|--os-parameters} *param*=*value*... ]
-| [--submit]
+| [{-H|\--hypervisor-parameters} *HYPERVISOR\_PARAMETERS*]
+| [{-B|\--backend-parameters} *BACKEND\_PARAMETERS*]
+| [{-m|\--runtime-memory} *SIZE*]
+| [\--net add*[:options]* \| \--net remove \| \--net *N:options*]
+| [\--disk add:size=*SIZE*[,vg=*VG*][,metavg=*VG*] \| \--disk remove \|
+|  \--disk *N*:mode=*MODE*]
+| [{-t|\--disk-template} plain | {-t|\--disk-template} drbd -n *new_secondary*] [\--no-wait-for-sync]
+| [\--os-type=*OS* [\--force-variant]]
+| [{-O|\--os-parameters} *param*=*value*... ]
+| [\--offline \| \--online]
+| [\--submit]
+| [\--ignore-ipolicy]
 | {*instance*}
 
 Modifies the memory size, number of vcpus, ip address, MAC address
@@ -800,20 +926,28 @@ option. The option ``--no-wait-for-sync`` can be used when converting
 to the ``drbd`` template in order to make the instance available for
 startup before DRBD has finished resyncing.
 
+The ``-m (--runtime-memory)`` option will change an instance's runtime
+memory to the given size (in MB if a different suffix is not specified),
+by ballooning it up or down to the new value.
+
 The ``--disk add:size=``*SIZE* option adds a disk to the instance. The
-optional ``vg=``*VG* option specifies LVM volume group other than
-default vg to create the disk on. For DRBD disks, the ``metavg=``*VG*
-option specifies the volume group for the metadata device. The
-``--disk remove`` option will remove the last disk of the
-instance. The ``--disk`` *N*``:mode=``*MODE* option will change the
-mode of the Nth disk of the instance between read-only (``ro``) and
+optional ``vg=``*VG* option specifies an LVM volume group other than
+the default volume group to create the disk on. For DRBD disks, the
+``metavg=``*VG* option specifies the volume group for the metadata
+device. ``--disk`` *N*``:add,size=``**SIZE** can be used to add a
+disk at a specific index. The ``--disk remove`` option will remove the
+last disk of the instance. Use ``--disk `` *N*``:remove`` to remove a
+disk by its index. The ``--disk`` *N*``:mode=``*MODE* option will change
+the mode of the Nth disk of the instance between read-only (``ro``) and
 read-write (``rw``).
 
-The ``--net add:``*options* option will add a new NIC to the
-instance. The available options are the same as in the **add** command
-(mac, ip, link, mode). The ``--net remove`` will remove the last NIC
-of the instance, while the ``--net`` *N*:*options* option will change
-the parameters of the Nth instance NIC.
+The ``--net add:``*options* and ``--net`` *N*``:add,``*options* option
+will add a new network interface to the instance. The available options
+are the same as in the **add** command (``mac``, ``ip``, ``link``,
+``mode``). The ``--net remove`` will remove the last network interface
+of the instance (``--net`` *N*``:remove`` for a specific index), while
+the ``--net`` *N*``:``*options* option will change the parameters of the Nth
+instance network interface.
 
 The option ``-o (--os-type)`` will change the OS name for the instance
 (without reinstallation). In case an OS variant is specified that is
@@ -821,20 +955,29 @@ not found, then by default the modification is refused, unless
 ``--force-variant`` is passed. An invalid OS will also be refused,
 unless the ``--force`` option is given.
 
-The ``--submit`` option is used to send the job to the master daemon
-but not wait for its completion. The job ID will be shown so that it
-can be examined via **gnt-job info**.
+The ``--online`` and ``--offline`` options are used to transition an
+instance into and out of the ``offline`` state. An instance can be
+turned offline only if it was previously down. The ``--online`` option
+fails if the instance was not in the ``offline`` state, otherwise it
+changes instance's state to ``down``. These modifications take effect
+immediately.
+
+If ``--ignore-ipolicy`` is given any instance policy violations occuring
+during this operation are ignored.
 
-All the changes take effect at the next restart. If the instance is
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
+
+Most of the changes take effect at the next restart. If the instance is
 running, there is no effect on the instance.
 
 REINSTALL
 ^^^^^^^^^
 
-| **reinstall** [{-o|--os-type} *os-type*] [--select-os] [-f *force*]
-| [--force-multiple]
-| [--instance \| --node \| --primary \| --secondary \| --all]
-| [{-O|--os-parameters} *OS\_PARAMETERS*] [--submit] {*instance*...}
+| **reinstall** [{-o|\--os-type} *os-type*] [\--select-os] [-f *force*]
+| [\--force-multiple]
+| [\--instance \| \--node \| \--primary \| \--secondary \| \--all]
+| [{-O|\--os-parameters} *OS\_PARAMETERS*] [\--submit] {*instance*...}
 
 Reinstalls the operating system on the given instance(s). The
 instance(s) must be stopped when running this command. If the ``-o
@@ -853,14 +996,13 @@ arguments or by using the ``--node``, ``--primary``, ``--secondary``
 or ``--all`` options), the user must pass the ``--force-multiple``
 options to skip the interactive confirmation.
 
-The ``--submit`` option is used to send the job to the master daemon
-but not wait for its completion. The job ID will be shown so that it
-can be examined via **gnt-job info**.
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
 
 RENAME
 ^^^^^^
 
-| **rename** [--no-ip-check] [--no-name-check] [--submit]
+| **rename** [\--no-ip-check] [\--no-name-check] [\--submit]
 | {*instance*} {*new\_name*}
 
 Renames the given instance. The instance must be stopped when running
@@ -876,9 +1018,8 @@ that the resolved name matches the provided name. Since the name check
 is used to compute the IP address, if you pass this option you must also
 pass the ``--no-ip-check`` option.
 
-The ``--submit`` option is used to send the job to the master daemon
-but not wait for its completion. The job ID will be shown so that it
-can be examined via **gnt-job info**.
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
 
 Starting/stopping/connecting to console
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -887,50 +1028,50 @@ STARTUP
 ^^^^^^^
 
 | **startup**
-| [--force] [--ignore-offline]
-| [--force-multiple] [--no-remember]
-| [--instance \| --node \| --primary \| --secondary \| --all \|
-| --tags \| --node-tags \| --pri-node-tags \| --sec-node-tags]
-| [{-H|--hypervisor-parameters} ``key=value...``]
-| [{-B|--backend-parameters} ``key=value...``]
-| [--submit] [--paused]
+| [\--force] [\--ignore-offline]
+| [\--force-multiple] [\--no-remember]
+| [\--instance \| \--node \| \--primary \| \--secondary \| \--all \|
+| \--tags \| \--node-tags \| \--pri-node-tags \| \--sec-node-tags]
+| [{-H|\--hypervisor-parameters} ``key=value...``]
+| [{-B|\--backend-parameters} ``key=value...``]
+| [\--submit] [\--paused]
 | {*name*...}
 
 Starts one or more instances, depending on the following options.  The
 four available modes are:
 
---instance
+\--instance
     will start the instances given as arguments (at least one argument
     required); this is the default selection
 
---node
+\--node
     will start the instances who have the given node as either primary
     or secondary
 
---primary
+\--primary
     will start all instances whose primary node is in the list of nodes
     passed as arguments (at least one node required)
 
---secondary
+\--secondary
     will start all instances whose secondary node is in the list of
     nodes passed as arguments (at least one node required)
 
---all
+\--all
     will start all instances in the cluster (no arguments accepted)
 
---tags
+\--tags
     will start all instances in the cluster with the tags given as
     arguments
 
---node-tags
+\--node-tags
     will start all instances in the cluster on nodes with the tags
     given as arguments
 
---pri-node-tags
+\--pri-node-tags
     will start all instances in the cluster on primary nodes with the
     tags given as arguments
 
---sec-node-tags
+\--sec-node-tags
     will start all instances in the cluster on secondary nodes with the
     tags given as arguments
 
@@ -958,7 +1099,7 @@ useful for quick testing without having to modify an instance back and
 forth, e.g.::
 
     # gnt-instance start -H kernel_args="single" instance1
-    # gnt-instance start -B memory=2048 instance2
+    # gnt-instance start -B maxmem=2048 instance2
 
 
 The first form will start the instance instance1 in single-user mode,
@@ -966,16 +1107,16 @@ and the instance instance2 with 2GB of RAM (this time only, unless
 that is the actual instance memory size already). Note that the values
 override the instance parameters (and not extend them): an instance
 with "kernel\_args=ro" when started with -H kernel\_args=single will
-result in "single", not "ro single".  The ``--submit`` option is used
-to send the job to the master daemon but not wait for its
-completion. The job ID will be shown so that it can be examined via
-**gnt-job info**.
+result in "single", not "ro single".
 
 The ``--paused`` option is only valid for Xen and kvm hypervisors.  This
 pauses the instance at the start of bootup, awaiting ``gnt-instance
 console`` to unpause it, allowing the entire boot process to be
 monitored for debugging.
 
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
+
 Example::
 
     # gnt-instance start instance1.example.com
@@ -987,11 +1128,11 @@ SHUTDOWN
 ^^^^^^^^
 
 | **shutdown**
-| [--timeout=*N*]
-| [--force-multiple] [--ignore-offline] [--no-remember]
-| [--instance \| --node \| --primary \| --secondary \| --all \|
-| --tags \| --node-tags \| --pri-node-tags \| --sec-node-tags]
-| [--submit]
+| [\--timeout=*N*]
+| [\--force-multiple] [\--ignore-offline] [\--no-remember]
+| [\--instance \| \--node \| \--primary \| \--secondary \| \--all \|
+| \--tags \| \--node-tags \| \--pri-node-tags \| \--sec-node-tags]
+| [\--submit]
 | {*name*...}
 
 Stops one or more instances. If the instance cannot be cleanly stopped
@@ -1009,10 +1150,6 @@ The ``--instance``, ``--node``, ``--primary``, ``--secondary``,
 ``--sec-node-tags`` options are similar as for the **startup** command
 and they influence the actual instances being shutdown.
 
-The ``--submit`` option is used to send the job to the master daemon
-but not wait for its completion. The job ID will be shown so that it
-can be examined via **gnt-job info**.
-
 ``--ignore-offline`` can be used to ignore offline primary nodes and
 force the instance to be marked as stopped. This option should be used
 with care as it can lead to an inconsistent cluster state.
@@ -1026,6 +1163,9 @@ you just need to disable the watcher, shutdown all instances with
 ``--no-remember``, and when the watcher is activated again it will
 restore the correct runtime state for all instances.
 
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
+
 Example::
 
     # gnt-instance shutdown instance1.example.com
@@ -1036,13 +1176,13 @@ REBOOT
 ^^^^^^
 
 | **reboot**
-| [{-t|--type} *REBOOT-TYPE*]
-| [--ignore-secondaries]
-| [--shutdown-timeout=*N*]
-| [--force-multiple]
-| [--instance \| --node \| --primary \| --secondary \| --all \|
-| --tags \| --node-tags \| --pri-node-tags \| --sec-node-tags]
-| [--submit]
+| [{-t|\--type} *REBOOT-TYPE*]
+| [\--ignore-secondaries]
+| [\--shutdown-timeout=*N*]
+| [\--force-multiple]
+| [\--instance \| \--node \| \--primary \| \--secondary \| \--all \|
+| \--tags \| \--node-tags \| \--pri-node-tags \| \--sec-node-tags]
+| [\--submit]
 | [*name*...]
 
 Reboots one or more instances. The type of reboot depends on the value
@@ -1068,6 +1208,9 @@ to stop.
 The ``--force-multiple`` will skip the interactive confirmation in the
 case the more than one instance will be affected.
 
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
+
 Example::
 
     # gnt-instance reboot instance1.example.com
@@ -1077,7 +1220,7 @@ Example::
 CONSOLE
 ^^^^^^^
 
-**console** [--show-cmd] {*instance*}
+**console** [\--show-cmd] {*instance*}
 
 Connects to the console of the given instance. If the instance is not
 up, an error is returned. Use the ``--show-cmd`` option to display the
@@ -1103,17 +1246,17 @@ Disk management
 REPLACE-DISKS
 ^^^^^^^^^^^^^
 
-**replace-disks** [--submit] [--early-release] {-p} [--disks *idx*]
-{*instance*}
+**replace-disks** [\--submit] [\--early-release] [\--ignore-ipolicy] {-p}
+[\--disks *idx*] {*instance*}
 
-**replace-disks** [--submit] [--early-release] {-s} [--disks *idx*]
-{*instance*}
+**replace-disks** [\--submit] [\--early-release] [\--ignore-ipolicy] {-s}
+[\--disks *idx*] {*instance*}
 
-**replace-disks** [--submit] [--early-release] {--iallocator *name*
-\| --new-secondary *NODE*} {*instance*}
+**replace-disks** [\--submit] [\--early-release] [\--ignore-ipolicy]
+{{-I\|\--iallocator} *name* \| \--node *node* } {*instance*}
 
-**replace-disks** [--submit] [--early-release] {--auto}
-{*instance*}
+**replace-disks** [\--submit] [\--early-release] [\--ignore-ipolicy]
+{\--auto} {*instance*}
 
 This command is a generalized form for replacing disks. It is
 currently only valid for the mirrored (DRBD) disk template.
@@ -1133,16 +1276,15 @@ selected automatically by the specified allocator plugin, otherwise
 the new secondary node will be the one chosen manually via the
 ``--new-secondary`` option.
 
+Note that it is not possible to select an offline or drained node as a
+new secondary.
+
 The fourth form (when using ``--auto``) will automatically determine
 which disks of an instance are faulty and replace them within the same
 node. The ``--auto`` option works only when an instance has only
 faulty disks on either the primary or secondary node; it doesn't work
 when both sides have faulty disks.
 
-The ``--submit`` option is used to send the job to the master daemon
-but not wait for its completion. The job ID will be shown so that it
-can be examined via **gnt-job info**.
-
 The ``--early-release`` changes the code so that the old storage on
 secondary node(s) is removed early (before the resync is completed)
 and the internal Ganeti locks for the current (and new, if any)
@@ -1152,13 +1294,17 @@ disk failure on the current secondary (thus the old storage is already
 broken) or when the storage on the primary node is known to be fine
 (thus we won't need the old storage for potential recovery).
 
-Note that it is not possible to select an offline or drained node as a
-new secondary.
+The ``--ignore-ipolicy`` let the command ignore instance policy
+violations if replace-disks changes groups and the instance would
+violate the new groups instance policy.
+
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
 
 ACTIVATE-DISKS
 ^^^^^^^^^^^^^^
 
-**activate-disks** [--submit] [--ignore-size] {*instance*}
+**activate-disks** [\--submit] [\--ignore-size] {*instance*}
 
 Activates the block devices of the given instance. If successful, the
 command will show the location and name of the block devices::
@@ -1171,10 +1317,7 @@ In this example, *node1.example.com* is the name of the node on which
 the devices have been activated. The *disk/0* and *disk/1* are the
 Ganeti-names of the instance disks; how they are visible inside the
 instance is hypervisor-specific. */dev/drbd0* and */dev/drbd1* are the
-actual block devices as visible on the node.  The ``--submit`` option
-is used to send the job to the master daemon but not wait for its
-completion. The job ID will be shown so that it can be examined via
-**gnt-job info**.
+actual block devices as visible on the node.
 
 The ``--ignore-size`` option can be used to activate disks ignoring
 the currently configured size in Ganeti. This can be used in cases
@@ -1186,10 +1329,13 @@ when activate-disks fails without it.
 Note that it is safe to run this command while the instance is already
 running.
 
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
+
 DEACTIVATE-DISKS
 ^^^^^^^^^^^^^^^^
 
-**deactivate-disks** [-f] [--submit] {*instance*}
+**deactivate-disks** [-f] [\--submit] {*instance*}
 
 De-activates the block devices of the given instance. Note that if you
 run this command for an instance with a drbd disk template, while it
@@ -1204,18 +1350,17 @@ option passed it will skip this check and directly try to deactivate
 the disks. This can still fail due to the instance actually running or
 other issues.
 
-The ``--submit`` option is used to send the job to the master daemon
-but not wait for its completion. The job ID will be shown so that it
-can be examined via **gnt-job info**.
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
 
 GROW-DISK
 ^^^^^^^^^
 
-**grow-disk** [--no-wait-for-sync] [--submit] {*instance*} {*disk*}
-{*amount*}
+| **grow-disk** [\--no-wait-for-sync] [\--submit] [\--absolute]
+| {*instance*} {*disk*} {*amount*}
 
 Grows an instance's disk. This is only possible for instances having a
-plain or drbd disk template.
+plain, drbd or rbd disk template.
 
 Note that this command only change the block device size; it will not
 grow the actual filesystems, partitions, etc. that live on that
@@ -1230,27 +1375,34 @@ disk. Usually, you will need to:
    the partition table on the disk
 
 The *disk* argument is the index of the instance disk to grow. The
-*amount* argument is given either as a number (and it represents the
-amount to increase the disk with in mebibytes) or can be given similar
-to the arguments in the create instance operation, with a suffix
-denoting the unit.
+*amount* argument is given as a number which can have a suffix (like the
+disk size in instance create); if the suffix is missing, the value will
+be interpreted as mebibytes.
+
+By default, the *amount* value represents the desired increase in the
+disk size (e.g. an amount of 1G will take a disk of size 3G to 4G). If
+the optional ``--absolute`` parameter is passed, then the *amount*
+argument doesn't represent the delta, but instead the desired final disk
+size (e.g. an amount of 8G will take a disk of size 4G to 8G).
 
-Note that the disk grow operation might complete on one node but fail
-on the other; this will leave the instance with different-sized LVs on
-the two nodes, but this will not create problems (except for unused
-space).
+For instances with a drbd template, note that the disk grow operation
+might complete on one node but fail on the other; this will leave the
+instance with different-sized LVs on the two nodes, but this will not
+create problems (except for unused space).
 
 If you do not want gnt-instance to wait for the new disk region to be
 synced, use the ``--no-wait-for-sync`` option.
 
-The ``--submit`` option is used to send the job to the master daemon
-but not wait for its completion. The job ID will be shown so that it
-can be examined via **gnt-job info**.
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
 
 Example (increase the first disk for instance1 by 16GiB)::
 
     # gnt-instance grow-disk instance1.example.com 0 16g
 
+Example for increasing the disk size to a certain size::
+
+   # gnt-instance grow-disk --absolute instance1.example.com 0 32g
 
 Also note that disk shrinking is not supported; use **gnt-backup
 export** and then **gnt-backup import** to reduce the disk size of an
@@ -1259,30 +1411,34 @@ instance.
 RECREATE-DISKS
 ^^^^^^^^^^^^^^
 
-**recreate-disks** [--submit] [--disks=``indices``] [-n node1:[node2]]
-  {*instance*}
+| **recreate-disks** [\--submit] [-n node1:[node2]]
+| [\--disk=*N*[:[size=*VAL*][,mode=*ro\|rw*]]] {*instance*}
 
-Recreates the disks of the given instance, or only a subset of the
-disks (if the option ``disks`` is passed, which must be a
-comma-separated list of disk indices, starting from zero).
+Recreates all or a subset of disks of the given instance.
 
 Note that this functionality should only be used for missing disks; if
 any of the given disks already exists, the operation will fail.  While
 this is suboptimal, recreate-disks should hopefully not be needed in
 normal operation and as such the impact of this is low.
 
+If only a subset should be recreated, any number of ``disk`` options can
+be specified. It expects a disk index and an optional list of disk
+parameters to change. Only ``size`` and ``mode`` can be changed while
+recreating disks. To recreate all disks while changing parameters on
+a subset only, a ``--disk`` option must be given for every disk of the
+instance.
+
 Optionally the instance's disks can be recreated on different
 nodes. This can be useful if, for example, the original nodes of the
 instance have gone down (and are marked offline), so we can't recreate
 on the same nodes. To do this, pass the new node(s) via ``-n`` option,
 with a syntax similar to the **add** command. The number of nodes
 passed must equal the number of nodes that the instance currently
-has. Note that changing nodes is only allowed for 'all disk'
-replacement (when ``--disks`` is not passed).
+has. Note that changing nodes is only allowed when all disks are
+replaced, e.g. when no ``--disk`` option is passed.
 
-The ``--submit`` option is used to send the job to the master daemon
-but not wait for its completion. The job ID will be shown so that it
-can be examined via **gnt-job info**.
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
 
 Recovery
 ~~~~~~~~
@@ -1290,16 +1446,25 @@ Recovery
 FAILOVER
 ^^^^^^^^
 
-**failover** [-f] [--ignore-consistency] [--shutdown-timeout=*N*]
-[--submit] {*instance*}
+| **failover** [-f] [\--ignore-consistency] [\--ignore-ipolicy]
+| [\--shutdown-timeout=*N*]
+| [{-n|\--target-node} *node* \| {-I|\--iallocator} *name*]
+| [\--submit]
+| {*instance*}
 
 Failover will stop the instance (if running), change its primary node,
 and if it was originally running it will start it again (on the new
 primary). This only works for instances with drbd template (in which
 case you can only fail to the secondary node) and for externally
-mirrored templates (shared storage) (which can change to any other
+mirrored templates (blockdev and rbd) (which can change to any other
 node).
 
+If the instance's disk template is of type blockdev or rbd, then you
+can explicitly specify the target node (which can be any node) using
+the ``-n`` or ``--target-node`` option, or specify an iallocator plugin
+using the ``-I`` or ``--iallocator`` option. If you omit both, the default
+iallocator will be used to specify the target node.
+
 Normally the failover will check the consistency of the disks before
 failing over the instance. If you are trying to migrate instances off
 a dead node, this will fail. Use the ``--ignore-consistency`` option
@@ -1313,9 +1478,11 @@ before forcing the shutdown (xm destroy in xen, killing the kvm
 process, for kvm). By default two minutes are given to each instance
 to stop.
 
-The ``--submit`` option is used to send the job to the master daemon
-but not wait for its completion. The job ID will be shown so that it
-can be examined via **gnt-job info**.
+If ``--ignore-ipolicy`` is given any instance policy violations occuring
+during this operation are ignored.
+
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
 
 Example::
 
@@ -1325,14 +1492,23 @@ Example::
 MIGRATE
 ^^^^^^^
 
-**migrate** [-f] {--cleanup} {*instance*}
+| **migrate** [-f] [\--allow-failover] [\--non-live]
+| [\--migration-mode=live\|non-live] [\--ignore-ipolicy]
+| [\--no-runtime-changes] [\--submit]
+| [{-n|\--target-node} *node* \| {-I|\--iallocator} *name*] {*instance*}
+
+| **migrate** [-f] \--cleanup [\--submit] {*instance*}
 
-**migrate** [-f] [--allow-failover] [--non-live]
-[--migration-mode=live\|non-live] {*instance*}
+Migrate will move the instance to its secondary node without shutdown.
+As with failover, it only works for instances having the drbd disk
+template or an externally mirrored disk template type such as blockdev
+or rbd.
 
-Migrate will move the instance to its secondary node without
-shutdown. It only works for instances having the drbd8 disk template
-type.
+If the instance's disk template is of type blockdev or rbd, then you can
+explicitly specify the target node (which can be any node) using the
+``-n`` or ``--target-node`` option, or specify an iallocator plugin
+using the ``-I`` or ``--iallocator`` option. If you omit both, the
+default iallocator will be used to specify the target node.
 
 The migration command needs a perfectly healthy instance, as we rely
 on the dual-master capability of drbd8 and the disks of the instance
@@ -1360,36 +1536,50 @@ ignored.
 The option ``-f`` will skip the prompting for confirmation.
 
 If ``--allow-failover`` is specified it tries to fallback to failover if
-it already can determine that a migration wont work (i.e. if the
-instance is shutdown). Please note that the fallback will not happen
+it already can determine that a migration won't work (e.g. if the
+instance is shut down). Please note that the fallback will not happen
 during execution. If a migration fails during execution it still fails.
 
+If ``--ignore-ipolicy`` is given any instance policy violations occuring
+during this operation are ignored.
+
+The ``--no-runtime-changes`` option forbids migrate to alter an
+instance's runtime before migrating it (eg. ballooning an instance
+down because the target node doesn't have enough available memory).
+
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
+
 Example (and expected output)::
 
     # gnt-instance migrate instance1
-    Migrate will happen to the instance instance1. Note that migration is
-    **experimental** in this version. This might impact the instance if
-    anything goes wrong. Continue?
+    Instance instance1 will be migrated. Note that migration
+    might impact the instance if anything goes wrong (e.g. due to bugs in
+    the hypervisor). Continue?
     y/[n]/?: y
+    Migrating instance instance1.example.com
     * checking disk consistency between source and target
-    * ensuring the target is in secondary mode
+    * switching node node2.example.com to secondary mode
+    * changing into standalone mode
     * changing disks into dual-master mode
-     - INFO: Waiting for instance instance1 to sync disks.
-     - INFO: Instance instance1's disks are in sync.
+    * wait until resync is done
+    * preparing node2.example.com to accept the instance
     * migrating instance to node2.example.com
-    * changing the instance's disks on source node to secondary
-     - INFO: Waiting for instance instance1 to sync disks.
-     - INFO: Instance instance1's disks are in sync.
-    * changing the instance's disks to single-master
+    * switching node node1.example.com to secondary mode
+    * wait until resync is done
+    * changing into standalone mode
+    * changing disks into single-master mode
+    * wait until resync is done
+    * done
     #
 
 
 MOVE
 ^^^^
 
-**move** [-f] [--ignore-consistency]
-[-n *node*] [--shutdown-timeout=*N*] [--submit]
-{*instance*}
+| **move** [-f] [\--ignore-consistency]
+| [-n *node*] [\--shutdown-timeout=*N*] [\--submit] [\--ignore-ipolicy]
+| {*instance*}
 
 Move will move the instance to an arbitrary node in the cluster.  This
 works only for instances having a plain or file disk template.
@@ -1407,9 +1597,11 @@ The ``--ignore-consistency`` option will make Ganeti ignore any errors
 in trying to shutdown the instance on its node; useful if the
 hypervisor is broken and you want to recuperate the data.
 
-The ``--submit`` option is used to send the job to the master daemon
-but not wait for its completion. The job ID will be shown so that it
-can be examined via **gnt-job info**.
+If ``--ignore-ipolicy`` is given any instance policy violations occuring
+during this operation are ignored.
+
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
 
 Example::
 
@@ -1419,7 +1611,8 @@ Example::
 CHANGE-GROUP
 ~~~~~~~~~~~~
 
-**change-group** [--iallocator *NAME*] [--to *GROUP*...] {*instance*}
+| **change-group** [\--submit]
+| [\--iallocator *NAME*] [\--to *GROUP*...] {*instance*}
 
 This command moves an instance to another node group. The move is
 calculated by an iallocator, either given on the command line or as a
@@ -1428,6 +1621,9 @@ cluster default.
 If no specific destination groups are specified using ``--to``, all
 groups except the one containing the instance are considered.
 
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
+
 Example::
 
     # gnt-instance change-group -I hail --to rack2 inst1.example.com
@@ -1439,7 +1635,7 @@ TAGS
 ADD-TAGS
 ^^^^^^^^
 
-**add-tags** [--from *file*] {*instancename*} {*tag*...}
+**add-tags** [\--from *file*] {*instancename*} {*tag*...}
 
 Add tags to the given instance. If any of the tags contains invalid
 characters, the entire operation will abort.
@@ -1460,7 +1656,7 @@ List the tags of the given instance.
 REMOVE-TAGS
 ^^^^^^^^^^^
 
-**remove-tags** [--from *file*] {*instancename*} {*tag*...}
+**remove-tags** [\--from *file*] {*instancename*} {*tag*...}
 
 Remove tags from the given instance. If any of the tags are not
 existing on the node, the entire operation will abort.
index ac54714..c634d0d 100644 (file)
@@ -59,7 +59,7 @@ information).
 LIST
 ~~~~
 
-**list** [--no-headers] [--separator=*SEPARATOR*]
+**list** [\--no-headers] [\--separator=*SEPARATOR*]
 [-o *[+]FIELD,...*]
 
 Lists the jobs and their status. By default, the job id, job
@@ -74,59 +74,21 @@ scripting.
 The ``-o`` option takes a comma-separated list of output fields.
 The available fields and their meaning are:
 
+@QUERY_FIELDS_JOB@
 
+If the value of the option starts with the character ``+``, the new
+fields will be added to the default list. This allows one to quickly
+see the default list plus a few other fields, instead of retyping
+the entire list of fields.
 
-id
-    the job id
-
-status
-    the status of the job
-
-priority
-    current priority of the job
-
-received_ts
-    the timestamp the job was received
-
-start_ts
-    the timestamp when the job was started
-
-end_ts
-    the timestamp when the job was ended
-
-summary
-    a summary of the opcodes that define the job
-
-ops
-    the list of opcodes defining the job
-
-opresult
-    the list of opcode results
-
-opstatus
-    the list of opcode statuses
-
-oplog
-    the list of opcode logs
-
-opstart
-    the list of opcode start times (before acquiring locks)
-
-opexec
-    the list of opcode execution start times (after acquiring any
-    necessary locks)
 
-opend
-    the list of opcode end times
+LIST-FIELDS
+~~~~~~~~~~~
 
-oppriority
-    the priority of each opcode
+**list-fields** [field...]
 
+Lists available fields for jobs.
 
-If the value of the option starts with the character ``+``, the new
-fields will be added to the default list. This allows one to quickly
-see the default list plus a few other fields, instead of retyping
-the entire list of fields.
 
 WATCH
 ~~~~~
index f94da99..8e0fd67 100644 (file)
@@ -23,10 +23,12 @@ COMMANDS
 ADD
 ~~~
 
-| **add** [--readd] [{-s|--secondary-ip} *secondary\_ip*]
-| [{-g|--node-group} *nodegroup*]
-| [--master-capable=``yes|no``] [--vm-capable=``yes|no``]
-| [--node-parameters *ndparams*]
+| **add** [\--readd] [{-s|\--secondary-ip} *secondary\_ip*]
+| [{-g|\--node-group} *nodegroup*]
+| [\--master-capable=``yes|no``] [\--vm-capable=``yes|no``]
+| [\--node-parameters *ndparams*]
+| [\--disk-state *diskstate*]
+| [\--hypervisor-state *hvstate*]
 | {*nodename*}
 
 Adds the given node to the cluster.
@@ -58,9 +60,16 @@ The ``-g (--node-group)`` option is used to add the new node into a
 specific node group, specified by UUID or name. If only one node group
 exists you can skip this option, otherwise it's mandatory.
 
-The ``vm_capable``, ``master_capable`` and ``ndparams`` options are
-described in **ganeti**(7), and are used to set the properties of the
-new node.
+The ``vm_capable``, ``master_capable``, ``ndparams``, ``diskstate`` and
+``hvstate`` options are described in **ganeti**(7), and are used to set
+the properties of the new node.
+
+The command performs some operations that change the state of the master
+and the new node, like copying certificates and starting the node daemon
+on the new node, or updating ``/etc/hosts`` on the master node.  If the
+command fails at a later stage, it doesn't undo such changes.  This
+should not be a problem, as a successful run of ``gnt-node add`` will
+bring everything back in sync.
 
 Example::
 
@@ -72,7 +81,7 @@ Example::
 ADD-TAGS
 ~~~~~~~~
 
-**add-tags** [--from *file*] {*nodename*} {*tag*...}
+**add-tags** [\--from *file*] {*nodename*} {*tag*...}
 
 Add tags to the given node. If any of the tags contains invalid
 characters, the entire operation will abort.
@@ -86,9 +95,10 @@ interpreted as stdin.
 EVACUATE
 ~~~~~~~~
 
-**evacuate** [-f] [--early-release] [--iallocator *NAME* \|
---new-secondary *destination\_node*]
-[--primary-only \| --secondary-only] [--early-release] {*node*}
+| **evacuate** [-f] [\--early-release] [\--submit]
+| [{-I|\--iallocator} *NAME* \| {-n|\--new-secondary} *destination\_node*]
+| [{-p|\--primary-only} \| {-s|\--secondary-only} ]
+|  {*node*}
 
 This command will move instances away from the given node. If
 ``--primary-only`` is given, only primary instances are evacuated, with
@@ -128,6 +138,9 @@ each affected instance individually:
   in the secondary node change mode (only valid for DRBD instances)
 - when neither of the above is done a combination of the two cases is run
 
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
+
 Example::
 
     # gnt-node evacuate -I hail node3.example.com
@@ -136,7 +149,7 @@ Example::
 FAILOVER
 ~~~~~~~~
 
-**failover** [-f] [--ignore-consistency] {*node*}
+**failover** [-f] [\--ignore-consistency] {*node*}
 
 This command will fail over all instances having the given node as
 primary to their secondary nodes. This works only for instances having
@@ -165,9 +178,9 @@ LIST
 ~~~~
 
 | **list**
-| [--no-headers] [--separator=*SEPARATOR*]
-| [--units=*UNITS*] [-v] [{-o|--output} *[+]FIELD,...*]
-| [--filter]
+| [\--no-headers] [\--separator=*SEPARATOR*]
+| [\--units=*UNITS*] [-v] [{-o|\--output} *[+]FIELD,...*]
+| [\--filter]
 | [node...]
 
 Lists the nodes in the cluster.
@@ -222,6 +235,37 @@ If no node names are given, then all nodes are queried. Otherwise,
 only the given nodes will be listed.
 
 
+LIST-DRBD
+~~~~~~~~~
+
+**list-drbd** [\--no-headers] [\--separator=*SEPARATOR*] node
+
+Lists the mapping of DRBD minors for a given node. This outputs a static
+list of fields (it doesn't accept the ``--output`` option), as follows:
+
+``Node``
+  The (full) name of the node we are querying
+``Minor``
+  The DRBD minor
+``Instance``
+  The instance the DRBD minor belongs to
+``Disk``
+  The disk index that the DRBD minor belongs to
+``Role``
+  Either ``primary`` or ``secondary``, denoting the role of the node for
+  the instance (note: this is not the live status of the DRBD device,
+  but the configuration value)
+``PeerNode``
+  The node that the minor is connected to on the other end
+
+This command can be used as a reverse lookup (from node and minor) to a
+given instance, which can be useful when debugging DRBD issues.
+
+Note that this command queries Ganeti via :manpage:`ganeti-confd(8)`, so
+it won't be available if support for ``confd`` has not been enabled at
+build time; furthermore, in Ganeti 2.6 this is only available via the
+Haskell version of confd (again selected at build time).
+
 LIST-FIELDS
 ~~~~~~~~~~~
 
@@ -240,16 +284,22 @@ List the tags of the given node.
 MIGRATE
 ~~~~~~~
 
-**migrate** [-f] [--non-live] [--migration-mode=live\|non-live]
-{*node*}
+| **migrate** [-f] [\--non-live] [\--migration-mode=live\|non-live]
+| [\--ignore-ipolicy] [\--submit] {*node*}
 
 This command will migrate all instances having the given node as
 primary to their secondary nodes. This works only for instances
 having a drbd disk template.
 
 As for the **gnt-instance migrate** command, the options
-``--no-live`` and ``--migration-mode`` can be given to influence
-the migration type.
+``--no-live``, ``--migration-mode`` and ``--no-runtime-changes``
+can be given to influence the migration type.
+
+If ``--ignore-ipolicy`` is given any instance policy violations occuring
+during this operation are ignored.
+
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
 
 Example::
 
@@ -259,19 +309,21 @@ Example::
 MODIFY
 ~~~~~~
 
-| **modify** [-f] [--submit]
-| [{-C|--master-candidate} ``yes|no``]
-| [{-D|--drained} ``yes|no``] [{-O|--offline} ``yes|no``]
-| [--master-capable=``yes|no``] [--vm-capable=``yes|no``] [--auto-promote]
-| [{-s|--secondary-ip} *secondary_ip*]
-| [--node-parameters *ndparams*]
-| [--node-powered=``yes|no``]
+| **modify** [-f] [\--submit]
+| [{-C|\--master-candidate} ``yes|no``]
+| [{-D|\--drained} ``yes|no``] [{-O|\--offline} ``yes|no``]
+| [\--master-capable=``yes|no``] [\--vm-capable=``yes|no``] [\--auto-promote]
+| [{-s|\--secondary-ip} *secondary_ip*]
+| [\--node-parameters *ndparams*]
+| [\--node-powered=``yes|no``]
+| [\--hypervisor-state *hvstate*]
+| [\--disk-state *diskstate*]
 | {*node*}
 
 This command changes the role of the node. Each options takes
 either a literal yes or no, and only one option should be given as
 yes. The meaning of the roles and flags are described in the
-manpage **ganeti**(7).
+manpage **ganeti(7)**.
 
 The option ``--node-powered`` can be used to modify state-of-record if
 it doesn't reflect the reality anymore.
@@ -294,6 +346,9 @@ The ``-s (--secondary-ip)`` option can be used to change the node's
 secondary ip. No drbd instances can be running on the node, while this
 operation is taking place.
 
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
+
 Example (setting the node back to online and master candidate)::
 
     # gnt-node modify --offline=no --master-candidate=yes node1.example.com
@@ -315,7 +370,7 @@ Example::
 REMOVE-TAGS
 ~~~~~~~~~~~
 
-**remove-tags** [--from *file*] {*nodename*} {*tag*...}
+**remove-tags** [\--from *file*] {*nodename*} {*tag*...}
 
 Remove tags from the given node. If any of the tags are not
 existing on the node, the entire operation will abort.
@@ -329,8 +384,8 @@ be interpreted as stdin.
 VOLUMES
 ~~~~~~~
 
-| **volumes** [--no-headers] [--human-readable]
-| [--separator=*SEPARATOR*] [{-o|--output} *FIELDS*]
+| **volumes** [\--no-headers] [\--human-readable]
+| [\--separator=*SEPARATOR*] [{-o|\--output} *FIELDS*]
 | [*node*...]
 
 Lists all logical volumes and their physical disks from the node(s)
@@ -382,9 +437,9 @@ Example::
 LIST-STORAGE
 ~~~~~~~~~~~~
 
-| **list-storage** [--no-headers] [--human-readable]
-| [--separator=*SEPARATOR*] [--storage-type=*STORAGE\_TYPE*]
-| [{-o|--output} *FIELDS*]
+| **list-storage** [\--no-headers] [\--human-readable]
+| [\--separator=*SEPARATOR*] [\--storage-type=*STORAGE\_TYPE*]
+| [{-o|\--output} *FIELDS*]
 | [*node*...]
 
 Lists the available storage units and their details for the given
@@ -453,8 +508,8 @@ Example::
 MODIFY-STORAGE
 ~~~~~~~~~~~~~~
 
-**modify-storage** [``--allocatable=yes|no``]
-{*node*} {*storage-type*} {*volume-name*}
+| **modify-storage** [\--allocatable={yes|no}] [\--submit]
+| {*node*} {*storage-type*} {*volume-name*}
 
 Modifies storage volumes on a node. Only LVM physical volumes can
 be modified at the moment. They have a storage type of "lvm-pv".
@@ -467,14 +522,14 @@ Example::
 REPAIR-STORAGE
 ~~~~~~~~~~~~~~
 
-**repair-storage** [--ignore-consistency] {*node*} {*storage-type*}
-{*volume-name*}
+| **repair-storage** [\--ignore-consistency] ]\--submit]
+| {*node*} {*storage-type*} {*volume-name*}
 
 Repairs a storage volume on a node. Only LVM volume groups can be
 repaired at this time. They have the storage type "lvm-vg".
 
-On LVM volume groups, **repair-storage** runs "vgreduce
---removemissing".
+On LVM volume groups, **repair-storage** runs ``vgreduce
+--removemissing``.
 
 
 
@@ -493,22 +548,25 @@ Example::
 POWERCYCLE
 ~~~~~~~~~~
 
-**powercycle** [``--yes``] [``--force``] {*node*}
+**powercycle** [\--yes] [\--force] [\--submit] {*node*}
 
 This command (tries to) forcefully reboot a node. It is a command
-that can be used if the node environemnt is broken, such that the
-admin can no longer login over ssh, but the Ganeti node daemon is
+that can be used if the node environment is broken, such that the
+admin can no longer login over SSH, but the Ganeti node daemon is
 still working.
 
 Note that this command is not guaranteed to work; it depends on the
 hypervisor how effective is the reboot attempt. For Linux, this
-command require that the kernel option CONFIG\_MAGIC\_SYSRQ is
+command requires the kernel option ``CONFIG_MAGIC_SYSRQ`` to be
 enabled.
 
 The ``--yes`` option can be used to skip confirmation, while the
 ``--force`` option is needed if the target node is the master
 node.
 
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
+
 POWER
 ~~~~~
 
index ac47c68..55fcd6a 100644 (file)
@@ -44,8 +44,8 @@ in the cluster, including its validity status, the supported API
 versions, the supported parameters (if any) and their
 documentations, etc.
 
-| **modify** [-H *HYPERVISOR*:option=*value*[,...]]
-| [--hidden=*yes|no*] [--blacklisted=*yes|no*]
+| **modify** [\--submit] [-H *HYPERVISOR*:option=*value*[,...]]
+| [\--hidden=*yes|no*] [\--blacklisted=*yes|no*]
 | {*OS*}
 
 This command will allow you to modify OS parameters.
@@ -66,6 +66,9 @@ Note: The given operating system doesn't have to exists. This allows
 preseeding the settings for operating systems not yet known to
 **gnt-os**.
 
+See **ganeti(7)** for a description of ``--submit`` and other common
+options.
+
 .. vim: set textwidth=72 :
 .. Local Variables:
 .. mode: rst
index 925fc44..f426cfa 100644 (file)
@@ -9,9 +9,9 @@ hail - Ganeti IAllocator plugin
 SYNOPSIS
 --------
 
-**hail** [ **-t** *file* | **--simulate** *spec* ] [options...] *input-file*
+**hail** [ **-t** *file* | **\--simulate** *spec* ] [options...] *input-file*
 
-**hail** --version
+**hail** \--version
 
 DESCRIPTION
 -----------
@@ -24,6 +24,9 @@ state and the request details, and output (on stdout) a JSON-formatted
 response. In case of critical failures, the error message is printed
 on stderr and the exit code is changed to show failure.
 
+If the input file name is ``-`` (a single minus sign), then the request
+data will be read from *stdin*.
+
 ALGORITHM
 ~~~~~~~~~
 
@@ -62,25 +65,26 @@ OPTIONS
 
 The options that can be passed to the program are as follows:
 
--p, --print-nodes
+-p, \--print-nodes
   Prints the before and after node status, in a format designed to allow
   the user to understand the node's most important parameters. See the
   man page **htools**(1) for more details about this option.
 
--t *datafile*, --text-data=*datafile*
-  The name of the file holding cluster information, to override the
-  data in the JSON request itself. This is mostly used for debugging.
+-t *datafile*, \--text-data=*datafile*
+  The name of the file holding cluster information, to override the data
+  in the JSON request itself. This is mostly used for debugging. The
+  format of the file is described in the man page **htools**(1).
 
---simulate *description*
-  Similar to the **-t** option, this allows overriding the cluster
-  data with a simulated cluster. For details about the description,
-  see the man page **hspace**(1).
+\--simulate *description*
+  Backend specification: similar to the **-t** option, this allows
+  overriding the cluster data with a simulated cluster. For details
+  about the description, see the man page **htools**(1).
 
--S *filename*, --save-cluster=*filename*
+-S *filename*, \--save-cluster=*filename*
   If given, the state of the cluster before and the iallocator run is
   saved to a file named *filename.pre-ialloc*, respectively
   *filename.post-ialloc*. This allows re-feeding the cluster state to
-  any of the htools utilities.
+  any of the htools utilities via the ``-t`` option.
 
 -v
   This option increases verbosity and can be used for debugging in order
index 49fd9ec..2cda02f 100644 (file)
@@ -11,33 +11,34 @@ SYNOPSIS
 
 **hbal** {backend options...} [algorithm options...] [reporting options...]
 
-**hbal** --version
+**hbal** \--version
 
 
 Backend options:
 
-{ **-m** *cluster* | **-L[** *path* **] [-X]** | **-t** *data-file* }
+{ **-m** *cluster* | **-L[** *path* **] [-X]** | **-t** *data-file* |
+**-I** *path* }
 
 Algorithm options:
 
-**[ --max-cpu *cpu-ratio* ]**
-**[ --min-disk *disk-ratio* ]**
+**[ \--max-cpu *cpu-ratio* ]**
+**[ \--min-disk *disk-ratio* ]**
 **[ -l *limit* ]**
 **[ -e *score* ]**
-**[ -g *delta* ]** **[ --min-gain-limit *threshold* ]**
+**[ -g *delta* ]** **[ \--min-gain-limit *threshold* ]**
 **[ -O *name...* ]**
-**[ --no-disk-moves ]**
-**[ --no-instance-moves ]**
+**[ \--no-disk-moves ]**
+**[ \--no-instance-moves ]**
 **[ -U *util-file* ]**
-**[ --evac-mode ]**
-**[ --select-instances *inst...* ]**
-**[ --exclude-instances *inst...* ]**
+**[ \--evac-mode ]**
+**[ \--select-instances *inst...* ]**
+**[ \--exclude-instances *inst...* ]**
 
 Reporting options:
 
 **[ -C[ *file* ] ]**
 **[ -p[ *fields* ] ]**
-**[ --print-instances ]**
+**[ \--print-instances ]**
 **[ -o ]**
 **[ -v... | -q ]**
 
@@ -52,9 +53,9 @@ the cluster into a better state.
 
 The algorithm used is designed to be stable (i.e. it will give you the
 same results when restarting it from the middle of the solution) and
-reasonably fast. It is not, however, designed to be a perfect
-algorithm--it is possible to make it go into a corner from which
-it can find no improvement, because it looks only one "step" ahead.
+reasonably fast. It is not, however, designed to be a perfect algorithm:
+it is possible to make it go into a corner from which it can find no
+improvement, because it looks only one "step" ahead.
 
 By default, the program will show the solution incrementally as it is
 computed, in a somewhat cryptic format; for getting the actual Ganeti
@@ -92,10 +93,10 @@ At each step, we prevent an instance move if it would cause:
 - an instance to move onto an offline node (offline nodes are either
   read from the cluster or declared with *-O*)
 - an exclusion-tag based conflict (exclusion tags are read from the
-  cluster and/or defined via the *--exclusion-tags* option)
-- a max vcpu/pcpu ratio to be exceeded (configured via *--max-cpu*)
+  cluster and/or defined via the *\--exclusion-tags* option)
+- a max vcpu/pcpu ratio to be exceeded (configured via *\--max-cpu*)
 - min disk free percentage to go below the configured limit
-  (configured via *--min-disk*)
+  (configured via *\--min-disk*)
 
 CLUSTER SCORING
 ~~~~~~~~~~~~~~~
@@ -178,10 +179,10 @@ which would make the respective node a SPOF for the given service.
 
 It works by tagging instances with certain tags and then building
 exclusion maps based on these. Which tags are actually used is
-configured either via the command line (option *--exclusion-tags*)
+configured either via the command line (option *\--exclusion-tags*)
 or via adding them to the cluster tags:
 
---exclusion-tags=a,b
+\--exclusion-tags=a,b
   This will make all instance tags of the form *a:\**, *b:\** be
   considered for the exclusion map
 
@@ -198,7 +199,7 @@ OPTIONS
 
 The options that can be passed to the program are as follows:
 
--C, --print-commands
+-C, \--print-commands
   Print the command list at the end of the run. Without this, the
   program will only show a shorter, but cryptic output.
 
@@ -216,27 +217,15 @@ The options that can be passed to the program are as follows:
   parallel (due to resource allocation in Ganeti) and thus we start a
   new jobset.
 
--p, --print-nodes
+-p, \--print-nodes
   Prints the before and after node status, in a format designed to allow
   the user to understand the node's most important parameters. See the
   man page **htools**(1) for more details about this option.
 
---print-instances
+\--print-instances
   Prints the before and after instance map. This is less useful as the
   node status, but it can help in understanding instance moves.
 
--o, --oneline
-  Only shows a one-line output from the program, designed for the case
-  when one wants to look at multiple clusters at once and check their
-  status.
-
-  The line will contain four fields:
-
-  - initial cluster score
-  - number of steps in the solution
-  - final cluster score
-  - improvement in the cluster score
-
 -O *name*
   This option (which can be given multiple times) will mark nodes as
   being *offline*. This means a couple of things:
@@ -251,7 +240,7 @@ The options that can be passed to the program are as follows:
   reported by RAPI as such, or that have "?" in file-based input in
   any numeric fields.
 
--e *score*, --min-score=*score*
+-e *score*, \--min-score=*score*
   This parameter denotes the minimum score we are happy with and alters
   the computation in two ways:
 
@@ -263,13 +252,13 @@ The options that can be passed to the program are as follows:
   The default value of the parameter is currently ``1e-9`` (chosen
   empirically).
 
--g *delta*, --min-gain=*delta*
+-g *delta*, \--min-gain=*delta*
   Since the balancing algorithm can sometimes result in just very tiny
   improvements, that bring less gain that they cost in relocation
   time, this parameter (defaulting to 0.01) represents the minimum
   gain we require during a step, to continue balancing.
 
---min-gain-limit=*threshold*
+\--min-gain-limit=*threshold*
   The above min-gain option will only take effect if the cluster score
   is already below *threshold* (defaults to 0.1). The rationale behind
   this setting is that at high cluster scores (badly balanced
@@ -278,30 +267,30 @@ The options that can be passed to the program are as follows:
   threshold, the total gain is only the threshold value, so we can
   exit early.
 
---no-disk-moves
+\--no-disk-moves
   This parameter prevents hbal from using disk move
   (i.e. "gnt-instance replace-disks") operations. This will result in
   a much quicker balancing, but of course the improvements are
   limited. It is up to the user to decide when to use one or another.
 
---no-instance-moves
+\--no-instance-moves
   This parameter prevents hbal from using instance moves
   (i.e. "gnt-instance migrate/failover") operations. This will only use
   the slow disk-replacement operations, and will also provide a worse
   balance, but can be useful if moving instances around is deemed unsafe
   or not preferred.
 
---evac-mode
+\--evac-mode
   This parameter restricts the list of instances considered for moving
   to the ones living on offline/drained nodes. It can be used as a
   (bulk) replacement for Ganeti's own *gnt-node evacuate*, with the
   note that it doesn't guarantee full evacuation.
 
---select-instances=*instances*
+\--select-instances=*instances*
   This parameter marks the given instances (as a comma-separated list)
   as the only ones being moved during the rebalance.
 
---exclude-instances=*instances*
+\--exclude-instances=*instances*
   This parameter marks the given instances (as a comma-separated list)
   from being moved during the rebalance.
 
@@ -325,32 +314,29 @@ The options that can be passed to the program are as follows:
   metrics and thus the influence of the dynamic utilisation will be
   practically insignificant.
 
--t *datafile*, --text-data=*datafile*
-  The name of the file holding node and instance information (if not
-  collecting via RAPI or LUXI). This or one of the other backends must
-  be selected.
-
--S *filename*, --save-cluster=*filename*
+-S *filename*, \--save-cluster=*filename*
   If given, the state of the cluster before the balancing is saved to
   the given file plus the extension "original"
   (i.e. *filename*.original), and the state at the end of the
   balancing is saved to the given file plus the extension "balanced"
   (i.e. *filename*.balanced). This allows re-feeding the cluster state
-  to either hbal itself or for example hspace.
+  to either hbal itself or for example hspace via the ``-t`` option.
+
+-t *datafile*, \--text-data=*datafile*
+  Backend specification: the name of the file holding node and instance
+  information (if not collecting via RAPI or LUXI). This or one of the
+  other backends must be selected. The option is described in the man
+  page **htools**(1).
 
 -m *cluster*
- Collect data directly from the *cluster* given as an argument via
- RAPI. If the argument doesn't contain a colon (:), then it is
- converted into a fully-built URL via prepending ``https://`` and
- appending the default RAPI port, otherwise it's considered a
- fully-specified URL and is used as-is.
+  Backend specification: collect data directly from the *cluster* given
+  as an argument via RAPI. The option is described in the man page
+  **htools**(1).
 
 -L [*path*]
-  Collect data directly from the master daemon, which is to be
-  contacted via the luxi (an internal Ganeti protocol). An optional
-  *path* argument is interpreted as the path to the unix socket on
-  which the master daemon listens; otherwise, the default path used by
-  ganeti when installed with *--localstatedir=/var* is used.
+  Backend specification: collect data directly from the master daemon,
+  which is to be contacted via LUXI (an internal Ganeti protocol). The
+  option is described in the man page **htools**(1).
 
 -X
   When using the Luxi backend, hbal can also execute the given
@@ -365,11 +351,11 @@ The options that can be passed to the program are as follows:
   The execution of the job series can be interrupted, see below for
   signal handling.
 
--l *N*, --max-length=*N*
+-l *N*, \--max-length=*N*
   Restrict the solution to this length. This can be used for example
   to automate the execution of the balancing.
 
---max-cpu=*cpu-ratio*
+\--max-cpu=*cpu-ratio*
   The maximum virtual to physical cpu ratio, as a floating point number
   greater than or equal to one. For example, specifying *cpu-ratio* as
   **2.5** means that, for a 4-cpu machine, a maximum of 10 virtual cpus
@@ -379,27 +365,27 @@ The options that can be passed to the program are as follows:
   make sense, as that means other resources (e.g. disk) won't be fully
   utilised due to CPU restrictions.
 
---min-disk=*disk-ratio*
+\--min-disk=*disk-ratio*
   The minimum amount of free disk space remaining, as a floating point
   number. For example, specifying *disk-ratio* as **0.25** means that
   at least one quarter of disk space should be left free on nodes.
 
--G *uuid*, --group=*uuid*
+-G *uuid*, \--group=*uuid*
   On an multi-group cluster, select this group for
   processing. Otherwise hbal will abort, since it cannot balance
   multiple groups at the same time.
 
--v, --verbose
+-v, \--verbose
   Increase the output verbosity. Each usage of this option will
   increase the verbosity (currently more than 2 doesn't make sense)
   from the default of one.
 
--q, --quiet
+-q, \--quiet
   Decrease the output verbosity. Each usage of this option will
   decrease the verbosity (less than zero doesn't make sense) from the
   default of one.
 
--V, --version
+-V, \--version
   Just show the program version and exit.
 
 SIGNAL HANDLING
diff --git a/man/hcheck.rst b/man/hcheck.rst
new file mode 100644 (file)
index 0000000..2a22c9b
--- /dev/null
@@ -0,0 +1,74 @@
+HCHECK(1) Ganeti | Version @GANETI_VERSION@
+===========================================
+
+NAME
+----
+
+hcheck \- Cluster checker
+
+SYNOPSIS
+--------
+
+**hcheck** {backend options...} [algorithm options...] [reporting options...]
+
+**hcheck** \--version
+
+
+Backend options:
+
+{ **-m** *cluster* | **-L[** *path* **] | **-t** *data-file* |
+**-I** *path* }
+
+Algorithm options:
+
+**[ \--no-simulation ]**
+**[ \--max-cpu *cpu-ratio* ]**
+**[ \--min-disk *disk-ratio* ]**
+**[ -l *limit* ]**
+**[ -e *score* ]**
+**[ -g *delta* ]** **[ \--min-gain-limit *threshold* ]**
+**[ -O *name...* ]**
+**[ \--no-disk-moves ]**
+**[ \--no-instance-moves ]**
+**[ -U *util-file* ]**
+**[ \--evac-mode ]**
+**[ \--select-instances *inst...* ]**
+**[ \--exclude-instances *inst...* ]**
+
+Reporting options:
+
+**[\--machine-readable**[=*CHOICE*] **]**
+**[ -p[ *fields* ] ]**
+**[ \--print-instances ]**
+**[ -v... | -q ]**
+
+
+DESCRIPTION
+-----------
+
+hcheck is the cluster checker. It prints information about cluster's
+health and checks whether a rebalance done using **hbal** would help.
+
+This information can be presented in both human-readable and
+machine-readable way.
+
+Note that it does not take any action, only performs a rebalance
+simulation if necessary.
+
+For more information about the algorithm details check **hbal(1)**.
+
+OPTIONS
+-------
+
+\--no-simulation
+  Only perform checks based on current cluster state, without trying
+  to simulate rebalancing.
+
+For a detailed description about the options listed above have a look at
+**htools(7)**, **hspace(1)** and **hbal(1)**.
+
+.. vim: set textwidth=72 :
+.. Local Variables:
+.. mode: rst
+.. fill-column: 72
+.. End:
diff --git a/man/hinfo.rst b/man/hinfo.rst
new file mode 100644 (file)
index 0000000..8169261
--- /dev/null
@@ -0,0 +1,52 @@
+HINFO(1) Ganeti | Version @GANETI_VERSION@
+==========================================
+
+NAME
+----
+
+hinfo \- Cluster information printer
+
+SYNOPSIS
+--------
+
+**hinfo** {backend options...} [algorithm options...] [reporting options...]
+
+**hinfo** \--version
+
+
+Backend options:
+
+{ **-m** *cluster* | **-L[** *path* **]** | **-t** *data-file* |
+**-I** *path* }
+
+Algorithm options:
+
+**[ -O *name...* ]**
+
+Reporting options:
+
+**[ -p[ *fields* ] ]**
+**[ \--print-instances ]**
+**[ -v... | -q ]**
+
+
+DESCRIPTION
+-----------
+
+hinfo is the cluster information printer. It prints information about
+the current cluster state and its residing nodes/instances. It's
+similar to the output of **hbal** except that it doesn't take any action
+is just for information purpose. This information might be useful for
+debugging a certain cluster state.
+
+OPTIONS
+-------
+
+For a detailed description about the options listed above have a look at
+**htools(7)** and **hbal(1)**.
+
+.. vim: set textwidth=72 :
+.. Local Variables:
+.. mode: rst
+.. fill-column: 72
+.. End:
index defb194..362700a 100644 (file)
@@ -9,9 +9,9 @@ hscan - Scan clusters via RAPI and save node/instance data
 SYNOPSIS
 --------
 
-**hscan** [-p] [--no-headers] [-d *path* ] *cluster...*
+**hscan** [-p] [\--no-headers] [-d *path* ] *cluster...*
 
-**hscan** --version
+**hscan** \--version
 
 DESCRIPTION
 -----------
@@ -71,7 +71,7 @@ OPTIONS
 
 The options that can be passed to the program are as follows:
 
--p, --print-nodes
+-p, \--print-nodes
   Prints the node status for each cluster after the cluster's one-line
   status display, in a format designed to allow the user to understand
   the node's most important parameters. For details, see the man page
@@ -81,7 +81,7 @@ The options that can be passed to the program are as follows:
   Save the node and instance data for each cluster under *path*,
   instead of the current directory.
 
--V, --version
+-V, \--version
   Just show the program version and exit.
 
 EXIT STATUS
index 2aff21d..b6b9d84 100644 (file)
@@ -12,39 +12,38 @@ SYNOPSIS
 **hspace** {backend options...} [algorithm options...] [request options...]
 [output options...] [-v... | -q]
 
-**hspace** --version
+**hspace** \--version
 
 Backend options:
 
 { **-m** *cluster* | **-L[** *path* **] [-X]** | **-t** *data-file* |
-**--simulate** *spec* }
+**\--simulate** *spec* | **-I** *path* }
 
 
 Algorithm options:
 
-**[ --max-cpu *cpu-ratio* ]**
-**[ --min-disk *disk-ratio* ]**
+**[ \--max-cpu *cpu-ratio* ]**
+**[ \--min-disk *disk-ratio* ]**
 **[ -O *name...* ]**
 
 
 Request options:
 
-**[--memory** *mem* **]**
-**[--disk** *disk* **]**
-**[--disk-template** *template* **]**
-**[--vcpus** *vcpus* **]**
-**[--tiered-alloc** *spec* **]**
+**[\--disk-template** *template* **]**
+
+**[\--standard-alloc** *disk,ram,cpu*  **]**
+
+**[\--tiered-alloc** *disk,ram,cpu* **]**
 
 Output options:
 
-**[--machine-readable**[=*CHOICE*] **]**
+**[\--machine-readable**[=*CHOICE*] **]**
 **[-p**[*fields*]**]**
 
 
 DESCRIPTION
 -----------
 
-
 hspace computes how many additional instances can be fit on a cluster,
 while maintaining N+1 status.
 
@@ -61,6 +60,10 @@ it is intended to interpreted as a shell fragment (or parsed as a
 output the additional information on stderr (such that the stdout is
 still parseable).
 
+By default, the instance specifications will be read from the cluster;
+the options ``--standard-alloc`` and ``--tiered-alloc`` can be used to
+override them.
+
 The following keys are available in the machine-readable output of the
 script (all prefixed with *HTS_*):
 
@@ -102,7 +105,7 @@ INI_MEM_INST, FIN_MEM_INST
   RAM).
 
 INI_MEM_OVERHEAD, FIN_MEM_OVERHEAD
-  The initial and final memory overhead--memory used for the node
+  The initial and final memory overhead, i.e. memory used for the node
   itself and unacounted memory (e.g. due to hypervisor overhead).
 
 INI_MEM_EFF, HTS_INI_MEM_EFF
@@ -132,14 +135,13 @@ INI_MNODE_DSK_AVAIL, FIN_MNODE_DSK_AVAIL
   Like the above but for disk.
 
 TSPEC
-  If the tiered allocation mode has been enabled, this parameter holds
-  the pairs of specifications and counts of instances that can be
-  created in this mode. The value of the key is a space-separated list
-  of values; each value is of the form *memory,disk,vcpu=count* where
-  the memory, disk and vcpu are the values for the current spec, and
-  count is how many instances of this spec can be created. A complete
-  value for this variable could be: **4096,102400,2=225
-  2560,102400,2=20 512,102400,2=21**.
+  This parameter holds the pairs of specifications and counts of
+  instances that can be created in the *tiered allocation* mode. The
+  value of the key is a space-separated list of values; each value is of
+  the form *memory,disk,vcpu=count* where the memory, disk and vcpu are
+  the values for the current spec, and count is how many instances of
+  this spec can be created. A complete value for this variable could be:
+  **4096,102400,2=225 2560,102400,2=20 512,102400,2=21**.
 
 KM_USED_CPU, KM_USED_NPU, KM_USED_MEM, KM_USED_DSK
   These represents the metrics of used resources at the start of the
@@ -160,7 +162,7 @@ KM_UNAV_CPU, KM_POOL_NPU, KM_UNAV_MEM, KM_UNAV_DSK
   example, the cluster might still have 100GiB disk free, but with no
   memory left for instances, we cannot allocate another instance, so
   in effect the disk space is unallocable. Note that the CPUs here
-  represent instance virtual CPUs, and in case the *--max-cpu* option
+  represent instance virtual CPUs, and in case the *\--max-cpu* option
   hasn't been specified this will be -1.
 
 ALLOC_USAGE
@@ -189,9 +191,8 @@ OK
   that the computation failed and any values present should not be
   relied upon.
 
-If the tiered allocation mode is enabled, then many of the INI_/FIN_
-metrics will be also displayed with a TRL_ prefix, and denote the
-cluster status at the end of the tiered allocation run.
+Many of the INI_/FIN_ metrics will be also displayed with a TRL_ prefix,
+and denote the cluster status at the end of the tiered allocation run.
 
 The human output format should be self-explanatory, so it is not
 described further.
@@ -201,22 +202,17 @@ OPTIONS
 
 The options that can be passed to the program are as follows:
 
---memory *mem*
-  The memory size of the instances to be placed (defaults to
-  4GiB). Units can be used (see below for more details).
+\--disk-template *template*
+  Overrides the disk template for the instance read from the cluster;
+  one of the Ganeti disk templates (e.g. plain, drbd, so on) should be
+  passed in.
 
---disk *disk*
-  The disk size of the instances to be placed (defaults to
-  100GiB). Units can be used.
+\--spindle-use *spindles*
+  Override the spindle use for the instance read from the cluster. The
+  value can be 0 (for example for instances that use very low I/O), but not
+  negative. For shared storage the value is ignored.
 
---disk-template *template*
-  The disk template for the instance; one of the Ganeti disk templates
-  (e.g. plain, drbd, so on) should be passed in.
-
---vcpus *vcpus*
-  The number of VCPUs of the instances to be placed (defaults to 1).
-
---max-cpu=*cpu-ratio*
+\--max-cpu=*cpu-ratio*
   The maximum virtual to physical cpu ratio, as a floating point number
   greater than or equal to one. For example, specifying *cpu-ratio* as
   **2.5** means that, for a 4-cpu machine, a maximum of 10 virtual cpus
@@ -226,12 +222,17 @@ The options that can be passed to the program are as follows:
   make sense, as that means other resources (e.g. disk) won't be fully
   utilised due to CPU restrictions.
 
---min-disk=*disk-ratio*
+\--min-disk=*disk-ratio*
   The minimum amount of free disk space remaining, as a floating point
   number. For example, specifying *disk-ratio* as **0.25** means that
   at least one quarter of disk space should be left free on nodes.
 
--p, --print-nodes
+-l *rounds*, \--max-length=*rounds*
+  Restrict the number of instance allocations to this length. This is
+  not very useful in practice, but can be used for testing hspace
+  itself, or to limit the runtime for very big clusters.
+
+-p, \--print-nodes
   Prints the before and after node status, in a format designed to allow
   the user to understand the node's most important parameters. See the
   man page **htools**(1) for more details about this option.
@@ -250,93 +251,81 @@ The options that can be passed to the program are as follows:
   are reported by RAPI as such, or that have "?" in file-based input
   in any numeric fields.
 
--t *datafile*, --text-data=*datafile*
-  The name of the file holding node and instance information (if not
-  collecting via RAPI or LUXI). This or one of the other backends must
-  be selected.
-
--S *filename*, --save-cluster=*filename*
+-S *filename*, \--save-cluster=*filename*
   If given, the state of the cluster at the end of the allocation is
   saved to a file named *filename.alloc*, and if tiered allocation is
   enabled, the state after tiered allocation will be saved to
   *filename.tiered*. This allows re-feeding the cluster state to
   either hspace itself (with different parameters) or for example
-  hbal.
+  hbal, via the ``-t`` option.
+
+-t *datafile*, \--text-data=*datafile*
+  Backend specification: the name of the file holding node and instance
+  information (if not collecting via RAPI or LUXI). This or one of the
+  other backends must be selected. The option is described in the man
+  page **htools**(1).
 
 -m *cluster*
- Collect data directly from the *cluster* given as an argument via
- RAPI. If the argument doesn't contain a colon (:), then it is
- converted into a fully-built URL via prepending ``https://`` and
- appending the default RAPI port, otherwise it's considered a
- fully-specified URL and is used as-is.
+  Backend specification: collect data directly from the *cluster* given
+  as an argument via RAPI. The option is described in the man page
+  **htools**(1).
 
 -L [*path*]
-  Collect data directly from the master daemon, which is to be
-  contacted via the luxi (an internal Ganeti protocol). An optional
-  *path* argument is interpreted as the path to the unix socket on
-  which the master daemon listens; otherwise, the default path used by
-  ganeti when installed with *--localstatedir=/var* is used.
-
---simulate *description*
-  Instead of using actual data, build an empty cluster given a node
-  description. The *description* parameter must be a comma-separated
-  list of five elements, describing in order:
-
-  - the allocation policy for this node group
-  - the number of nodes in the cluster
-  - the disk size of the nodes (default in mebibytes, units can be used)
-  - the memory size of the nodes (default in mebibytes, units can be used)
-  - the cpu core count for the nodes
-
-  An example description would be **preferred,B20,100G,16g,4**
-  describing a 20-node cluster where each node has 100GB of disk
-  space, 16GiB of memory and 4 CPU cores. Note that all nodes must
-  have the same specs currently.
-
-  This option can be given multiple times, and each new use defines a
-  new node group. Hence different node groups can have different
-  allocation policies and node count/specifications.
-
---tiered-alloc *spec*
-  Besides the standard, fixed-size allocation, also do a tiered
-  allocation scheme where the algorithm starts from the given
-  specification and allocates until there is no more space; then it
-  decreases the specification and tries the allocation again. The
-  decrease is done on the matric that last failed during
-  allocation. The specification given is similar to the *--simulate*
-  option and it holds:
+  Backend specification: collect data directly from the master daemon,
+  which is to be contacted via LUXI (an internal Ganeti protocol). The
+  option is described in the man page **htools**(1).
+
+\--simulate *description*
+  Backend specification: similar to the **-t** option, this allows
+  overriding the cluster data with a simulated cluster. For details
+  about the description, see the man page **htools**(1).
+
+\--standard-alloc *disk,ram,cpu*
+  This option overrides the instance size read from the cluster for the
+  *standard* allocation mode, where we simply allocate instances of the
+  same, fixed size until the cluster runs out of space.
+
+  The specification given is similar to the *\--simulate* option and it
+  holds:
 
   - the disk size of the instance (units can be used)
   - the memory size of the instance (units can be used)
   - the vcpu count for the insance
 
-  An example description would be *100G,4g,2* describing an initial
-  starting specification of 100GB of disk space, 4GiB of memory and 2
-  VCPUs.
+  An example description would be *100G,4g,2* describing an instance
+  specification of 100GB of disk space, 4GiB of memory and 2 VCPUs.
+
+\--tiered-alloc *disk,ram,cpu*
+  This option overrides the instance size for the *tiered* allocation
+  mode. In this mode, the algorithm starts from the given specification
+  and allocates until there is no more space; then it decreases the
+  specification and tries the allocation again. The decrease is done on
+  the metric that last failed during allocation. The argument should
+  have the same format as for ``--standard-alloc``.
 
   Also note that the normal allocation and the tiered allocation are
   independent, and both start from the initial cluster state; as such,
   the instance count for these two modes are not related one to
   another.
 
---machines-readable[=*choice*]
+\--machine-readable[=*choice*]
   By default, the output of the program is in "human-readable" format,
   i.e. text descriptions. By passing this flag you can either enable
   (``--machine-readable`` or ``--machine-readable=yes``) or explicitly
   disable (``--machine-readable=no``) the machine readable format
   described above.
 
--v, --verbose
+-v, \--verbose
   Increase the output verbosity. Each usage of this option will
   increase the verbosity (currently more than 2 doesn't make sense)
   from the default of one.
 
--q, --quiet
+-q, \--quiet
   Decrease the output verbosity. Each usage of this option will
   decrease the verbosity (less than zero doesn't make sense) from the
   default of one.
 
--V, --version
+-V, \--version
   Just show the program version and exit.
 
 UNITS
index 3104bf0..ea9019f 100644 (file)
@@ -12,6 +12,9 @@ SYNOPSIS
 **hbal**
   cluster balancer
 
+**hcheck**
+  cluster checker
+
 **hspace**
   cluster capacity computation
 
@@ -21,6 +24,9 @@ SYNOPSIS
 **hscan**
   saves cluster state for later reuse
 
+**hinfo**
+  cluster information printer
+
 
 DESCRIPTION
 -----------
@@ -35,6 +41,9 @@ environment variable HTOOLS can be used to set the desired role.
 Installed as ``hbal``, it computes and optionally executes a suite of
 instance moves in order to balance the cluster.
 
+Installed as ``hcheck``, it preforms cluster checks and optionally
+simulates rebalancing with all the ``hbal`` options available.
+
 Installed as ``hspace``, it computes how many additional instances can
 be fit on a cluster, while maintaining N+1 status. It can run on models
 of existing clusters or of simulated clusters.
@@ -45,13 +54,16 @@ by Ganeti to compute new instance allocations and instance moves.
 Installed as ``hscan``, it scans the local or remote cluster state and
 saves it to files which can later be reused by the other roles.
 
+Installed as ``hinfo``, it prints information about the current cluster
+state.
+
 COMMON OPTIONS
 --------------
 
 Options behave the same in all program modes, but not all program modes
 support all options. Some common options are:
 
--p, --print-nodes
+-p, \--print-nodes
   Prints the node status, in a format designed to allow the user to
   understand the node's most important parameters. If the command in
   question makes a cluster transition (e.g. balancing or allocation),
@@ -131,17 +143,116 @@ support all options. Some common options are:
   lNet
     the dynamic net load (if the information is available)
 
--v, --verbose
+-t *datafile*, \--text-data=*datafile*
+  Backend specification: the name of the file holding node and instance
+  information (if not collecting via RAPI or LUXI). This or one of the
+  other backends must be selected. The option is described in the man
+  page **htools**(1).
+
+  The file should contain text data, line-based, with two empty lines
+  separating sections. The lines themselves are column-based, with the
+  pipe symbol (``|``) acting as separator.
+
+  The first section contains group data, with two columns:
+
+  - group name
+  - group uuid
+
+  The second sections contains node data, with the following columns:
+
+  - node name
+  - node total memory
+  - node free memory
+  - node total disk
+  - node free disk
+  - node physical cores
+  - offline field (as ``Y`` or ``N``)
+  - group UUID
+  - node spindle count
+
+  The third section contains instance data, with the fields:
+
+  - instance name
+  - instance memory
+  - instance disk size
+  - instance vcpus
+  - instance status (in Ganeti's format, e.g. ``running`` or ``ERROR_down``)
+  - instance ``auto_balance`` flag (see man page **gnt-instance** (7))
+  - instance primary node
+  - instance secondary node(s), if any
+  - instance disk type (e.g. ``plain`` or ``drbd``)
+  - instance tags
+
+  The fourth section contains the cluster tags, with one tag per line
+  (no columns/no column processing).
+
+  The fifth section contains the ipolicies of the cluster and the node
+  groups, in the following format (separated by ``|``):
+
+  - owner (empty if cluster, group name otherwise)
+  - standard, min, max instance specs, containing the following values
+    separated by commas:
+    - memory size
+    - cpu count
+    - disk size
+    - disk count
+    - nic count
+  - disk templates
+  - vcpu ratio
+  - spindle ratio
+
+-m *cluster*
+  Backend specification: collect data directly from the *cluster* given
+  as an argument via RAPI. If the argument doesn't contain a colon (:),
+  then it is converted into a fully-built URL via prepending
+  ``https://`` and appending the default RAPI port, otherwise it is
+  considered a fully-specified URL and used as-is.
+
+-L [*path*]
+  Backend specification: collect data directly from the master daemon,
+  which is to be contacted via LUXI (an internal Ganeti protocol). An
+  optional *path* argument is interpreted as the path to the unix socket
+  on which the master daemon listens; otherwise, the default path used
+  by Ganeti (configured at build time) is used.
+
+-I|\--ialloc-src *path*
+  Backend specification: load data directly from an iallocator request
+  (as produced by Ganeti when doing an iallocator call).  The iallocator
+  request is read from specified path.
+
+\--simulate *description*
+  Backend specification: instead of using actual data, build an empty
+  cluster given a node description. The *description* parameter must be
+  a comma-separated list of five elements, describing in order:
+
+  - the allocation policy for this node group (*preferred*, *allocable*
+    or *unallocable*, or alternatively the short forms *p*, *a* or *u*)
+  - the number of nodes in the cluster
+  - the disk size of the nodes (default in mebibytes, units can be used)
+  - the memory size of the nodes (default in mebibytes, units can be used)
+  - the cpu core count for the nodes
+  - the spindle count for the nodes
+
+  An example description would be **preferred,20,100G,16g,4,2**
+  describing a 20-node cluster where each node has 100GB of disk space,
+  16GiB of memory, 4 CPU cores and 2 disk spindles. Note that all nodes
+  must have the same specs currently.
+
+  This option can be given multiple times, and each new use defines a
+  new node group. Hence different node groups can have different
+  allocation policies and node count/specifications.
+
+-v, \--verbose
   Increase the output verbosity. Each usage of this option will
   increase the verbosity (currently more than 2 doesn't make sense)
   from the default of one.
 
--q, --quiet
+-q, \--quiet
   Decrease the output verbosity. Each usage of this option will
   decrease the verbosity (less than zero doesn't make sense) from the
   default of one.
 
--V, --version
+-V, \--version
   Just show the program version and exit.
 
 UNITS
diff --git a/qa/__init__.py b/qa/__init__.py
new file mode 100644 (file)
index 0000000..f703026
--- /dev/null
@@ -0,0 +1,24 @@
+#
+#
+
+# Copyright (C) 2012 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.
+
+
+# empty file for package definition
+
+"""Ganeti QA scripts"""
index a2a4eb0..9451812 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/python -u
 #
 
-# Copyright (C) 2007, 2008, 2009, 2010, 2011 Google Inc.
+# Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012 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
@@ -38,6 +38,7 @@ import qa_group
 import qa_instance
 import qa_node
 import qa_os
+import qa_job
 import qa_rapi
 import qa_tags
 import qa_utils
@@ -71,7 +72,7 @@ def _DescriptionOf(fn):
   return desc.rstrip(".")
 
 
-def RunTest(fn, *args):
+def RunTest(fn, *args, **kwargs):
   """Runs a test after printing a header.
 
   """
@@ -84,7 +85,7 @@ def RunTest(fn, *args):
   print _FormatHeader("%s start %s" % (tstart, desc))
 
   try:
-    retval = fn(*args)
+    retval = fn(*args, **kwargs)
     return retval
   finally:
     tstop = datetime.datetime.now()
@@ -92,7 +93,7 @@ def RunTest(fn, *args):
     print _FormatHeader("%s time=%s %s" % (tstop, tdelta, desc))
 
 
-def RunTestIf(testnames, fn, *args):
+def RunTestIf(testnames, fn, *args, **kwargs):
   """Runs a test conditionally.
 
   @param testnames: either a single test name in the configuration
@@ -100,7 +101,7 @@ def RunTestIf(testnames, fn, *args):
 
   """
   if qa_config.TestEnabled(testnames):
-    RunTest(fn, *args)
+    RunTest(fn, *args, **kwargs)
   else:
     tstart = datetime.datetime.now()
     desc = _DescriptionOf(fn)
@@ -130,6 +131,7 @@ def SetupCluster(rapi_user, rapi_secret):
   # Test on empty cluster
   RunTestIf("node-list", qa_node.TestNodeList)
   RunTestIf("instance-list", qa_instance.TestInstanceList)
+  RunTestIf("job-list", qa_job.TestJobList)
 
   RunTestIf("create-cluster", qa_node.TestNodeAddAll)
   if not qa_config.TestEnabled("create-cluster"):
@@ -146,6 +148,8 @@ def SetupCluster(rapi_user, rapi_secret):
   # Test listing fields
   RunTestIf("node-list", qa_node.TestNodeListFields)
   RunTestIf("instance-list", qa_instance.TestInstanceListFields)
+  RunTestIf("job-list", qa_job.TestJobListFields)
+  RunTestIf("instance-export", qa_instance.TestBackupListFields)
 
   RunTestIf("node-info", qa_node.TestNodeInfo)
 
@@ -155,11 +159,14 @@ def RunClusterTests():
 
   """
   for test, fn in [
+    ("create-cluster", qa_cluster.TestClusterInitDisk),
     ("cluster-renew-crypto", qa_cluster.TestClusterRenewCrypto),
     ("cluster-verify", qa_cluster.TestClusterVerify),
     ("cluster-reserved-lvs", qa_cluster.TestClusterReservedLvs),
     # TODO: add more cluster modify tests
+    ("cluster-modify", qa_cluster.TestClusterModifyEmpty),
     ("cluster-modify", qa_cluster.TestClusterModifyBe),
+    ("cluster-modify", qa_cluster.TestClusterModifyDisk),
     ("cluster-rename", qa_cluster.TestClusterRename),
     ("cluster-info", qa_cluster.TestClusterVersion),
     ("cluster-info", qa_cluster.TestClusterInfo),
@@ -223,6 +230,8 @@ def RunCommonInstanceTests(instance):
   RunTestIf("instance-shutdown", qa_instance.TestInstanceShutdown, instance)
   RunTestIf(["instance-shutdown", "instance-console", "rapi"],
             qa_rapi.TestRapiStoppedInstanceConsole, instance)
+  RunTestIf(["instance-shutdown", "instance-modify"],
+            qa_instance.TestInstanceStoppedModify, instance)
   RunTestIf("instance-shutdown", qa_instance.TestInstanceStartup, instance)
 
   # Test shutdown/start via RAPI
@@ -243,31 +252,41 @@ def RunCommonInstanceTests(instance):
   RunTestIf(["instance-console", "rapi"],
             qa_rapi.TestRapiInstanceConsole, instance)
 
-  RunTestIf("instance-reinstall", qa_instance.TestInstanceShutdown, instance)
+  DOWN_TESTS = qa_config.Either([
+    "instance-reinstall",
+    "instance-rename",
+    "instance-grow-disk",
+    ])
+
+  # shutdown instance for any 'down' tests
+  RunTestIf(DOWN_TESTS, qa_instance.TestInstanceShutdown, instance)
+
+  # now run the 'down' state tests
   RunTestIf("instance-reinstall", qa_instance.TestInstanceReinstall, instance)
   RunTestIf(["instance-reinstall", "rapi"],
             qa_rapi.TestRapiInstanceReinstall, instance)
-  RunTestIf("instance-reinstall", qa_instance.TestInstanceStartup, instance)
-
-  RunTestIf("instance-reboot", qa_instance.TestInstanceReboot, instance)
 
   if qa_config.TestEnabled("instance-rename"):
     rename_source = instance["name"]
     rename_target = qa_config.get("rename", None)
-    RunTest(qa_instance.TestInstanceShutdown, instance)
     # perform instance rename to the same name
-    RunTest(qa_instance.TestInstanceRename, rename_source, rename_source)
-    RunTestIf("rapi", qa_rapi.TestRapiInstanceRename,
+    RunTest(qa_instance.TestInstanceRenameAndBack,
+            rename_source, rename_source)
+    RunTestIf("rapi", qa_rapi.TestRapiInstanceRenameAndBack,
               rename_source, rename_source)
     if rename_target is not None:
       # perform instance rename to a different name, if we have one configured
-      RunTest(qa_instance.TestInstanceRename, rename_source, rename_target)
-      RunTest(qa_instance.TestInstanceRename, rename_target, rename_source)
-      RunTestIf("rapi", qa_rapi.TestRapiInstanceRename,
+      RunTest(qa_instance.TestInstanceRenameAndBack,
+              rename_source, rename_target)
+      RunTestIf("rapi", qa_rapi.TestRapiInstanceRenameAndBack,
                 rename_source, rename_target)
-      RunTestIf("rapi", qa_rapi.TestRapiInstanceRename,
-                rename_target, rename_source)
-    RunTest(qa_instance.TestInstanceStartup, instance)
+
+  RunTestIf(["instance-grow-disk"], qa_instance.TestInstanceGrowDisk, instance)
+
+  # and now start the instance again
+  RunTestIf(DOWN_TESTS, qa_instance.TestInstanceStartup, instance)
+
+  RunTestIf("instance-reboot", qa_instance.TestInstanceReboot, instance)
 
   RunTestIf("tags", qa_tags.TestInstanceTags, instance)
 
@@ -278,6 +297,9 @@ def RunCommonInstanceTests(instance):
   # Lists instances, too
   RunTestIf("node-list", qa_node.TestNodeList)
 
+  # Some jobs have been run, let's test listing them
+  RunTestIf("job-list", qa_job.TestJobList)
+
 
 def RunCommonNodeTests():
   """Run a few common node tests.
@@ -328,8 +350,10 @@ def RunExportImportTests(instance, pnode, snode):
       if qa_config.TestEnabled("instance-import"):
         newinst = qa_config.AcquireInstance()
         try:
-          RunTest(qa_instance.TestInstanceImport, pnode, newinst,
+          RunTest(qa_instance.TestInstanceImport, newinst, pnode,
                   expnode, name)
+          # Check if starting the instance works
+          RunTest(qa_instance.TestInstanceStartup, newinst)
           RunTest(qa_instance.TestInstanceRemove, newinst)
         finally:
           qa_config.ReleaseInstance(newinst)
@@ -425,6 +449,7 @@ def RunQa():
   try:
     RunTestIf("node-readd", qa_node.TestNodeReadd, pnode)
     RunTestIf("node-modify", qa_node.TestNodeModify, pnode)
+    RunTestIf("delay", qa_cluster.TestDelay, pnode)
   finally:
     qa_config.ReleaseNode(pnode)
 
@@ -439,7 +464,8 @@ def RunQa():
         for use_client in [True, False]:
           rapi_instance = RunTest(qa_rapi.TestRapiInstanceAdd, pnode,
                                   use_client)
-          RunCommonInstanceTests(rapi_instance)
+          if qa_config.TestEnabled("instance-plain-rapi-common-tests"):
+            RunCommonInstanceTests(rapi_instance)
           RunTest(qa_rapi.TestRapiInstanceRemove, rapi_instance, use_client)
           del rapi_instance
 
@@ -464,6 +490,8 @@ def RunQa():
         snode = qa_config.AcquireNode(exclude=pnode)
         try:
           instance = RunTest(func, pnode, snode)
+          RunTestIf("haskell-confd", qa_node.TestNodeListDrbd, pnode)
+          RunTestIf("haskell-confd", qa_node.TestNodeListDrbd, snode)
           RunCommonInstanceTests(instance)
           RunGroupListTests()
           RunTest(qa_group.TestAssignNodesIncludingSplit,
@@ -481,6 +509,18 @@ def RunQa():
         finally:
           qa_config.ReleaseNode(snode)
 
+    # Test removing instance with offline drbd secondary
+    if qa_config.TestEnabled("instance-remove-drbd-offline"):
+      snode = qa_config.AcquireNode(exclude=pnode)
+      instance = \
+        qa_instance.TestInstanceAddWithDrbdDisk(pnode, snode)
+      try:
+        qa_node.MakeNodeOffline(snode, "yes")
+        RunTest(qa_instance.TestInstanceRemove, instance)
+      finally:
+        qa_node.MakeNodeOffline(snode, "no")
+        qa_config.ReleaseNode(snode)
+
     if qa_config.TestEnabled(["instance-add-plain-disk", "instance-export"]):
       for shutdown in [False, True]:
         instance = RunTest(qa_instance.TestInstanceAddWithPlainDisk, pnode)
@@ -528,7 +568,12 @@ def main():
 
   qa_config.Load(config_file)
 
-  qa_utils.StartMultiplexer(qa_config.GetMasterNode()["primary"])
+  primary = qa_config.GetMasterNode()["primary"]
+  qa_utils.StartMultiplexer(primary)
+  print ("SSH command for primary node: %s" %
+         utils.ShellQuoteArgs(qa_utils.GetSSHCommand(primary, "")))
+  print ("SSH command for other nodes: %s" %
+         utils.ShellQuoteArgs(qa_utils.GetSSHCommand("NODE", "")))
   try:
     RunQa()
   finally:
index 5c71ce2..dc033ac 100644 (file)
   "primary_ip_version": 4,
 
   "os": "debian-etch",
-  "mem": "512M",
+  "maxmem": "1024M",
+  "minmem": "512M",
+
+  "# Instance policy specs": null,
+  "ispec_mem_size_max": 1024,
+  "ispec_disk_size_min": 512,
 
   "# Lists of disk sizes": null,
   "disk": ["1G", "512M"],
   "disk-growth": ["2G", "768M"],
 
+  "# Script to check instance status": null,
+  "instance-check": null,
+
+  "# Regular expression to ignore existing tags": null,
+  "ignore-tags-re": null,
+
   "nodes": [
     {
       "# Master node": null,
 
   "instances": [
     {
-      "name": "xen-test-inst1"
+      "name": "xen-test-inst1",
+
+      "# Static MAC address": null,
+      "#nic.mac/0": "AA:00:00:11:11:11"
     },
     {
-      "name": "xen-test-inst2"
+      "name": "xen-test-inst2",
+
+      "# Static MAC address": null,
+      "#nic.mac/0": "AA:00:00:22:22:22"
     }
   ],
 
@@ -59,6 +76,7 @@
     "tags": true,
     "rapi": true,
     "test-jobqueue": true,
+    "delay": true,
 
     "create-cluster": true,
     "cluster-verify": true,
@@ -77,6 +95,8 @@
     "cluster-redist-conf": true,
     "cluster-repair-disk-sizes": true,
 
+    "haskell-confd": true,
+
     "group-list": true,
     "group-rwops": true,
 
     "instance-add-plain-disk": true,
     "instance-add-drbd-disk": true,
     "instance-convert-disk": true,
+    "instance-plain-rapi-common-tests": true,
+    "instance-remove-drbd-offline": true,
 
     "instance-export": true,
     "instance-failover": true,
+    "instance-grow-disk": true,
     "instance-import": true,
     "instance-info": true,
     "instance-list": true,
     "instance-rename": true,
     "instance-shutdown": true,
 
+    "job-list": true,
+
     "# cron/ganeti-watcher should be disabled for these tests": null,
     "instance-automatic-restart": false,
     "instance-consecutive-failures": false,
index bd3d737..1727098 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2007, 2010, 2011 Google Inc.
+# Copyright (C) 2007, 2010, 2011, 2012 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
@@ -58,6 +58,20 @@ def _CheckFileOnAllNodes(filename, content):
     AssertEqual(qa_utils.GetCommandOutput(node["primary"], cmd), content)
 
 
+# data for testing failures due to bad keys/values for disk parameters
+_FAIL_PARAMS = ["nonexistent:resync-rate=1",
+                "drbd:nonexistent=1",
+                "drbd:resync-rate=invalid",
+                ]
+
+
+def TestClusterInitDisk():
+  """gnt-cluster init -D"""
+  name = qa_config.get("name")
+  for param in _FAIL_PARAMS:
+    AssertCommand(["gnt-cluster", "init", "-D", param, name], fail=True)
+
+
 def TestClusterInit(rapi_user, rapi_secret):
   """gnt-cluster init"""
   master = qa_config.GetMasterNode()
@@ -80,10 +94,19 @@ def TestClusterInit(rapi_user, rapi_secret):
     fh.close()
 
   # Initialize cluster
-  cmd = ["gnt-cluster", "init"]
+  cmd = [
+    "gnt-cluster", "init",
+    "--primary-ip-version=%d" % qa_config.get("primary_ip_version", 4),
+    "--enabled-hypervisors=%s" % ",".join(qa_config.GetEnabledHypervisors()),
+    ]
 
-  cmd.append("--primary-ip-version=%d" %
-             qa_config.get("primary_ip_version", 4))
+  for spec_type in ("mem-size", "disk-size", "disk-count", "cpu-count",
+                    "nic-count"):
+    for spec_val in ("min", "max", "std"):
+      spec = qa_config.get("ispec_%s_%s" %
+                           (spec_type.replace('-', '_'), spec_val), None)
+      if spec:
+        cmd.append("--specs-%s=%s=%d" % (spec_type, spec_val, spec))
 
   if master.get("secondary", None):
     cmd.append("--secondary-ip=%s" % master["secondary"])
@@ -93,15 +116,11 @@ def TestClusterInit(rapi_user, rapi_secret):
     cmd.append("--bridge=%s" % bridge)
     cmd.append("--master-netdev=%s" % bridge)
 
-  htype = qa_config.get("enabled-hypervisors", None)
-  if htype:
-    cmd.append("--enabled-hypervisors=%s" % htype)
-
   cmd.append(qa_config.get("name"))
-
   AssertCommand(cmd)
 
   cmd = ["gnt-cluster", "modify"]
+
   # hypervisor parameter modifications
   hvp = qa_config.get("hypervisor-parameters", {})
   for k, v in hvp.items():
@@ -183,7 +202,7 @@ def TestClusterEpo():
 
   # Assert that OOB is unavailable for all nodes
   result_output = GetCommandOutput(master["primary"],
-                                   "gnt-node list --verbose --no-header -o"
+                                   "gnt-node list --verbose --no-headers -o"
                                    " powered")
   AssertEqual(compat.all(powered == "(unavail)"
                          for powered in result_output.splitlines()), True)
@@ -201,8 +220,9 @@ def TestClusterEpo():
 
   # All instances should have been stopped now
   result_output = GetCommandOutput(master["primary"],
-                                   "gnt-instance list --no-header -o status")
-  AssertEqual(compat.all(status == "ADMIN_down"
+                                   "gnt-instance list --no-headers -o status")
+  # ERROR_down because the instance is stopped but not recorded as such
+  AssertEqual(compat.all(status == "ERROR_down"
                          for status in result_output.splitlines()), True)
 
   # Now start everything again
@@ -210,7 +230,7 @@ def TestClusterEpo():
 
   # All instances should have been started now
   result_output = GetCommandOutput(master["primary"],
-                                   "gnt-instance list --no-header -o status")
+                                   "gnt-instance list --no-headers -o status")
   AssertEqual(compat.all(status == "running"
                          for status in result_output.splitlines()), True)
 
@@ -226,6 +246,14 @@ def TestJobqueue():
   AssertCommand(["gnt-debug", "test-jobqueue"])
 
 
+def TestDelay(node):
+  """gnt-debug delay"""
+  AssertCommand(["gnt-debug", "delay", "1"])
+  AssertCommand(["gnt-debug", "delay", "--no-master", "1"])
+  AssertCommand(["gnt-debug", "delay", "--no-master",
+                 "-n", node["primary"], "1"])
+
+
 def TestClusterReservedLvs():
   """gnt-cluster reserved lvs"""
   for fail, cmd in [
@@ -246,15 +274,32 @@ def TestClusterReservedLvs():
     AssertCommand(cmd, fail=fail)
 
 
+def TestClusterModifyEmpty():
+  """gnt-cluster modify"""
+  AssertCommand(["gnt-cluster", "modify"], fail=True)
+
+
+def TestClusterModifyDisk():
+  """gnt-cluster modify -D"""
+  for param in _FAIL_PARAMS:
+    AssertCommand(["gnt-cluster", "modify", "-D", param], fail=True)
+
+
 def TestClusterModifyBe():
   """gnt-cluster modify -B"""
   for fail, cmd in [
-    # mem
-    (False, ["gnt-cluster", "modify", "-B", "memory=256"]),
-    (False, ["sh", "-c", "gnt-cluster info|grep '^ *memory: 256$'"]),
-    (True, ["gnt-cluster", "modify", "-B", "memory=a"]),
-    (False, ["gnt-cluster", "modify", "-B", "memory=128"]),
-    (False, ["sh", "-c", "gnt-cluster info|grep '^ *memory: 128$'"]),
+    # max/min mem
+    (False, ["gnt-cluster", "modify", "-B", "maxmem=256"]),
+    (False, ["sh", "-c", "gnt-cluster info|grep '^ *maxmem: 256$'"]),
+    (False, ["gnt-cluster", "modify", "-B", "minmem=256"]),
+    (False, ["sh", "-c", "gnt-cluster info|grep '^ *minmem: 256$'"]),
+    (True, ["gnt-cluster", "modify", "-B", "maxmem=a"]),
+    (False, ["sh", "-c", "gnt-cluster info|grep '^ *maxmem: 256$'"]),
+    (True, ["gnt-cluster", "modify", "-B", "minmem=a"]),
+    (False, ["sh", "-c", "gnt-cluster info|grep '^ *minmem: 256$'"]),
+    (False, ["gnt-cluster", "modify", "-B", "maxmem=128,minmem=128"]),
+    (False, ["sh", "-c", "gnt-cluster info|grep '^ *maxmem: 128$'"]),
+    (False, ["sh", "-c", "gnt-cluster info|grep '^ *minmem: 128$'"]),
     # vcpus
     (False, ["gnt-cluster", "modify", "-B", "vcpus=4"]),
     (False, ["sh", "-c", "gnt-cluster info|grep '^ *vcpus: 4$'"]),
@@ -388,6 +433,8 @@ def TestClusterBurnin():
       # Run burnin
       cmd = [script,
              "--os=%s" % qa_config.get("os"),
+             "--minmem-size=%s" % qa_config.get(constants.BE_MINMEM),
+             "--maxmem-size=%s" % qa_config.get(constants.BE_MAXMEM),
              "--disk-size=%s" % ",".join(qa_config.get("disk")),
              "--disk-growth=%s" % ",".join(qa_config.get("disk-growth")),
              "--disk-template=%s" % disk_template]
index e058a71..a99dac9 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2007, 2011 Google Inc.
+# Copyright (C) 2007, 2011, 2012 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
@@ -23,7 +23,9 @@
 
 """
 
+import os
 
+from ganeti import constants
 from ganeti import utils
 from ganeti import serializer
 from ganeti import compat
@@ -31,6 +33,10 @@ from ganeti import compat
 import qa_error
 
 
+_INSTANCE_CHECK_KEY = "instance-check"
+_ENABLED_HV_KEY = "enabled-hypervisors"
+
+
 cfg = None
 options = None
 
@@ -55,27 +61,143 @@ def Validate():
     raise qa_error.Error("Config options 'disk' and 'disk-growth' must have"
                          " the same number of items")
 
+  check = GetInstanceCheckScript()
+  if check:
+    try:
+      os.stat(check)
+    except EnvironmentError, err:
+      raise qa_error.Error("Can't find instance check script '%s': %s" %
+                           (check, err))
+
+  enabled_hv = frozenset(GetEnabledHypervisors())
+  if not enabled_hv:
+    raise qa_error.Error("No hypervisor is enabled")
+
+  difference = enabled_hv - constants.HYPER_TYPES
+  if difference:
+    raise qa_error.Error("Unknown hypervisor(s) enabled: %s" %
+                         utils.CommaJoin(difference))
+
 
 def get(name, default=None):
   return cfg.get(name, default)
 
 
-def TestEnabled(tests):
+class Either:
+  def __init__(self, tests):
+    """Initializes this class.
+
+    @type tests: list or string
+    @param tests: List of test names
+    @see: L{TestEnabled} for details
+
+    """
+    self.tests = tests
+
+
+def _MakeSequence(value):
+  """Make sequence of single argument.
+
+  If the single argument is not already a list or tuple, a list with the
+  argument as a single item is returned.
+
+  """
+  if isinstance(value, (list, tuple)):
+    return value
+  else:
+    return [value]
+
+
+def _TestEnabledInner(check_fn, names, fn):
+  """Evaluate test conditions.
+
+  @type check_fn: callable
+  @param check_fn: Callback to check whether a test is enabled
+  @type names: sequence or string
+  @param names: Test name(s)
+  @type fn: callable
+  @param fn: Aggregation function
+  @rtype: bool
+  @return: Whether test is enabled
+
+  """
+  names = _MakeSequence(names)
+
+  result = []
+
+  for name in names:
+    if isinstance(name, Either):
+      value = _TestEnabledInner(check_fn, name.tests, compat.any)
+    elif isinstance(name, (list, tuple)):
+      value = _TestEnabledInner(check_fn, name, compat.all)
+    else:
+      value = check_fn(name)
+
+    result.append(value)
+
+  return fn(result)
+
+
+def TestEnabled(tests, _cfg=None):
   """Returns True if the given tests are enabled.
 
-  @param tests: a single test, or a list of tests to check
+  @param tests: A single test as a string, or a list of tests to check; can
+    contain L{Either} for OR conditions, AND is default
 
   """
-  if isinstance(tests, basestring):
-    tests = [tests]
+  if _cfg is None:
+    _cfg = cfg
 
   # Get settings for all tests
-  all_tests = cfg.get("tests", {})
+  cfg_tests = _cfg.get("tests", {})
 
   # Get default setting
-  default = all_tests.get("default", True)
+  default = cfg_tests.get("default", True)
+
+  return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
+                           tests, compat.all)
+
+
+def GetInstanceCheckScript():
+  """Returns path to instance check script or C{None}.
+
+  """
+  return cfg.get(_INSTANCE_CHECK_KEY, None)
+
+
+def GetEnabledHypervisors():
+  """Returns list of enabled hypervisors.
+
+  @rtype: list
 
-  return compat.all(all_tests.get(name, default) for name in tests)
+  """
+  try:
+    value = cfg[_ENABLED_HV_KEY]
+  except KeyError:
+    return [constants.DEFAULT_ENABLED_HYPERVISOR]
+  else:
+    if isinstance(value, basestring):
+      # The configuration key ("enabled-hypervisors") implies there can be
+      # multiple values. Multiple hypervisors are comma-separated on the
+      # command line option to "gnt-cluster init", so we need to handle them
+      # equally here.
+      return value.split(",")
+    else:
+      return value
+
+
+def GetDefaultHypervisor():
+  """Returns the default hypervisor to be used.
+
+  """
+  return GetEnabledHypervisors()[0]
+
+
+def GetInstanceNicMac(inst, default=None):
+  """Returns MAC address for instance's network interface.
+
+  """
+  return inst.get("nic.mac/0", default)
 
 
 def GetMasterNode():
index e09c2f8..2adc620 100644 (file)
@@ -90,11 +90,19 @@ def TestGroupModify():
 
   AssertCommand(["gnt-group", "add", group1])
 
+  std_defaults = constants.IPOLICY_DEFAULTS[constants.ISPECS_STD]
+  min_v = std_defaults[constants.ISPEC_MEM_SIZE] * 10
+  max_v = min_v * 10
+
   try:
     AssertCommand(["gnt-group", "modify", "--alloc-policy", "unallocable",
                    "--node-parameters", "oob_program=/bin/false", group1])
     AssertCommand(["gnt-group", "modify",
                    "--alloc-policy", "notvalid", group1], fail=True)
+    AssertCommand(["gnt-group", "modify", "--specs-mem-size",
+                   "min=%s,max=%s,std=0" % (min_v, max_v), group1], fail=True)
+    AssertCommand(["gnt-group", "modify", "--specs-mem-size",
+                   "min=%s,max=%s" % (min_v, max_v), group1])
   finally:
     AssertCommand(["gnt-group", "remove", group1])
 
index 45ee521..0ea9f2b 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2007, 2011 Google Inc.
+# Copyright (C) 2007, 2011, 2012 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
@@ -35,16 +35,30 @@ import qa_utils
 import qa_error
 
 from qa_utils import AssertIn, AssertCommand, AssertEqual
+from qa_utils import InstanceCheck, INST_DOWN, INST_UP, FIRST_ARG, RETURN_VALUE
 
 
 def _GetDiskStatePath(disk):
   return "/sys/block/%s/device/state" % disk
 
 
-def _GetGenericAddParameters():
-  params = ["-B", "%s=%s" % (constants.BE_MEMORY, qa_config.get("mem"))]
+def _GetGenericAddParameters(inst, force_mac=None):
+  params = ["-B"]
+  params.append("%s=%s,%s=%s" % (constants.BE_MINMEM,
+                                 qa_config.get(constants.BE_MINMEM),
+                                 constants.BE_MAXMEM,
+                                 qa_config.get(constants.BE_MAXMEM)))
   for idx, size in enumerate(qa_config.get("disk")):
     params.extend(["--disk", "%s:size=%s" % (idx, size)])
+
+  # Set static MAC address if configured
+  if force_mac:
+    nic0_mac = force_mac
+  else:
+    nic0_mac = qa_config.GetInstanceNicMac(inst)
+  if nic0_mac:
+    params.extend(["--net", "0:mac=%s" % nic0_mac])
+
   return params
 
 
@@ -55,7 +69,7 @@ def _DiskTest(node, disk_template):
             "--os-type=%s" % qa_config.get("os"),
             "--disk-template=%s" % disk_template,
             "--node=%s" % node] +
-           _GetGenericAddParameters())
+           _GetGenericAddParameters(instance))
     cmd.append(instance["name"])
 
     AssertCommand(cmd)
@@ -68,17 +82,20 @@ def _DiskTest(node, disk_template):
     raise
 
 
+@InstanceCheck(None, INST_UP, RETURN_VALUE)
 def TestInstanceAddWithPlainDisk(node):
   """gnt-instance add -t plain"""
   return _DiskTest(node["primary"], "plain")
 
 
+@InstanceCheck(None, INST_UP, RETURN_VALUE)
 def TestInstanceAddWithDrbdDisk(node, node2):
   """gnt-instance add -t drbd"""
   return _DiskTest("%s:%s" % (node["primary"], node2["primary"]),
                    "drbd")
 
 
+@InstanceCheck(None, INST_DOWN, FIRST_ARG)
 def TestInstanceRemove(instance):
   """gnt-instance remove"""
   AssertCommand(["gnt-instance", "remove", "-f", instance["name"]])
@@ -86,16 +103,19 @@ def TestInstanceRemove(instance):
   qa_config.ReleaseInstance(instance)
 
 
+@InstanceCheck(INST_DOWN, INST_UP, FIRST_ARG)
 def TestInstanceStartup(instance):
   """gnt-instance startup"""
   AssertCommand(["gnt-instance", "startup", instance["name"]])
 
 
+@InstanceCheck(INST_UP, INST_DOWN, FIRST_ARG)
 def TestInstanceShutdown(instance):
   """gnt-instance shutdown"""
   AssertCommand(["gnt-instance", "shutdown", instance["name"]])
 
 
+@InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
 def TestInstanceReboot(instance):
   """gnt-instance reboot"""
   options = qa_config.get("options", {})
@@ -105,15 +125,17 @@ def TestInstanceReboot(instance):
     AssertCommand(["gnt-instance", "reboot", "--type=%s" % rtype, name])
 
   AssertCommand(["gnt-instance", "shutdown", name])
+  qa_utils.RunInstanceCheck(instance, False)
   AssertCommand(["gnt-instance", "reboot", name])
 
   master = qa_config.GetMasterNode()
-  cmd = ["gnt-instance", "list", "--no-header", "-o", "status", name]
+  cmd = ["gnt-instance", "list", "--no-headers", "-o", "status", name]
   result_output = qa_utils.GetCommandOutput(master["primary"],
                                             utils.ShellQuoteArgs(cmd))
   AssertEqual(result_output.strip(), constants.INSTST_RUNNING)
 
 
+@InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
 def TestInstanceReinstall(instance):
   """gnt-instance reinstall"""
   AssertCommand(["gnt-instance", "reinstall", "-f", instance["name"]])
@@ -143,13 +165,17 @@ def _CheckSsconfInstanceList(instance):
            _ReadSsconfInstanceList())
 
 
-def TestInstanceRename(rename_source, rename_target):
-  """gnt-instance rename"""
-  _CheckSsconfInstanceList(rename_source)
-  AssertCommand(["gnt-instance", "rename", rename_source, rename_target])
-  _CheckSsconfInstanceList(rename_target)
-  AssertCommand(["gnt-instance", "rename", rename_target, rename_source])
+@InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
+def TestInstanceRenameAndBack(rename_source, rename_target):
+  """gnt-instance rename
+
+  This must leave the instance with the original name, not the target
+  name.
+
+  """
   _CheckSsconfInstanceList(rename_source)
+
+  # first do a rename to a different actual name, expecting it to fail
   qa_utils.AddToEtcHosts(["meeeeh-not-exists", rename_target])
   try:
     AssertCommand(["gnt-instance", "rename", rename_source, rename_target],
@@ -157,32 +183,72 @@ def TestInstanceRename(rename_source, rename_target):
     _CheckSsconfInstanceList(rename_source)
   finally:
     qa_utils.RemoveFromEtcHosts(["meeeeh-not-exists", rename_target])
+
+  # and now rename instance to rename_target...
   AssertCommand(["gnt-instance", "rename", rename_source, rename_target])
   _CheckSsconfInstanceList(rename_target)
+  qa_utils.RunInstanceCheck(rename_source, False)
+  qa_utils.RunInstanceCheck(rename_target, False)
+
+  # and back
+  AssertCommand(["gnt-instance", "rename", rename_target, rename_source])
+  _CheckSsconfInstanceList(rename_source)
+  qa_utils.RunInstanceCheck(rename_target, False)
 
 
+@InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
 def TestInstanceFailover(instance):
   """gnt-instance failover"""
   cmd = ["gnt-instance", "failover", "--force", instance["name"]]
+
   # failover ...
   AssertCommand(cmd)
+  qa_utils.RunInstanceCheck(instance, True)
+
   # ... and back
   AssertCommand(cmd)
 
 
+@InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
 def TestInstanceMigrate(instance):
   """gnt-instance migrate"""
   cmd = ["gnt-instance", "migrate", "--force", instance["name"]]
+
   # migrate ...
   AssertCommand(cmd)
+  qa_utils.RunInstanceCheck(instance, True)
+
   # ... and back
   AssertCommand(cmd)
+
+  # TODO: Split into multiple tests
   AssertCommand(["gnt-instance", "shutdown", instance["name"]])
+  qa_utils.RunInstanceCheck(instance, False)
   AssertCommand(cmd, fail=True)
   AssertCommand(["gnt-instance", "migrate", "--force", "--allow-failover",
                  instance["name"]])
   AssertCommand(["gnt-instance", "start", instance["name"]])
   AssertCommand(cmd)
+  qa_utils.RunInstanceCheck(instance, True)
+
+  AssertCommand(["gnt-instance", "modify", "-B",
+                 ("%s=%s" %
+                  (constants.BE_ALWAYS_FAILOVER, constants.VALUE_TRUE)),
+                 instance["name"]])
+
+  AssertCommand(cmd, fail=True)
+  qa_utils.RunInstanceCheck(instance, True)
+  AssertCommand(["gnt-instance", "migrate", "--force", "--allow-failover",
+                 instance["name"]])
+
+  # TODO: Verify whether the default value is restored here (not hardcoded)
+  AssertCommand(["gnt-instance", "modify", "-B",
+                 ("%s=%s" %
+                  (constants.BE_ALWAYS_FAILOVER, constants.VALUE_FALSE)),
+                 instance["name"]])
+
+  AssertCommand(cmd)
+  qa_utils.RunInstanceCheck(instance, True)
 
 
 def TestInstanceInfo(instance):
@@ -190,42 +256,77 @@ def TestInstanceInfo(instance):
   AssertCommand(["gnt-instance", "info", instance["name"]])
 
 
+@InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
 def TestInstanceModify(instance):
   """gnt-instance modify"""
+  default_hv = qa_config.GetDefaultHypervisor()
+
   # Assume /sbin/init exists on all systems
   test_kernel = "/sbin/init"
   test_initrd = test_kernel
 
-  orig_memory = qa_config.get("mem")
+  orig_maxmem = qa_config.get(constants.BE_MAXMEM)
+  orig_minmem = qa_config.get(constants.BE_MINMEM)
   #orig_bridge = qa_config.get("bridge", "xen-br0")
+
   args = [
-    ["-B", "%s=128" % constants.BE_MEMORY],
-    ["-B", "%s=%s" % (constants.BE_MEMORY, orig_memory)],
+    ["-B", "%s=128" % constants.BE_MINMEM],
+    ["-B", "%s=128" % constants.BE_MAXMEM],
+    ["-B", "%s=%s,%s=%s" % (constants.BE_MINMEM, orig_minmem,
+                            constants.BE_MAXMEM, orig_maxmem)],
     ["-B", "%s=2" % constants.BE_VCPUS],
     ["-B", "%s=1" % constants.BE_VCPUS],
     ["-B", "%s=%s" % (constants.BE_VCPUS, constants.VALUE_DEFAULT)],
+    ["-B", "%s=%s" % (constants.BE_ALWAYS_FAILOVER, constants.VALUE_TRUE)],
+    ["-B", "%s=%s" % (constants.BE_ALWAYS_FAILOVER, constants.VALUE_DEFAULT)],
 
     ["-H", "%s=%s" % (constants.HV_KERNEL_PATH, test_kernel)],
     ["-H", "%s=%s" % (constants.HV_KERNEL_PATH, constants.VALUE_DEFAULT)],
-    ["-H", "%s=%s" % (constants.HV_INITRD_PATH, test_initrd)],
-    ["-H", "no_%s" % (constants.HV_INITRD_PATH, )],
-    ["-H", "%s=%s" % (constants.HV_INITRD_PATH, constants.VALUE_DEFAULT)],
 
     # TODO: bridge tests
     #["--bridge", "xen-br1"],
     #["--bridge", orig_bridge],
-
-    # TODO: Do these tests only with xen-hvm
-    #["-H", "%s=acn" % constants.HV_BOOT_ORDER],
-    #["-H", "%s=%s" % (constants.HV_BOOT_ORDER, constants.VALUE_DEFAULT)],
     ]
+
+  if default_hv == constants.HT_XEN_PVM:
+    args.extend([
+      ["-H", "%s=%s" % (constants.HV_INITRD_PATH, test_initrd)],
+      ["-H", "no_%s" % (constants.HV_INITRD_PATH, )],
+      ["-H", "%s=%s" % (constants.HV_INITRD_PATH, constants.VALUE_DEFAULT)],
+      ])
+  elif default_hv == constants.HT_XEN_HVM:
+    args.extend([
+      ["-H", "%s=acn" % constants.HV_BOOT_ORDER],
+      ["-H", "%s=%s" % (constants.HV_BOOT_ORDER, constants.VALUE_DEFAULT)],
+      ])
+
   for alist in args:
     AssertCommand(["gnt-instance", "modify"] + alist + [instance["name"]])
 
   # check no-modify
   AssertCommand(["gnt-instance", "modify", instance["name"]], fail=True)
 
+  # Marking offline/online while instance is running must fail
+  for arg in ["--online", "--offline"]:
+    AssertCommand(["gnt-instance", "modify", arg, instance["name"]], fail=True)
+
 
+@InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
+def TestInstanceStoppedModify(instance):
+  """gnt-instance modify (stopped instance)"""
+  name = instance["name"]
+
+  # Instance was not marked offline; try marking it online once more
+  AssertCommand(["gnt-instance", "modify", "--online", name])
+
+  # Mark instance as offline
+  AssertCommand(["gnt-instance", "modify", "--offline", name])
+
+  # And online again
+  AssertCommand(["gnt-instance", "modify", "--online", name])
+
+
+@InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
 def TestInstanceConvertDisk(instance, snode):
   """gnt-instance modify -t"""
   name = instance["name"]
@@ -234,6 +335,29 @@ def TestInstanceConvertDisk(instance, snode):
                  "-n", snode["primary"], name])
 
 
+@InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
+def TestInstanceGrowDisk(instance):
+  """gnt-instance grow-disk"""
+  name = instance["name"]
+  all_size = qa_config.get("disk")
+  all_grow = qa_config.get("disk-growth")
+  if not all_grow:
+    # missing disk sizes but instance grow disk has been enabled,
+    # let's set fixed/nomimal growth
+    all_grow = ["128M" for _ in all_size]
+  for idx, (size, grow) in enumerate(zip(all_size, all_grow)):
+    # succeed in grow by amount
+    AssertCommand(["gnt-instance", "grow-disk", name, str(idx), grow])
+    # fail in grow to the old size
+    AssertCommand(["gnt-instance", "grow-disk", "--absolute", name, str(idx),
+                   size], fail=True)
+    # succeed to grow to old size + 2 * growth
+    int_size = utils.ParseUnit(size)
+    int_grow = utils.ParseUnit(grow)
+    AssertCommand(["gnt-instance", "grow-disk", "--absolute", name, str(idx),
+                   str(int_size + 2 * int_grow)])
+
+
 def TestInstanceList():
   """gnt-instance list"""
   qa_utils.GenericQueryTest("gnt-instance", query.INSTANCE_FIELDS.keys())
@@ -244,11 +368,13 @@ def TestInstanceListFields():
   qa_utils.GenericQueryFieldsTest("gnt-instance", query.INSTANCE_FIELDS.keys())
 
 
+@InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
 def TestInstanceConsole(instance):
   """gnt-instance console"""
   AssertCommand(["gnt-instance", "console", "--show-cmd", instance["name"]])
 
 
+@InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
 def TestReplaceDisks(instance, pnode, snode, othernode):
   """gnt-instance replace-disks"""
   # pylint: disable=W0613
@@ -277,6 +403,7 @@ def TestReplaceDisks(instance, pnode, snode, othernode):
   AssertCommand(["gnt-instance", "start", instance["name"]])
 
 
+@InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
 def TestInstanceExport(instance, node):
   """gnt-backup export -n ..."""
   name = instance["name"]
@@ -284,27 +411,29 @@ def TestInstanceExport(instance, node):
   return qa_utils.ResolveInstanceName(name)
 
 
+@InstanceCheck(None, INST_DOWN, FIRST_ARG)
 def TestInstanceExportWithRemove(instance, node):
   """gnt-backup export --remove-instance"""
   AssertCommand(["gnt-backup", "export", "-n", node["primary"],
                  "--remove-instance", instance["name"]])
 
 
+@InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
 def TestInstanceExportNoTarget(instance):
   """gnt-backup export (without target node, should fail)"""
   AssertCommand(["gnt-backup", "export", instance["name"]], fail=True)
 
 
-def TestInstanceImport(node, newinst, expnode, name):
+@InstanceCheck(None, INST_DOWN, FIRST_ARG)
+def TestInstanceImport(newinst, node, expnode, name):
   """gnt-backup import"""
   cmd = (["gnt-backup", "import",
           "--disk-template=plain",
           "--no-ip-check",
-          "--net", "0:mac=generate",
           "--src-node=%s" % expnode["primary"],
           "--src-dir=%s/%s" % (constants.EXPORT_DIR, name),
           "--node=%s" % node["primary"]] +
-         _GetGenericAddParameters())
+         _GetGenericAddParameters(newinst, force_mac=constants.VALUE_GENERATE))
   cmd.append(newinst["name"])
   AssertCommand(cmd)
 
@@ -313,6 +442,14 @@ def TestBackupList(expnode):
   """gnt-backup list"""
   AssertCommand(["gnt-backup", "list", "--node=%s" % expnode["primary"]])
 
+  qa_utils.GenericQueryTest("gnt-backup", query.EXPORT_FIELDS.keys(),
+                            namefield=None, test_unknown=False)
+
+
+def TestBackupListFields():
+  """gnt-backup list-fields"""
+  qa_utils.GenericQueryFieldsTest("gnt-backup", query.EXPORT_FIELDS.keys())
+
 
 def _TestInstanceDiskFailure(instance, node, node2, onmaster):
   """Testing disk failure."""
@@ -385,7 +522,7 @@ def _TestInstanceDiskFailure(instance, node, node2, onmaster):
     AssertCommand(" && ".join(cmds), node=[node2, node][int(onmaster)])
 
     print qa_utils.FormatInfo("Write to disks and give some time to notice"
-                              " to notice the problem")
+                              " the problem")
     cmds = []
     for disk in devpath:
       cmds.append(sq(["dd", "count=1", "bs=512", "conv=notrunc",
diff --git a/qa/qa_job.py b/qa/qa_job.py
new file mode 100644 (file)
index 0000000..0fc410e
--- /dev/null
@@ -0,0 +1,39 @@
+#
+#
+
+# Copyright (C) 2012 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.
+
+
+"""Job-related QA tests.
+
+"""
+
+from ganeti import query
+
+import qa_utils
+
+
+def TestJobList():
+  """gnt-job list"""
+  qa_utils.GenericQueryTest("gnt-job", query.JOB_FIELDS.keys(),
+                            namefield="id", test_unknown=False)
+
+
+def TestJobListFields():
+  """gnt-node list-fields"""
+  qa_utils.GenericQueryFieldsTest("gnt-job", query.JOB_FIELDS.keys())
index 8b2d406..647af19 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2007, 2011 Google Inc.
+# Copyright (C) 2007, 2011, 2012 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
@@ -58,6 +58,12 @@ def _NodeRemove(node):
   node["_added"] = False
 
 
+def MakeNodeOffline(node, value):
+  """gnt-node modify --offline=value"""
+  # value in ["yes", "no"]
+  AssertCommand(["gnt-node", "modify", "--offline", value, node["primary"]])
+
+
 def TestNodeAddAll():
   """Adding all nodes to cluster."""
   master = qa_config.GetMasterNode()
@@ -209,6 +215,10 @@ def TestNodeModify(node):
   AssertCommand(["gnt-node", "modify", "--master-candidate=yes",
                  "--auto-promote", node["primary"]])
 
+  # Test setting secondary IP address
+  AssertCommand(["gnt-node", "modify", "--secondary-ip=%s" % node["secondary"],
+                 node["primary"]])
+
 
 def _CreateOobScriptStructure():
   """Create a simple OOB handling script and its structure."""
@@ -407,3 +417,8 @@ def TestNodeList():
 def TestNodeListFields():
   """gnt-node list-fields"""
   qa_utils.GenericQueryFieldsTest("gnt-node", query.NODE_FIELDS.keys())
+
+
+def TestNodeListDrbd(node):
+  """gnt-node list-drbd"""
+  AssertCommand(["gnt-node", "list-drbd", node["primary"]])
index a4c5921..25778c7 100644 (file)
@@ -1,6 +1,7 @@
 #
+#
 
-# Copyright (C) 2007, 2008, 2009, 2010, 2011 Google Inc.
+# Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012 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
@@ -24,6 +25,8 @@
 
 import tempfile
 import random
+import re
+import itertools
 
 from ganeti import utils
 from ganeti import constants
@@ -43,6 +46,7 @@ import qa_utils
 import qa_error
 
 from qa_utils import (AssertEqual, AssertIn, AssertMatch, StartLocalCommand)
+from qa_utils import InstanceCheck, INST_DOWN, INST_UP, FIRST_ARG
 
 
 _rapi_ca = None
@@ -232,6 +236,13 @@ def TestRapiQuery():
   rnd = random.Random(7818)
 
   for what in constants.QR_VIA_RAPI:
+    if what == constants.QR_JOB:
+      namefield = "id"
+    elif what == constants.QR_EXPORT:
+      namefield = "export"
+    else:
+      namefield = "name"
+
     all_fields = query.ALL_FIELDS[what].keys()
     rnd.shuffle(all_fields)
 
@@ -241,7 +252,7 @@ def TestRapiQuery():
     AssertEqual(len(qresult.fields), len(all_fields))
 
     # One field
-    result = _rapi_client.QueryFields(what, fields=["name"])
+    result = _rapi_client.QueryFields(what, fields=[namefield])
     qresult = objects.QueryFieldsResponse.FromDict(result)
     AssertEqual(len(qresult.fields), 1)
 
@@ -286,24 +297,25 @@ def TestRapiQuery():
       ("/2/query/%s?fields=%s" % (what, ",".join(all_fields)),
        compat.partial(_Check, all_fields), "GET", None),
 
-      ("/2/query/%s?fields=name" % what,
-       compat.partial(_Check, ["name"]), "GET", None),
+      ("/2/query/%s?fields=%s" % (what, namefield),
+       compat.partial(_Check, [namefield]), "GET", None),
 
       # Note the spaces
-      ("/2/query/%s?fields=name,%%20name%%09,name%%20" % what,
-       compat.partial(_Check, ["name"] * 3), "GET", None),
+      ("/2/query/%s?fields=%s,%%20%s%%09,%s%%20" %
+       (what, namefield, namefield, namefield),
+       compat.partial(_Check, [namefield] * 3), "GET", None),
 
       # PUT with fields in query
-      ("/2/query/%s?fields=name" % what,
-       compat.partial(_Check, ["name"]), "PUT", {}),
+      ("/2/query/%s?fields=%s" % (what, namefield),
+       compat.partial(_Check, [namefield]), "PUT", {}),
 
       # Fields in body
       ("/2/query/%s" % what, compat.partial(_Check, all_fields), "PUT", {
          "fields": all_fields,
          }),
 
-      ("/2/query/%s" % what, compat.partial(_Check, ["name"] * 4), "PUT", {
-         "fields": ["name"] * 4,
+      ("/2/query/%s" % what, compat.partial(_Check, [namefield] * 4), "PUT", {
+         "fields": [namefield] * 4,
          }),
       ])
 
@@ -312,7 +324,7 @@ def TestRapiQuery():
         # With filter
         ("/2/query/%s" % what, compat.partial(_Check, all_fields), "PUT", {
            "fields": all_fields,
-           "filter": [qlang.OP_TRUE, "name"],
+           "filter": [qlang.OP_TRUE, namefield],
            }),
         ])
 
@@ -340,6 +352,7 @@ def TestRapiQuery():
         ])
 
 
+@InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
 def TestInstance(instance):
   """Testing getting instance(s) info via remote API.
 
@@ -405,6 +418,18 @@ def TestNode(node):
     ])
 
 
+def _FilterTags(seq):
+  """Removes unwanted tags from a sequence.
+
+  """
+  ignore_re = qa_config.get("ignore-tags-re", None)
+
+  if ignore_re:
+    return itertools.ifilterfalse(re.compile(ignore_re).match, seq)
+  else:
+    return seq
+
+
 def TestTags(kind, name, tags):
   """Tests .../tags resources.
 
@@ -421,7 +446,7 @@ def TestTags(kind, name, tags):
     raise errors.ProgrammerError("Unknown tag kind")
 
   def _VerifyTags(data):
-    AssertEqual(sorted(tags), sorted(data))
+    AssertEqual(sorted(tags), sorted(_FilterTags(data)))
 
   queryargs = "&".join("tag=%s" % i for i in tags)
 
@@ -526,13 +551,17 @@ def TestRapiInstanceAdd(node, use_client):
   """Test adding a new instance via RAPI"""
   instance = qa_config.AcquireInstance()
   try:
-    memory = utils.ParseUnit(qa_config.get("mem"))
     disk_sizes = [utils.ParseUnit(size) for size in qa_config.get("disk")]
     disks = [{"size": size} for size in disk_sizes]
-    nics = [{}]
+    nic0_mac = qa_config.GetInstanceNicMac(instance,
+                                           default=constants.VALUE_GENERATE)
+    nics = [{
+      constants.INIC_MAC: nic0_mac,
+      }]
 
     beparams = {
-      constants.BE_MEMORY: memory,
+      constants.BE_MAXMEM: utils.ParseUnit(qa_config.get(constants.BE_MAXMEM)),
+      constants.BE_MINMEM: utils.ParseUnit(qa_config.get(constants.BE_MINMEM)),
       }
 
     if use_client:
@@ -568,6 +597,7 @@ def TestRapiInstanceAdd(node, use_client):
     raise
 
 
+@InstanceCheck(None, INST_DOWN, FIRST_ARG)
 def TestRapiInstanceRemove(instance, use_client):
   """Test removing instance via RAPI"""
   if use_client:
@@ -582,42 +612,66 @@ def TestRapiInstanceRemove(instance, use_client):
   qa_config.ReleaseInstance(instance)
 
 
+@InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
 def TestRapiInstanceMigrate(instance):
   """Test migrating instance via RAPI"""
   # Move to secondary node
   _WaitForRapiJob(_rapi_client.MigrateInstance(instance["name"]))
+  qa_utils.RunInstanceCheck(instance, True)
   # And back to previous primary
   _WaitForRapiJob(_rapi_client.MigrateInstance(instance["name"]))
 
 
+@InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
 def TestRapiInstanceFailover(instance):
   """Test failing over instance via RAPI"""
   # Move to secondary node
   _WaitForRapiJob(_rapi_client.FailoverInstance(instance["name"]))
+  qa_utils.RunInstanceCheck(instance, True)
   # And back to previous primary
   _WaitForRapiJob(_rapi_client.FailoverInstance(instance["name"]))
 
 
+@InstanceCheck(INST_UP, INST_DOWN, FIRST_ARG)
 def TestRapiInstanceShutdown(instance):
   """Test stopping an instance via RAPI"""
   _WaitForRapiJob(_rapi_client.ShutdownInstance(instance["name"]))
 
 
+@InstanceCheck(INST_DOWN, INST_UP, FIRST_ARG)
 def TestRapiInstanceStartup(instance):
   """Test starting an instance via RAPI"""
   _WaitForRapiJob(_rapi_client.StartupInstance(instance["name"]))
 
 
-def TestRapiInstanceRename(rename_source, rename_target):
-  """Test renaming instance via RAPI"""
+@InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
+def TestRapiInstanceRenameAndBack(rename_source, rename_target):
+  """Test renaming instance via RAPI
+
+  This must leave the instance with the original name (in the
+  non-failure case).
+
+  """
   _WaitForRapiJob(_rapi_client.RenameInstance(rename_source, rename_target))
+  qa_utils.RunInstanceCheck(rename_source, False)
+  qa_utils.RunInstanceCheck(rename_target, False)
+  _WaitForRapiJob(_rapi_client.RenameInstance(rename_target, rename_source))
+  qa_utils.RunInstanceCheck(rename_target, False)
 
 
+@InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
 def TestRapiInstanceReinstall(instance):
   """Test reinstalling an instance via RAPI"""
   _WaitForRapiJob(_rapi_client.ReinstallInstance(instance["name"]))
+  # By default, the instance is started again
+  qa_utils.RunInstanceCheck(instance, True)
+
+  # Reinstall again without starting
+  _WaitForRapiJob(_rapi_client.ReinstallInstance(instance["name"],
+                                                 no_startup=True))
 
 
+@InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
 def TestRapiInstanceReplaceDisks(instance):
   """Test replacing instance disks via RAPI"""
   _WaitForRapiJob(_rapi_client.ReplaceInstanceDisks(instance["name"],
@@ -626,15 +680,14 @@ def TestRapiInstanceReplaceDisks(instance):
     mode=constants.REPLACE_DISK_SEC, disks="0"))
 
 
+@InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
 def TestRapiInstanceModify(instance):
   """Test modifying instance via RAPI"""
+  default_hv = qa_config.GetDefaultHypervisor()
+
   def _ModifyInstance(**kwargs):
     _WaitForRapiJob(_rapi_client.ModifyInstance(instance["name"], **kwargs))
 
-  _ModifyInstance(hvparams={
-    constants.HV_KERNEL_ARGS: "single",
-    })
-
   _ModifyInstance(beparams={
     constants.BE_VCPUS: 3,
     })
@@ -643,11 +696,23 @@ def TestRapiInstanceModify(instance):
     constants.BE_VCPUS: constants.VALUE_DEFAULT,
     })
 
-  _ModifyInstance(hvparams={
-    constants.HV_KERNEL_ARGS: constants.VALUE_DEFAULT,
-    })
-
-
+  if default_hv == constants.HT_XEN_PVM:
+    _ModifyInstance(hvparams={
+      constants.HV_KERNEL_ARGS: "single",
+      })
+    _ModifyInstance(hvparams={
+      constants.HV_KERNEL_ARGS: constants.VALUE_DEFAULT,
+      })
+  elif default_hv == constants.HT_XEN_HVM:
+    _ModifyInstance(hvparams={
+      constants.HV_BOOT_ORDER: "acn",
+      })
+    _ModifyInstance(hvparams={
+      constants.HV_BOOT_ORDER: constants.VALUE_DEFAULT,
+      })
+
+
+@InstanceCheck(INST_UP, INST_UP, FIRST_ARG)
 def TestRapiInstanceConsole(instance):
   """Test getting instance console information via RAPI"""
   result = _rapi_client.GetInstanceConsole(instance["name"])
@@ -656,6 +721,7 @@ def TestRapiInstanceConsole(instance):
   AssertEqual(console.instance, qa_utils.ResolveInstanceName(instance["name"]))
 
 
+@InstanceCheck(INST_DOWN, INST_DOWN, FIRST_ARG)
 def TestRapiStoppedInstanceConsole(instance):
   """Test getting stopped instance's console information via RAPI"""
   try:
@@ -712,4 +778,7 @@ def TestInterClusterInstanceMove(src_instance, dest_instance,
       si,
       ]
 
+    qa_utils.RunInstanceCheck(di, False)
     AssertEqual(StartLocalCommand(cmd).wait(), 0)
+    qa_utils.RunInstanceCheck(si, False)
+    qa_utils.RunInstanceCheck(di, True)
index a4350aa..5aa316d 100644 (file)
@@ -1,3 +1,6 @@
+#
+#
+
 # Copyright (C) 2007 Google Inc.
 #
 # This program is free software; you can redistribute it and/or modify
index 671a6f2..a91cc94 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2007, 2011 Google Inc.
+# Copyright (C) 2007, 2011, 2012 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
@@ -30,9 +30,15 @@ import subprocess
 import random
 import tempfile
 
+try:
+  import functools
+except ImportError, err:
+  raise ImportError("Python 2.5 or higher is required: %s" % err)
+
 from ganeti import utils
 from ganeti import compat
 from ganeti import constants
+from ganeti import ht
 
 import qa_config
 import qa_error
@@ -45,6 +51,16 @@ _RESET_SEQ = None
 
 _MULTIPLEXERS = {}
 
+#: Unique ID per QA run
+_RUN_UUID = utils.NewUUID()
+
+
+(INST_DOWN,
+ INST_UP) = range(500, 502)
+
+(FIRST_ARG,
+ RETURN_VALUE) = range(1000, 1002)
+
 
 def _SetupColours():
   """Initializes the colour constants.
@@ -117,6 +133,28 @@ def AssertMatch(string, pattern):
     raise qa_error.Error("%r doesn't match /%r/" % (string, pattern))
 
 
+def _GetName(entity, key):
+  """Tries to get name of an entity.
+
+  @type entity: string or dict
+  @type key: string
+  @param key: Dictionary key containing name
+
+  """
+  if isinstance(entity, basestring):
+    result = entity
+  elif isinstance(entity, dict):
+    result = entity[key]
+  else:
+    raise qa_error.Error("Expected string or dictionary, got %s: %s" %
+                         (type(entity), entity))
+
+  if not ht.TNonEmptyString(result):
+    raise Exception("Invalid name '%s'" % result)
+
+  return result
+
+
 def AssertCommand(cmd, fail=False, node=None):
   """Checks that a remote command succeeds.
 
@@ -132,10 +170,7 @@ def AssertCommand(cmd, fail=False, node=None):
   if node is None:
     node = qa_config.GetMasterNode()
 
-  if isinstance(node, basestring):
-    nodename = node
-  else:
-    nodename = node["primary"]
+  nodename = _GetName(node, "primary")
 
   if isinstance(cmd, basestring):
     cmdstr = cmd
@@ -156,7 +191,7 @@ def AssertCommand(cmd, fail=False, node=None):
   return rcode
 
 
-def GetSSHCommand(node, cmd, strict=True, opts=None, tty=True):
+def GetSSHCommand(node, cmd, strict=True, opts=None, tty=None):
   """Builds SSH command to be executed.
 
   @type node: string
@@ -168,11 +203,14 @@ def GetSSHCommand(node, cmd, strict=True, opts=None, tty=True):
   @param strict: whether to enable strict host key checking
   @type opts: list
   @param opts: list of additional options
-  @type tty: Bool
-  @param tty: If we should use tty
+  @type tty: boolean or None
+  @param tty: if we should use tty; if None, will be auto-detected
 
   """
-  args = ["ssh", "-oEscapeChar=none", "-oBatchMode=yes", "-l", "root"]
+  args = ["ssh", "-oEscapeChar=none", "-oBatchMode=yes", "-lroot"]
+
+  if tty is None:
+    tty = sys.stdout.isatty()
 
   if tty:
     args.append("-t")
@@ -197,11 +235,15 @@ def GetSSHCommand(node, cmd, strict=True, opts=None, tty=True):
   return args
 
 
-def StartLocalCommand(cmd, **kwargs):
+def StartLocalCommand(cmd, _nolog_opts=False, **kwargs):
   """Starts a local command.
 
   """
-  print "Command: %s" % utils.ShellQuoteArgs(cmd)
+  if _nolog_opts:
+    pcmd = [i for i in cmd if not i.startswith("-")]
+  else:
+    pcmd = cmd
+  print "Command: %s" % utils.ShellQuoteArgs(pcmd)
   return subprocess.Popen(cmd, shell=False, **kwargs)
 
 
@@ -209,7 +251,8 @@ def StartSSH(node, cmd, strict=True):
   """Starts SSH.
 
   """
-  return StartLocalCommand(GetSSHCommand(node, cmd, strict=strict))
+  return StartLocalCommand(GetSSHCommand(node, cmd, strict=strict),
+                           _nolog_opts=True)
 
 
 def StartMultiplexer(node):
@@ -240,7 +283,7 @@ def CloseMultiplexers():
     utils.RemoveFile(sname)
 
 
-def GetCommandOutput(node, cmd, tty=True):
+def GetCommandOutput(node, cmd, tty=None):
   """Returns the output of a command executed on the given node.
 
   """
@@ -400,7 +443,7 @@ def _List(listcmd, fields, names):
   """
   master = qa_config.GetMasterNode()
 
-  cmd = [listcmd, "list", "--separator=|", "--no-header",
+  cmd = [listcmd, "list", "--separator=|", "--no-headers",
          "--output", ",".join(fields)]
 
   if names:
@@ -410,7 +453,7 @@ def _List(listcmd, fields, names):
                           utils.ShellQuoteArgs(cmd)).splitlines()
 
 
-def GenericQueryTest(cmd, fields):
+def GenericQueryTest(cmd, fields, namefield="name", test_unknown=True):
   """Runs a number of tests on query commands.
 
   @param cmd: Command name
@@ -426,22 +469,25 @@ def GenericQueryTest(cmd, fields):
   for testfields in _SelectQueryFields(rnd, fields):
     AssertCommand([cmd, "list", "--output", ",".join(testfields)])
 
-  namelist_fn = compat.partial(_List, cmd, ["name"])
+  if namefield is not None:
+    namelist_fn = compat.partial(_List, cmd, [namefield])
 
-  # When no names were requested, the list must be sorted
-  names = namelist_fn(None)
-  AssertEqual(names, utils.NiceSort(names))
+    # When no names were requested, the list must be sorted
+    names = namelist_fn(None)
+    AssertEqual(names, utils.NiceSort(names))
 
-  # When requesting specific names, the order must be kept
-  revnames = list(reversed(names))
-  AssertEqual(namelist_fn(revnames), revnames)
+    # When requesting specific names, the order must be kept
+    revnames = list(reversed(names))
+    AssertEqual(namelist_fn(revnames), revnames)
 
-  randnames = list(names)
-  rnd.shuffle(randnames)
-  AssertEqual(namelist_fn(randnames), randnames)
+    randnames = list(names)
+    rnd.shuffle(randnames)
+    AssertEqual(namelist_fn(randnames), randnames)
 
-  # Listing unknown items must fail
-  AssertCommand([cmd, "list", "this.name.certainly.does.not.exist"], fail=True)
+  if test_unknown:
+    # Listing unknown items must fail
+    AssertCommand([cmd, "list", "this.name.certainly.does.not.exist"],
+                  fail=True)
 
   # Check exit code for listing unknown field
   AssertEqual(AssertCommand([cmd, "list", "--output=field/does/not/exist"],
@@ -519,3 +565,84 @@ def RemoveFromEtcHosts(hostnames):
                                               quoted_tmp_hosts))
   except qa_error.Error:
     AssertCommand(["rm", tmp_hosts])
+
+
+def RunInstanceCheck(instance, running):
+  """Check if instance is running or not.
+
+  """
+  instance_name = _GetName(instance, "name")
+
+  script = qa_config.GetInstanceCheckScript()
+  if not script:
+    return
+
+  master_node = qa_config.GetMasterNode()
+
+  # Build command to connect to master node
+  master_ssh = GetSSHCommand(master_node["primary"], "--")
+
+  if running:
+    running_shellval = "1"
+    running_text = ""
+  else:
+    running_shellval = ""
+    running_text = "not "
+
+  print FormatInfo("Checking if instance '%s' is %srunning" %
+                   (instance_name, running_text))
+
+  args = [script, instance_name]
+  env = {
+    "PATH": constants.HOOKS_PATH,
+    "RUN_UUID": _RUN_UUID,
+    "MASTER_SSH": utils.ShellQuoteArgs(master_ssh),
+    "INSTANCE_NAME": instance_name,
+    "INSTANCE_RUNNING": running_shellval,
+    }
+
+  result = os.spawnve(os.P_WAIT, script, args, env)
+  if result != 0:
+    raise qa_error.Error("Instance check failed with result %s" % result)
+
+
+def _InstanceCheckInner(expected, instarg, args, result):
+  """Helper function used by L{InstanceCheck}.
+
+  """
+  if instarg == FIRST_ARG:
+    instance = args[0]
+  elif instarg == RETURN_VALUE:
+    instance = result
+  else:
+    raise Exception("Invalid value '%s' for instance argument" % instarg)
+
+  if expected in (INST_DOWN, INST_UP):
+    RunInstanceCheck(instance, (expected == INST_UP))
+  elif expected is not None:
+    raise Exception("Invalid value '%s'" % expected)
+
+
+def InstanceCheck(before, after, instarg):
+  """Decorator to check instance status before and after test.
+
+  @param before: L{INST_DOWN} if instance must be stopped before test,
+    L{INST_UP} if instance must be running before test, L{None} to not check.
+  @param after: L{INST_DOWN} if instance must be stopped after test,
+    L{INST_UP} if instance must be running after test, L{None} to not check.
+  @param instarg: L{FIRST_ARG} to use first argument to test as instance (a
+    dictionary), L{RETURN_VALUE} to use return value (disallows pre-checks)
+
+  """
+  def decorator(fn):
+    @functools.wraps(fn)
+    def wrapper(*args, **kwargs):
+      _InstanceCheckInner(before, instarg, args, NotImplemented)
+
+      result = fn(*args, **kwargs)
+
+      _InstanceCheckInner(after, instarg, args, result)
+
+      return result
+    return wrapper
+  return decorator
index ba54b88..7ca5c3d 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 #
 
-# Copyright (C) 2010 Google Inc.
+# Copyright (C) 2010, 2012 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
@@ -91,6 +91,7 @@ class TestCfgupgrade(unittest.TestCase):
     utils.WriteFile(self.config_path, data=serializer.DumpJson({
       "version": constants.CONFIG_VERSION,
       "cluster": {},
+      "instances": {},
       }))
 
     hostname = netutils.GetHostname().name
@@ -105,6 +106,7 @@ class TestCfgupgrade(unittest.TestCase):
     utils.WriteFile(self.config_path, data=serializer.DumpJson({
       "version": constants.CONFIG_VERSION,
       "cluster": {},
+      "instances": {},
       }))
 
     utils.WriteFile(self.ss_master_node_path,
@@ -120,6 +122,7 @@ class TestCfgupgrade(unittest.TestCase):
       "cluster": {
         "config_version": 0,
         },
+      "instances": {},
       }
     utils.WriteFile(self.config_path, data=serializer.DumpJson(cfg))
     self.assertRaises(Exception, _RunUpgrade, self.tmpdir, False, True)
@@ -134,6 +137,7 @@ class TestCfgupgrade(unittest.TestCase):
     cfg = {
       "version": from_version,
       "cluster": {},
+      "instances": {},
       }
     self._CreateValidConfigDir()
     utils.WriteFile(self.config_path, data=serializer.DumpJson(cfg))
@@ -285,6 +289,9 @@ class TestCfgupgrade(unittest.TestCase):
   def testUpgradeFrom_2_5(self):
     self._TestSimpleUpgrade(constants.BuildVersion(2, 5, 0), False)
 
+  def testUpgradeFrom_2_6(self):
+    self._TestSimpleUpgrade(constants.BuildVersion(2, 6, 0), False)
+
   def testUpgradeCurrent(self):
     self._TestSimpleUpgrade(constants.CONFIG_VERSION, False)
 
@@ -306,6 +313,9 @@ class TestCfgupgrade(unittest.TestCase):
   def testUpgradeDryRunFrom_2_5(self):
     self._TestSimpleUpgrade(constants.BuildVersion(2, 5, 0), True)
 
+  def testUpgradeDryRunFrom_2_6(self):
+    self._TestSimpleUpgrade(constants.BuildVersion(2, 6, 0), True)
+
   def testUpgradeCurrentDryRun(self):
     self._TestSimpleUpgrade(constants.CONFIG_VERSION, True)
 
diff --git a/test/cli-test.bash b/test/cli-test.bash
new file mode 100755 (executable)
index 0000000..9c1f1fa
--- /dev/null
@@ -0,0 +1,8 @@
+#!/bin/bash
+
+export SCRIPTS=${TOP_BUILDDIR:-.}/scripts
+export DAEMONS=${TOP_BUILDDIR:-.}/daemons
+
+shelltest $SHELLTESTARGS \
+  ${TOP_SRCDIR:-.}/test/gnt-*.test \
+  -- --hide-successes
index 37e69c8..aa3eded 100755 (executable)
@@ -1,7 +1,7 @@
 #!/bin/bash
 #
 
-# Copyright (C) 2010 Google Inc.
+# 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
@@ -28,6 +28,18 @@ err() {
   exit 1
 }
 
+if ! grep -q '^ENABLE_CONFD = ' lib/_autoconf.py; then
+  err "Please update $0, confd enable feature is missing"
+fi
+
+if grep -q '^ENABLE_CONFD = True' lib/_autoconf.py; then
+  DAEMONS="$(echo ganeti-{noded,masterd,rapi,confd})"
+  STOPDAEMONS="$(echo ganeti-{confd,rapi,masterd,noded})"
+else
+  DAEMONS="$(echo ganeti-{noded,masterd,rapi})"
+  STOPDAEMONS="$(echo ganeti-{rapi,masterd,noded})"
+fi
+
 $daemon_util >/dev/null 2>&1 &&
   err "daemon-util succeeded without command"
 
@@ -49,11 +61,11 @@ $daemon_util check-exitcode 11 >/dev/null 2>&1 ||
   err "check-exitcode 11 (not master) didn't return 0"
 
 tmp=$(echo $($daemon_util list-start-daemons))
-test "$tmp" == "$(echo ganeti-{noded,masterd,rapi,confd})" ||
+test "$tmp" == "$DAEMONS" ||
   err "list-start-daemons didn't return correct list of daemons"
 
 tmp=$(echo $($daemon_util list-stop-daemons))
-test "$tmp" == "$(echo ganeti-{confd,rapi,masterd,noded})" ||
+test "$tmp" == "$STOPDAEMONS" ||
   err "list-stop-daemons didn't return correct list of daemons"
 
 $daemon_util is-daemon-name >/dev/null 2>&1 &&
@@ -64,7 +76,7 @@ for i in '' '.' '..' '-' 'not-a-daemon'; do
     err "is-daemon-name thinks '$i' is a daemon name"
 done
 
-for i in ganeti-{confd,rapi,masterd,noded}; do
+for i in $DAEMONS; do
   $daemon_util is-daemon-name $i >/dev/null 2>&1 ||
     err "is-daemon-name doesn't think '$i' is a daemon name"
 done
diff --git a/test/data/htools/common-suffix.data b/test/data/htools/common-suffix.data
new file mode 100644 (file)
index 0000000..78ff9c6
--- /dev/null
@@ -0,0 +1,10 @@
+default|fake-uuid-01|preferred
+
+node1.example.com|1024|0|1024|95367|95367|4|N|fake-uuid-01|1
+node2.example.com|1024|0|896|95367|94343|4|N|fake-uuid-01|1
+
+instance1.example.com|128|1024|1|running|Y|node2.example.com||plain|
+
+
+|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,1|diskless,file,sharedfile,plain,blockdev,drbd,rbd|4.0|32.0
+default|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,1|diskless,file,sharedfile,plain,blockdev,drbd,rbd|4.0|32.0
diff --git a/test/data/htools/hail-alloc-drbd.json b/test/data/htools/hail-alloc-drbd.json
new file mode 100644 (file)
index 0000000..8860d52
--- /dev/null
@@ -0,0 +1,515 @@
+{
+  "cluster_tags": [
+    "htools:iextags:test",
+    "htools:iextags:service-group"
+  ],
+  "nodegroups": {
+    "uuid-group-1": {
+      "ipolicy": {
+        "std": {
+          "nic-count": 1,
+          "disk-size": 1024,
+          "disk-count": 1,
+          "memory-size": 128,
+          "cpu-count": 1,
+          "spindle-use": 1
+        },
+        "min": {
+          "nic-count": 1,
+          "disk-size": 128,
+          "disk-count": 1,
+          "memory-size": 128,
+          "cpu-count": 1,
+          "spindle-use": 1
+        },
+        "max": {
+          "nic-count": 8,
+          "disk-size": 1048576,
+          "disk-count": 16,
+          "memory-size": 32768,
+          "cpu-count": 8,
+          "spindle-use": 8
+        },
+        "vcpu-ratio": 4.0,
+        "disk-templates": [
+          "sharedfile",
+          "diskless",
+          "plain",
+          "blockdev",
+          "drbd",
+          "file",
+          "rbd"
+        ],
+        "spindle-ratio": 32.0
+      },
+      "alloc_policy": "preferred",
+      "name": "default"
+    }
+  },
+  "ipolicy": {
+    "std": {
+      "nic-count": 1,
+      "disk-size": 1024,
+      "memory-size": 128,
+      "cpu-count": 1,
+      "disk-count": 1,
+      "spindle-use": 1
+    },
+    "min": {
+      "nic-count": 1,
+      "disk-size": 1024,
+      "memory-size": 128,
+      "cpu-count": 1,
+      "disk-count": 1,
+      "spindle-use": 1
+    },
+    "max": {
+      "nic-count": 8,
+      "disk-size": 1048576,
+      "memory-size": 32768,
+      "cpu-count": 8,
+      "disk-count": 16,
+      "spindle-use": 8
+    },
+    "vcpu-ratio": 4.0,
+    "disk-templates": [
+      "sharedfile",
+      "diskless",
+      "plain",
+      "blockdev",
+      "drbd",
+      "file",
+      "rbd"
+    ],
+    "spindle-ratio": 32.0
+  },
+  "enabled_hypervisors": [
+    "xen-pvm",
+    "xen-hvm"
+  ],
+  "cluster_name": "cluster",
+  "instances": {
+    "instance14": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 128
+        }
+      ],
+      "disk_space_total": 256,
+      "hypervisor": "xen-pvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:eb:0b:a5",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "admin_state": "down",
+      "disk_template": "drbd",
+      "spindle_use": 1,
+      "memory": 128,
+      "nodes": [
+        "node3",
+        "node4"
+      ],
+      "os": "debian-image"
+    },
+    "instance13": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 512
+        }
+      ],
+      "disk_space_total": 512,
+      "hypervisor": "xen-pvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:7f:8c:9c",
+          "link": "xen-br1",
+          "mode": "bridged",
+          "bridge": "xen-br1"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "plain",
+      "memory": 128,
+      "nodes": [
+        "node4"
+      ],
+      "os": "instance-debootstrap"
+    },
+    "instance18": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 128
+        }
+      ],
+      "disk_space_total": 128,
+      "hypervisor": "xen-pvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:55:94:93",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "plain",
+      "memory": 8192,
+      "nodes": [
+        "node4"
+      ],
+      "os": "instance-debootstrap"
+    },
+    "instance19": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 128
+        }
+      ],
+      "disk_space_total": 256,
+      "hypervisor": "xen-pvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:15:92:6f",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "drbd",
+      "memory": 128,
+      "nodes": [
+        "node3",
+        "node4"
+      ],
+      "os": "debian-image"
+    },
+    "instance2": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 128
+        }
+      ],
+      "disk_space_total": 256,
+      "hypervisor": "xen-pvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:73:20:3e",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "up",
+      "disk_template": "drbd",
+      "memory": 128,
+      "nodes": [
+        "node3",
+        "node4"
+      ],
+      "os": "debian-image"
+    },
+    "instance3": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 256
+        },
+        {
+          "mode": "rw",
+          "size": 128
+        }
+      ],
+      "disk_space_total": 384,
+      "hypervisor": "xen-pvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:ec:e8:a2",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "plain",
+      "memory": 128,
+      "nodes": [
+        "node4"
+      ],
+      "os": "debian-image"
+    },
+    "instance4": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 2048
+        }
+      ],
+      "disk_space_total": 2176,
+      "hypervisor": "xen-pvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:62:b0:76",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "drbd",
+      "memory": 128,
+      "nodes": [
+        "node4",
+        "node3"
+      ],
+      "os": "instance-debootstrap"
+    },
+    "instance8": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 256
+        }
+      ],
+      "disk_space_total": 256,
+      "hypervisor": "kvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:3f:6d:e3",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "plain",
+      "memory": 128,
+      "nodes": [
+        "node4"
+      ],
+      "os": "debian-image"
+    },
+    "instance9": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 128
+        }
+      ],
+      "disk_space_total": 256,
+      "hypervisor": "xen-pvm",
+      "tags": [
+        "test:test"
+      ],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:10:d2:01",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "drbd",
+      "memory": 128,
+      "nodes": [
+        "node3",
+        "node4"
+      ],
+      "os": "instance-debootstrap"
+    },
+    "instance20": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 512
+        }
+      ],
+      "disk_space_total": 512,
+      "hypervisor": "kvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:db:2a:6d",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "plain",
+      "memory": 128,
+      "nodes": [
+        "node4"
+      ],
+      "os": "instance-debootstrap"
+    }
+  },
+  "version": 2,
+  "nodes": {
+    "node1": {
+      "total_disk": 1377280,
+      "total_cpus": 4,
+      "group": "uuid-group-1",
+      "secondary_ip": "192.168.2.1",
+      "i_pri_up_memory": 0,
+      "tags": [],
+      "master_candidate": true,
+      "free_memory": 31389,
+      "ndparams": {
+        "spindle_count": 1,
+        "oob_program": null
+      },
+      "reserved_memory": 1017,
+      "master_capable": true,
+      "free_disk": 1377280,
+      "drained": false,
+      "total_memory": 32763,
+      "primary_ip": "192.168.1.1",
+      "i_pri_memory": 0,
+      "vm_capable": true,
+      "offline": false
+    },
+    "node2": {
+      "total_disk": 1377280,
+      "total_cpus": 4,
+      "group": "uuid-group-1",
+      "secondary_ip": "192.168.2.2",
+      "i_pri_up_memory": 0,
+      "tags": [],
+      "master_candidate": true,
+      "free_memory": 31746,
+      "ndparams": {
+        "spindle_count": 1,
+        "oob_program": null
+      },
+      "reserved_memory": 1017,
+      "master_capable": true,
+      "free_disk": 1376640,
+      "drained": false,
+      "total_memory": 32763,
+      "primary_ip": "192.168.1.2",
+      "i_pri_memory": 0,
+      "vm_capable": true,
+      "offline": false
+    },
+    "node3": {
+      "total_disk": 1377304,
+      "total_cpus": 4,
+      "group": "uuid-group-1",
+      "secondary_ip": "192.168.2.3",
+      "i_pri_up_memory": 128,
+      "tags": [],
+      "master_candidate": true,
+      "free_memory": 31234,
+      "ndparams": {
+        "spindle_count": 1,
+        "oob_program": null
+      },
+      "reserved_memory": 1017,
+      "master_capable": true,
+      "free_disk": 1373336,
+      "drained": false,
+      "total_memory": 32763,
+      "primary_ip": "192.168.1.3",
+      "i_pri_memory": 2432,
+      "vm_capable": true,
+      "offline": false
+    },
+    "node4": {
+      "total_disk": 1377280,
+      "total_cpus": 4,
+      "group": "uuid-group-1",
+      "secondary_ip": "192.168.2.4",
+      "i_pri_up_memory": 128,
+      "tags": [],
+      "master_candidate": true,
+      "free_memory": 22914,
+      "ndparams": {
+        "spindle_count": 1,
+        "oob_program": null
+      },
+      "reserved_memory": 1017,
+      "master_capable": true,
+      "free_disk": 1371520,
+      "drained": false,
+      "total_memory": 32763,
+      "primary_ip": "192.168.1.4",
+      "i_pri_memory": 23552,
+      "vm_capable": true,
+      "offline": false
+    }
+  },
+  "request": {
+    "disks": [
+      {
+        "mode": "rw",
+        "size": 1024
+      }
+    ],
+    "required_nodes": 2,
+    "name": "instance1",
+    "tags": [],
+    "hypervisor": "xen-pvm",
+    "disk_space_total": 1024,
+    "nics": [
+      {
+        "ip": null,
+        "mac": "00:11:22:33:44:55",
+        "bridge": null
+      }
+    ],
+    "vcpus": 1,
+    "spindle_use": 1,
+    "os": "instance-debootstrap",
+    "disk_template": "drbd",
+    "memory": 1024,
+    "type": "allocate"
+  }
+}
diff --git a/test/data/htools/hail-change-group.json b/test/data/htools/hail-change-group.json
new file mode 100644 (file)
index 0000000..9c41c15
--- /dev/null
@@ -0,0 +1,560 @@
+{
+  "cluster_tags": [
+    "htools:iextags:test",
+    "htools:iextags:service-group"
+  ],
+  "nodegroups": {
+    "uuid-group-1": {
+      "ipolicy": {
+        "std": {
+          "nic-count": 1,
+          "disk-size": 1024,
+          "disk-count": 1,
+          "memory-size": 128,
+          "cpu-count": 1,
+          "spindle-use": 1
+        },
+        "min": {
+          "nic-count": 1,
+          "disk-size": 128,
+          "disk-count": 1,
+          "memory-size": 128,
+          "cpu-count": 1,
+          "spindle-use": 1
+        },
+        "max": {
+          "nic-count": 8,
+          "disk-size": 1048576,
+          "disk-count": 16,
+          "memory-size": 32768,
+          "cpu-count": 8,
+          "spindle-use": 8
+        },
+        "vcpu-ratio": 4.0,
+        "disk-templates": [
+          "sharedfile",
+          "diskless",
+          "plain",
+          "blockdev",
+          "drbd",
+          "file",
+          "rbd"
+        ],
+        "spindle-ratio": 32.0
+      },
+      "alloc_policy": "preferred",
+      "name": "default"
+    },
+    "uuid-group-2": {
+      "ipolicy": {
+        "std": {
+          "nic-count": 1,
+          "disk-size": 1024,
+          "disk-count": 1,
+          "memory-size": 128,
+          "cpu-count": 1,
+          "spindle-use": 1
+        },
+        "min": {
+          "nic-count": 1,
+          "disk-size": 128,
+          "disk-count": 1,
+          "memory-size": 128,
+          "cpu-count": 1,
+          "spindle-use": 1
+        },
+        "max": {
+          "nic-count": 8,
+          "disk-size": 1048576,
+          "disk-count": 16,
+          "memory-size": 32768,
+          "cpu-count": 8,
+          "spindle-use": 8
+        },
+        "vcpu-ratio": 4.0,
+        "disk-templates": [
+          "sharedfile",
+          "diskless",
+          "plain",
+          "blockdev",
+          "drbd",
+          "file",
+          "rbd"
+        ],
+        "spindle-ratio": 32.0
+      },
+      "alloc_policy": "preferred",
+      "name": "empty"
+    }
+  },
+  "ipolicy": {
+    "std": {
+      "nic-count": 1,
+      "disk-size": 1024,
+      "memory-size": 128,
+      "cpu-count": 1,
+      "disk-count": 1,
+      "spindle-use": 1
+    },
+    "min": {
+      "nic-count": 1,
+      "disk-size": 1024,
+      "memory-size": 128,
+      "cpu-count": 1,
+      "disk-count": 1,
+      "spindle-use": 1
+    },
+    "max": {
+      "nic-count": 8,
+      "disk-size": 1048576,
+      "memory-size": 32768,
+      "cpu-count": 8,
+      "disk-count": 16,
+      "spindle-use": 8
+    },
+    "vcpu-ratio": 4.0,
+    "disk-templates": [
+      "sharedfile",
+      "diskless",
+      "plain",
+      "blockdev",
+      "drbd",
+      "file",
+      "rbd"
+    ],
+    "spindle-ratio": 32.0
+  },
+  "enabled_hypervisors": [
+    "xen-pvm",
+    "xen-hvm"
+  ],
+  "cluster_name": "cluster",
+  "instances": {
+    "instance14": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 128
+        }
+      ],
+      "disk_space_total": 256,
+      "hypervisor": "xen-pvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:eb:0b:a5",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "drbd",
+      "memory": 128,
+      "nodes": [
+        "node3",
+        "node4"
+      ],
+      "os": "debian-image"
+    },
+    "instance13": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 512
+        }
+      ],
+      "disk_space_total": 512,
+      "hypervisor": "xen-pvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:7f:8c:9c",
+          "link": "xen-br1",
+          "mode": "bridged",
+          "bridge": "xen-br1"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "plain",
+      "memory": 128,
+      "nodes": [
+        "node4"
+      ],
+      "os": "instance-debootstrap"
+    },
+    "instance18": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 128
+        }
+      ],
+      "disk_space_total": 128,
+      "hypervisor": "xen-pvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:55:94:93",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "plain",
+      "memory": 8192,
+      "nodes": [
+        "node4"
+      ],
+      "os": "instance-debootstrap"
+    },
+    "instance19": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 128
+        }
+      ],
+      "disk_space_total": 256,
+      "hypervisor": "xen-pvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:15:92:6f",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "drbd",
+      "memory": 128,
+      "nodes": [
+        "node3",
+        "node4"
+      ],
+      "os": "debian-image"
+    },
+    "instance2": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 128
+        }
+      ],
+      "disk_space_total": 256,
+      "hypervisor": "xen-pvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:73:20:3e",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "up",
+      "disk_template": "drbd",
+      "memory": 128,
+      "nodes": [
+        "node3",
+        "node4"
+      ],
+      "os": "debian-image"
+    },
+    "instance3": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 256
+        },
+        {
+          "mode": "rw",
+          "size": 128
+        }
+      ],
+      "disk_space_total": 384,
+      "hypervisor": "xen-pvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:ec:e8:a2",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "plain",
+      "memory": 128,
+      "nodes": [
+        "node4"
+      ],
+      "os": "debian-image"
+    },
+    "instance4": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 2048
+        }
+      ],
+      "disk_space_total": 2176,
+      "hypervisor": "xen-pvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:62:b0:76",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "drbd",
+      "memory": 128,
+      "nodes": [
+        "node4",
+        "node3"
+      ],
+      "os": "instance-debootstrap"
+    },
+    "instance8": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 256
+        }
+      ],
+      "disk_space_total": 256,
+      "hypervisor": "kvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:3f:6d:e3",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "plain",
+      "memory": 128,
+      "nodes": [
+        "node4"
+      ],
+      "os": "debian-image"
+    },
+    "instance9": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 128
+        }
+      ],
+      "disk_space_total": 256,
+      "hypervisor": "xen-pvm",
+      "tags": [
+        "test:test"
+      ],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:10:d2:01",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "drbd",
+      "memory": 128,
+      "nodes": [
+        "node3",
+        "node4"
+      ],
+      "os": "instance-debootstrap"
+    },
+    "instance20": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 512
+        }
+      ],
+      "disk_space_total": 512,
+      "hypervisor": "kvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:db:2a:6d",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "plain",
+      "memory": 128,
+      "nodes": [
+        "node4"
+      ],
+      "os": "instance-debootstrap"
+    }
+  },
+  "version": 2,
+  "nodes": {
+    "node1": {
+      "total_disk": 1377280,
+      "total_cpus": 4,
+      "group": "uuid-group-1",
+      "secondary_ip": "192.168.2.1",
+      "i_pri_up_memory": 0,
+      "tags": [],
+      "master_candidate": true,
+      "free_memory": 31389,
+      "ndparams": {
+        "spindle_count": 1,
+        "oob_program": null
+      },
+      "reserved_memory": 1017,
+      "master_capable": true,
+      "free_disk": 1377280,
+      "drained": false,
+      "total_memory": 32763,
+      "primary_ip": "192.168.1.1",
+      "i_pri_memory": 0,
+      "vm_capable": true,
+      "offline": false
+    },
+    "node3": {
+      "total_disk": 1377304,
+      "total_cpus": 4,
+      "group": "uuid-group-1",
+      "secondary_ip": "192.168.2.3",
+      "i_pri_up_memory": 128,
+      "tags": [],
+      "master_candidate": true,
+      "free_memory": 31234,
+      "ndparams": {
+        "spindle_count": 1,
+        "oob_program": null
+      },
+      "reserved_memory": 1017,
+      "master_capable": true,
+      "free_disk": 1373336,
+      "drained": false,
+      "total_memory": 32763,
+      "primary_ip": "192.168.1.3",
+      "i_pri_memory": 2432,
+      "vm_capable": true,
+      "offline": false
+    },
+    "node4": {
+      "total_disk": 1377280,
+      "total_cpus": 4,
+      "group": "uuid-group-1",
+      "secondary_ip": "192.168.2.4",
+      "i_pri_up_memory": 128,
+      "tags": [],
+      "master_candidate": true,
+      "free_memory": 22914,
+      "ndparams": {
+        "spindle_count": 1,
+        "oob_program": null
+      },
+      "reserved_memory": 1017,
+      "master_capable": true,
+      "free_disk": 1371520,
+      "drained": false,
+      "total_memory": 32763,
+      "primary_ip": "192.168.1.4",
+      "i_pri_memory": 23552,
+      "vm_capable": true,
+      "offline": false
+    },
+    "node10": {
+      "total_disk": 1377280,
+      "total_cpus": 4,
+      "group": "uuid-group-2",
+      "secondary_ip": "192.168.2.10",
+      "i_pri_up_memory": 128,
+      "tags": [],
+      "master_candidate": true,
+      "free_memory": 31746,
+      "ndparams": {
+        "spindle_count": 1,
+        "oob_program": null
+      },
+      "reserved_memory": 1017,
+      "master_capable": true,
+      "free_disk": 1376640,
+      "drained": false,
+      "total_memory": 32763,
+      "primary_ip": "192.168.1.10",
+      "i_pri_memory": 23552,
+      "vm_capable": true,
+      "offline": false
+    },
+    "node11": {
+      "total_disk": 1377280,
+      "total_cpus": 4,
+      "group": "uuid-group-2",
+      "secondary_ip": "192.168.2.11",
+      "i_pri_up_memory": 128,
+      "tags": [],
+      "master_candidate": true,
+      "free_memory": 31746,
+      "ndparams": {
+        "spindle_count": 1,
+        "oob_program": null
+      },
+      "reserved_memory": 1017,
+      "master_capable": true,
+      "free_disk": 1376640,
+      "drained": false,
+      "total_memory": 32763,
+      "primary_ip": "192.168.1.11",
+      "i_pri_memory": 23552,
+      "vm_capable": true,
+      "offline": false
+    }
+  },
+  "request": {
+    "instances": [
+      "instance14"
+    ],
+    "target_groups": [],
+    "type": "change-group"
+  }
+}
diff --git a/test/data/htools/hail-invalid-reloc.json b/test/data/htools/hail-invalid-reloc.json
new file mode 100644 (file)
index 0000000..74391f4
--- /dev/null
@@ -0,0 +1,15 @@
+{
+  "cluster_tags": [],
+  "nodegroups": {},
+  "nodes": {},
+  "instances": {},
+  "request": {
+    "relocate_from": [
+      "node4"
+    ],
+    "required_nodes": "aaa",
+    "type": "relocate",
+    "name": 0,
+    "disk_space_total": "aaa"
+  }
+}
diff --git a/test/data/htools/hail-node-evac.json b/test/data/htools/hail-node-evac.json
new file mode 100644 (file)
index 0000000..1f3381f
--- /dev/null
@@ -0,0 +1,496 @@
+{
+  "cluster_tags": [
+    "htools:iextags:test",
+    "htools:iextags:service-group"
+  ],
+  "nodegroups": {
+    "uuid-group-1": {
+      "ipolicy": {
+        "std": {
+          "nic-count": 1,
+          "disk-size": 1024,
+          "disk-count": 1,
+          "memory-size": 128,
+          "cpu-count": 1,
+          "spindle-use": 1
+        },
+        "min": {
+          "nic-count": 1,
+          "disk-size": 128,
+          "disk-count": 1,
+          "memory-size": 128,
+          "cpu-count": 1,
+          "spindle-use": 1
+        },
+        "max": {
+          "nic-count": 8,
+          "disk-size": 1048576,
+          "disk-count": 16,
+          "memory-size": 32768,
+          "cpu-count": 8,
+          "spindle-use": 8
+        },
+        "vcpu-ratio": 4.0,
+        "disk-templates": [
+          "sharedfile",
+          "diskless",
+          "plain",
+          "blockdev",
+          "drbd",
+          "file",
+          "rbd"
+        ],
+        "spindle-ratio": 32.0
+      },
+      "alloc_policy": "preferred",
+      "name": "default"
+    }
+  },
+  "ipolicy": {
+    "std": {
+      "nic-count": 1,
+      "disk-size": 1024,
+      "memory-size": 128,
+      "cpu-count": 1,
+      "disk-count": 1,
+      "spindle-use": 1
+    },
+    "min": {
+      "nic-count": 1,
+      "disk-size": 1024,
+      "memory-size": 128,
+      "cpu-count": 1,
+      "disk-count": 1,
+      "spindle-use": 1
+    },
+    "max": {
+      "nic-count": 8,
+      "disk-size": 1048576,
+      "memory-size": 32768,
+      "cpu-count": 8,
+      "disk-count": 16,
+      "spindle-use": 8
+    },
+    "vcpu-ratio": 4.0,
+    "disk-templates": [
+      "sharedfile",
+      "diskless",
+      "plain",
+      "blockdev",
+      "drbd",
+      "file",
+      "rbd"
+    ],
+    "spindle-ratio": 32.0
+  },
+  "enabled_hypervisors": [
+    "xen-pvm",
+    "xen-hvm"
+  ],
+  "cluster_name": "cluster",
+  "instances": {
+    "instance14": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 128
+        }
+      ],
+      "disk_space_total": 256,
+      "hypervisor": "xen-pvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:eb:0b:a5",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "drbd",
+      "memory": 128,
+      "nodes": [
+        "node3",
+        "node4"
+      ],
+      "os": "debian-image"
+    },
+    "instance13": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 512
+        }
+      ],
+      "disk_space_total": 512,
+      "hypervisor": "xen-pvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:7f:8c:9c",
+          "link": "xen-br1",
+          "mode": "bridged",
+          "bridge": "xen-br1"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "plain",
+      "memory": 128,
+      "nodes": [
+        "node4"
+      ],
+      "os": "instance-debootstrap"
+    },
+    "instance18": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 128
+        }
+      ],
+      "disk_space_total": 128,
+      "hypervisor": "xen-pvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:55:94:93",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "plain",
+      "memory": 8192,
+      "nodes": [
+        "node4"
+      ],
+      "os": "instance-debootstrap"
+    },
+    "instance19": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 128
+        }
+      ],
+      "disk_space_total": 256,
+      "hypervisor": "xen-pvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:15:92:6f",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "drbd",
+      "memory": 128,
+      "nodes": [
+        "node3",
+        "node4"
+      ],
+      "os": "debian-image"
+    },
+    "instance2": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 128
+        }
+      ],
+      "disk_space_total": 256,
+      "hypervisor": "xen-pvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:73:20:3e",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "up",
+      "disk_template": "drbd",
+      "memory": 128,
+      "nodes": [
+        "node3",
+        "node4"
+      ],
+      "os": "debian-image"
+    },
+    "instance3": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 256
+        },
+        {
+          "mode": "rw",
+          "size": 128
+        }
+      ],
+      "disk_space_total": 384,
+      "hypervisor": "xen-pvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:ec:e8:a2",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "plain",
+      "memory": 128,
+      "nodes": [
+        "node4"
+      ],
+      "os": "debian-image"
+    },
+    "instance4": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 2048
+        }
+      ],
+      "disk_space_total": 2176,
+      "hypervisor": "xen-pvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:62:b0:76",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "drbd",
+      "memory": 128,
+      "nodes": [
+        "node4",
+        "node3"
+      ],
+      "os": "instance-debootstrap"
+    },
+    "instance8": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 256
+        }
+      ],
+      "disk_space_total": 256,
+      "hypervisor": "kvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:3f:6d:e3",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "plain",
+      "memory": 128,
+      "nodes": [
+        "node4"
+      ],
+      "os": "debian-image"
+    },
+    "instance9": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 128
+        }
+      ],
+      "disk_space_total": 256,
+      "hypervisor": "xen-pvm",
+      "tags": [
+        "test:test"
+      ],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:10:d2:01",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "drbd",
+      "memory": 128,
+      "nodes": [
+        "node3",
+        "node4"
+      ],
+      "os": "instance-debootstrap"
+    },
+    "instance20": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 512
+        }
+      ],
+      "disk_space_total": 512,
+      "hypervisor": "kvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:db:2a:6d",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "plain",
+      "memory": 128,
+      "nodes": [
+        "node4"
+      ],
+      "os": "instance-debootstrap"
+    }
+  },
+  "version": 2,
+  "nodes": {
+    "node1": {
+      "total_disk": 1377280,
+      "total_cpus": 4,
+      "group": "uuid-group-1",
+      "secondary_ip": "192.168.2.1",
+      "i_pri_up_memory": 0,
+      "tags": [],
+      "master_candidate": true,
+      "free_memory": 31389,
+      "ndparams": {
+        "spindle_count": 1,
+        "oob_program": null
+      },
+      "reserved_memory": 1017,
+      "master_capable": true,
+      "free_disk": 1377280,
+      "drained": false,
+      "total_memory": 32763,
+      "primary_ip": "192.168.1.1",
+      "i_pri_memory": 0,
+      "vm_capable": true,
+      "offline": false
+    },
+    "node2": {
+      "total_disk": 1377280,
+      "total_cpus": 4,
+      "group": "uuid-group-1",
+      "secondary_ip": "192.168.2.2",
+      "i_pri_up_memory": 0,
+      "tags": [],
+      "master_candidate": true,
+      "free_memory": 31746,
+      "ndparams": {
+        "spindle_count": 1,
+        "oob_program": null
+      },
+      "reserved_memory": 1017,
+      "master_capable": true,
+      "free_disk": 1376640,
+      "drained": false,
+      "total_memory": 32763,
+      "primary_ip": "192.168.1.2",
+      "i_pri_memory": 0,
+      "vm_capable": true,
+      "offline": false
+    },
+    "node3": {
+      "total_disk": 1377304,
+      "total_cpus": 4,
+      "group": "uuid-group-1",
+      "secondary_ip": "192.168.2.3",
+      "i_pri_up_memory": 128,
+      "tags": [],
+      "master_candidate": true,
+      "free_memory": 31234,
+      "ndparams": {
+        "spindle_count": 1,
+        "oob_program": null
+      },
+      "reserved_memory": 1017,
+      "master_capable": true,
+      "free_disk": 1373336,
+      "drained": false,
+      "total_memory": 32763,
+      "primary_ip": "192.168.1.3",
+      "i_pri_memory": 2432,
+      "vm_capable": true,
+      "offline": false
+    },
+    "node4": {
+      "total_disk": 1377280,
+      "total_cpus": 4,
+      "group": "uuid-group-1",
+      "secondary_ip": "192.168.2.4",
+      "i_pri_up_memory": 128,
+      "tags": [],
+      "master_candidate": true,
+      "free_memory": 22914,
+      "ndparams": {
+        "spindle_count": 1,
+        "oob_program": null
+      },
+      "reserved_memory": 1017,
+      "master_capable": true,
+      "free_disk": 1371520,
+      "drained": false,
+      "total_memory": 32763,
+      "primary_ip": "192.168.1.4",
+      "i_pri_memory": 23552,
+      "vm_capable": true,
+      "offline": false
+    }
+  },
+  "request": {
+    "evac_mode": "all",
+    "instances": [
+      "instance2"
+    ],
+    "type": "node-evacuate"
+  }
+}
diff --git a/test/data/htools/hail-reloc-drbd.json b/test/data/htools/hail-reloc-drbd.json
new file mode 100644 (file)
index 0000000..bcf72a2
--- /dev/null
@@ -0,0 +1,498 @@
+{
+  "cluster_tags": [
+    "htools:iextags:test",
+    "htools:iextags:service-group"
+  ],
+  "nodegroups": {
+    "uuid-group-1": {
+      "ipolicy": {
+        "std": {
+          "nic-count": 1,
+          "disk-size": 1024,
+          "disk-count": 1,
+          "memory-size": 128,
+          "cpu-count": 1,
+          "spindle-use": 1
+        },
+        "min": {
+          "nic-count": 1,
+          "disk-size": 128,
+          "disk-count": 1,
+          "memory-size": 128,
+          "cpu-count": 1,
+          "spindle-use": 1
+        },
+        "max": {
+          "nic-count": 8,
+          "disk-size": 1048576,
+          "disk-count": 16,
+          "memory-size": 32768,
+          "cpu-count": 8,
+          "spindle-use": 8
+        },
+        "vcpu-ratio": 4.0,
+        "disk-templates": [
+          "sharedfile",
+          "diskless",
+          "plain",
+          "blockdev",
+          "drbd",
+          "file",
+          "rbd"
+        ],
+        "spindle-ratio": 32.0
+      },
+      "alloc_policy": "preferred",
+      "name": "default"
+    }
+  },
+  "ipolicy": {
+    "std": {
+      "nic-count": 1,
+      "disk-size": 1024,
+      "memory-size": 128,
+      "cpu-count": 1,
+      "disk-count": 1,
+      "spindle-use": 1
+    },
+    "min": {
+      "nic-count": 1,
+      "disk-size": 1024,
+      "memory-size": 128,
+      "cpu-count": 1,
+      "disk-count": 1,
+      "spindle-use": 1
+    },
+    "max": {
+      "nic-count": 8,
+      "disk-size": 1048576,
+      "memory-size": 32768,
+      "cpu-count": 8,
+      "disk-count": 16,
+      "spindle-use": 8
+    },
+    "vcpu-ratio": 4.0,
+    "disk-templates": [
+      "sharedfile",
+      "diskless",
+      "plain",
+      "blockdev",
+      "drbd",
+      "file",
+      "rbd"
+    ],
+    "spindle-ratio": 32.0
+  },
+  "enabled_hypervisors": [
+    "xen-pvm",
+    "xen-hvm"
+  ],
+  "cluster_name": "cluster",
+  "instances": {
+    "instance14": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 128
+        }
+      ],
+      "disk_space_total": 256,
+      "hypervisor": "xen-pvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:eb:0b:a5",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "drbd",
+      "memory": 128,
+      "nodes": [
+        "node3",
+        "node4"
+      ],
+      "os": "debian-image"
+    },
+    "instance13": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 512
+        }
+      ],
+      "disk_space_total": 512,
+      "hypervisor": "xen-pvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:7f:8c:9c",
+          "link": "xen-br1",
+          "mode": "bridged",
+          "bridge": "xen-br1"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "plain",
+      "memory": 128,
+      "nodes": [
+        "node4"
+      ],
+      "os": "instance-debootstrap"
+    },
+    "instance18": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 128
+        }
+      ],
+      "disk_space_total": 128,
+      "hypervisor": "xen-pvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:55:94:93",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "plain",
+      "memory": 8192,
+      "nodes": [
+        "node4"
+      ],
+      "os": "instance-debootstrap"
+    },
+    "instance19": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 128
+        }
+      ],
+      "disk_space_total": 256,
+      "hypervisor": "xen-pvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:15:92:6f",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "drbd",
+      "memory": 128,
+      "nodes": [
+        "node3",
+        "node4"
+      ],
+      "os": "debian-image"
+    },
+    "instance2": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 128
+        }
+      ],
+      "disk_space_total": 256,
+      "hypervisor": "xen-pvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:73:20:3e",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "up",
+      "disk_template": "drbd",
+      "memory": 128,
+      "nodes": [
+        "node3",
+        "node4"
+      ],
+      "os": "debian-image"
+    },
+    "instance3": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 256
+        },
+        {
+          "mode": "rw",
+          "size": 128
+        }
+      ],
+      "disk_space_total": 384,
+      "hypervisor": "xen-pvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:ec:e8:a2",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "plain",
+      "memory": 128,
+      "nodes": [
+        "node4"
+      ],
+      "os": "debian-image"
+    },
+    "instance4": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 2048
+        }
+      ],
+      "disk_space_total": 2176,
+      "hypervisor": "xen-pvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:62:b0:76",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "drbd",
+      "memory": 128,
+      "nodes": [
+        "node4",
+        "node3"
+      ],
+      "os": "instance-debootstrap"
+    },
+    "instance8": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 256
+        }
+      ],
+      "disk_space_total": 256,
+      "hypervisor": "kvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:3f:6d:e3",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "plain",
+      "memory": 128,
+      "nodes": [
+        "node4"
+      ],
+      "os": "debian-image"
+    },
+    "instance9": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 128
+        }
+      ],
+      "disk_space_total": 256,
+      "hypervisor": "xen-pvm",
+      "tags": [
+        "test:test"
+      ],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:10:d2:01",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "drbd",
+      "memory": 128,
+      "nodes": [
+        "node3",
+        "node4"
+      ],
+      "os": "instance-debootstrap"
+    },
+    "instance20": {
+      "disks": [
+        {
+          "mode": "rw",
+          "size": 512
+        }
+      ],
+      "disk_space_total": 512,
+      "hypervisor": "kvm",
+      "tags": [],
+      "nics": [
+        {
+          "ip": null,
+          "mac": "aa:00:00:db:2a:6d",
+          "link": "xen-br0",
+          "mode": "bridged",
+          "bridge": "xen-br0"
+        }
+      ],
+      "vcpus": 1,
+      "spindle_use": 1,
+      "admin_state": "down",
+      "disk_template": "plain",
+      "memory": 128,
+      "nodes": [
+        "node4"
+      ],
+      "os": "instance-debootstrap"
+    }
+  },
+  "version": 2,
+  "nodes": {
+    "node1": {
+      "total_disk": 1377280,
+      "total_cpus": 4,
+      "group": "uuid-group-1",
+      "secondary_ip": "192.168.2.1",
+      "i_pri_up_memory": 0,
+      "tags": [],
+      "master_candidate": true,
+      "free_memory": 31389,
+      "ndparams": {
+        "spindle_count": 1,
+        "oob_program": null
+      },
+      "reserved_memory": 1017,
+      "master_capable": true,
+      "free_disk": 1377280,
+      "drained": false,
+      "total_memory": 32763,
+      "primary_ip": "192.168.1.1",
+      "i_pri_memory": 0,
+      "vm_capable": true,
+      "offline": false
+    },
+    "node2": {
+      "total_disk": 1377280,
+      "total_cpus": 4,
+      "group": "uuid-group-1",
+      "secondary_ip": "192.168.2.2",
+      "i_pri_up_memory": 0,
+      "tags": [],
+      "master_candidate": true,
+      "free_memory": 31746,
+      "ndparams": {
+        "spindle_count": 1,
+        "oob_program": null
+      },
+      "reserved_memory": 1017,
+      "master_capable": true,
+      "free_disk": 1376640,
+      "drained": false,
+      "total_memory": 32763,
+      "primary_ip": "192.168.1.2",
+      "i_pri_memory": 0,
+      "vm_capable": true,
+      "offline": false
+    },
+    "node3": {
+      "total_disk": 1377304,
+      "total_cpus": 4,
+      "group": "uuid-group-1",
+      "secondary_ip": "192.168.2.3",
+      "i_pri_up_memory": 128,
+      "tags": [],
+      "master_candidate": true,
+      "free_memory": 31234,
+      "ndparams": {
+        "spindle_count": 1,
+        "oob_program": null
+      },
+      "reserved_memory": 1017,
+      "master_capable": true,
+      "free_disk": 1373336,
+      "drained": false,
+      "total_memory": 32763,
+      "primary_ip": "192.168.1.3",
+      "i_pri_memory": 2432,
+      "vm_capable": true,
+      "offline": false
+    },
+    "node4": {
+      "total_disk": 1377280,
+      "total_cpus": 4,
+      "group": "uuid-group-1",
+      "secondary_ip": "192.168.2.4",
+      "i_pri_up_memory": 128,
+      "tags": [],
+      "master_candidate": true,
+      "free_memory": 22914,
+      "ndparams": {
+        "spindle_count": 1,
+        "oob_program": null
+      },
+      "reserved_memory": 1017,
+      "master_capable": true,
+      "free_disk": 1371520,
+      "drained": false,
+      "total_memory": 32763,
+      "primary_ip": "192.168.1.4",
+      "i_pri_memory": 23552,
+      "vm_capable": true,
+      "offline": false
+    }
+  },
+  "request": {
+    "relocate_from": [
+      "node4"
+    ],
+    "required_nodes": 1,
+    "type": "relocate",
+    "name": "instance14",
+    "disk_space_total": 256
+  }
+}
diff --git a/test/data/htools/hbal-split-insts.data b/test/data/htools/hbal-split-insts.data
new file mode 100644 (file)
index 0000000..4d654e8
--- /dev/null
@@ -0,0 +1,145 @@
+group-01|fake-uuid-01|preferred
+group-02|fake-uuid-02|preferred
+
+node-01-001|98304|0|96256|8388608|8355840|16|N|fake-uuid-01|1
+node-01-002|98304|0|96256|8388608|8355840|16|N|fake-uuid-01|1
+node-01-003|98304|0|96256|8388608|8355840|16|N|fake-uuid-01|1
+node-01-004|98304|0|96256|8388608|8355840|16|N|fake-uuid-01|1
+node-01-005|98304|0|96256|8388608|8355840|16|N|fake-uuid-01|1
+node-01-006|98304|0|96256|8388608|8355840|16|N|fake-uuid-01|1
+node-01-007|98304|0|96256|8388608|8355840|16|N|fake-uuid-02|1
+node-01-008|98304|0|96256|8388608|8355840|16|N|fake-uuid-02|1
+
+new-0|128|1024|1|running|Y|node-01-008|node-01-007|drbd||1
+new-1|128|1024|1|running|Y|node-01-006|node-01-005|drbd||1
+new-2|128|1024|1|running|Y|node-01-004|node-01-003|drbd||1
+new-3|128|1024|1|running|Y|node-01-002|node-01-001|drbd||1
+new-4|128|1024|1|running|Y|node-01-007|node-01-008|drbd||1
+new-5|128|1024|1|running|Y|node-01-005|node-01-006|drbd||1
+new-6|128|1024|1|running|Y|node-01-003|node-01-004|drbd||1
+new-7|128|1024|1|running|Y|node-01-001|node-01-002|drbd||1
+new-8|128|1024|1|running|Y|node-01-008|node-01-006|drbd||1
+new-9|128|1024|1|running|Y|node-01-007|node-01-005|drbd||1
+new-10|128|1024|1|running|Y|node-01-004|node-01-002|drbd||1
+new-11|128|1024|1|running|Y|node-01-003|node-01-001|drbd||1
+new-12|128|1024|1|running|Y|node-01-006|node-01-008|drbd||1
+new-13|128|1024|1|running|Y|node-01-005|node-01-007|drbd||1
+new-14|128|1024|1|running|Y|node-01-002|node-01-004|drbd||1
+new-15|128|1024|1|running|Y|node-01-001|node-01-003|drbd||1
+new-16|128|1024|1|running|Y|node-01-008|node-01-005|drbd||1
+new-17|128|1024|1|running|Y|node-01-007|node-01-006|drbd||1
+new-18|128|1024|1|running|Y|node-01-004|node-01-001|drbd||1
+new-19|128|1024|1|running|Y|node-01-003|node-01-002|drbd||1
+new-20|128|1024|1|running|Y|node-01-006|node-01-007|drbd||1
+new-21|128|1024|1|running|Y|node-01-005|node-01-008|drbd||1
+new-22|128|1024|1|running|Y|node-01-002|node-01-003|drbd||1
+new-23|128|1024|1|running|Y|node-01-001|node-01-004|drbd||1
+new-24|128|1024|1|running|Y|node-01-008|node-01-004|drbd||1
+new-25|128|1024|1|running|Y|node-01-007|node-01-003|drbd||1
+new-26|128|1024|1|running|Y|node-01-006|node-01-002|drbd||1
+new-27|128|1024|1|running|Y|node-01-005|node-01-001|drbd||1
+new-28|128|1024|1|running|Y|node-01-004|node-01-008|drbd||1
+new-29|128|1024|1|running|Y|node-01-003|node-01-007|drbd||1
+new-30|128|1024|1|running|Y|node-01-002|node-01-006|drbd||1
+new-31|128|1024|1|running|Y|node-01-001|node-01-005|drbd||1
+new-32|128|1024|1|running|Y|node-01-008|node-01-003|drbd||1
+new-33|128|1024|1|running|Y|node-01-007|node-01-004|drbd||1
+new-34|128|1024|1|running|Y|node-01-006|node-01-001|drbd||1
+new-35|128|1024|1|running|Y|node-01-005|node-01-002|drbd||1
+new-36|128|1024|1|running|Y|node-01-004|node-01-007|drbd||1
+new-37|128|1024|1|running|Y|node-01-003|node-01-008|drbd||1
+new-38|128|1024|1|running|Y|node-01-002|node-01-005|drbd||1
+new-39|128|1024|1|running|Y|node-01-001|node-01-006|drbd||1
+new-40|128|1024|1|running|Y|node-01-008|node-01-002|drbd||1
+new-41|128|1024|1|running|Y|node-01-007|node-01-001|drbd||1
+new-42|128|1024|1|running|Y|node-01-006|node-01-004|drbd||1
+new-43|128|1024|1|running|Y|node-01-005|node-01-003|drbd||1
+new-44|128|1024|1|running|Y|node-01-004|node-01-006|drbd||1
+new-45|128|1024|1|running|Y|node-01-003|node-01-005|drbd||1
+new-46|128|1024|1|running|Y|node-01-002|node-01-008|drbd||1
+new-47|128|1024|1|running|Y|node-01-001|node-01-007|drbd||1
+new-48|128|1024|1|running|Y|node-01-008|node-01-001|drbd||1
+new-49|128|1024|1|running|Y|node-01-007|node-01-002|drbd||1
+new-50|128|1024|1|running|Y|node-01-006|node-01-003|drbd||1
+new-51|128|1024|1|running|Y|node-01-005|node-01-004|drbd||1
+new-52|128|1024|1|running|Y|node-01-004|node-01-005|drbd||1
+new-53|128|1024|1|running|Y|node-01-003|node-01-006|drbd||1
+new-54|128|1024|1|running|Y|node-01-002|node-01-007|drbd||1
+new-55|128|1024|1|running|Y|node-01-001|node-01-008|drbd||1
+new-56|128|1024|1|running|Y|node-01-008|node-01-007|drbd||1
+new-57|128|1024|1|running|Y|node-01-006|node-01-005|drbd||1
+new-58|128|1024|1|running|Y|node-01-004|node-01-003|drbd||1
+new-59|128|1024|1|running|Y|node-01-002|node-01-001|drbd||1
+new-60|128|1024|1|running|Y|node-01-007|node-01-008|drbd||1
+new-61|128|1024|1|running|Y|node-01-005|node-01-006|drbd||1
+new-62|128|1024|1|running|Y|node-01-003|node-01-004|drbd||1
+new-63|128|1024|1|running|Y|node-01-001|node-01-002|drbd||1
+new-64|128|1024|1|running|Y|node-01-008|node-01-006|drbd||1
+new-65|128|1024|1|running|Y|node-01-007|node-01-005|drbd||1
+new-66|128|1024|1|running|Y|node-01-004|node-01-002|drbd||1
+new-67|128|1024|1|running|Y|node-01-003|node-01-001|drbd||1
+new-68|128|1024|1|running|Y|node-01-006|node-01-008|drbd||1
+new-69|128|1024|1|running|Y|node-01-005|node-01-007|drbd||1
+new-70|128|1024|1|running|Y|node-01-002|node-01-004|drbd||1
+new-71|128|1024|1|running|Y|node-01-001|node-01-003|drbd||1
+new-72|128|1024|1|running|Y|node-01-008|node-01-005|drbd||1
+new-73|128|1024|1|running|Y|node-01-007|node-01-006|drbd||1
+new-74|128|1024|1|running|Y|node-01-004|node-01-001|drbd||1
+new-75|128|1024|1|running|Y|node-01-003|node-01-002|drbd||1
+new-76|128|1024|1|running|Y|node-01-006|node-01-007|drbd||1
+new-77|128|1024|1|running|Y|node-01-005|node-01-008|drbd||1
+new-78|128|1024|1|running|Y|node-01-002|node-01-003|drbd||1
+new-79|128|1024|1|running|Y|node-01-001|node-01-004|drbd||1
+new-80|128|1024|1|running|Y|node-01-008|node-01-004|drbd||1
+new-81|128|1024|1|running|Y|node-01-007|node-01-003|drbd||1
+new-82|128|1024|1|running|Y|node-01-006|node-01-002|drbd||1
+new-83|128|1024|1|running|Y|node-01-005|node-01-001|drbd||1
+new-84|128|1024|1|running|Y|node-01-004|node-01-008|drbd||1
+new-85|128|1024|1|running|Y|node-01-003|node-01-007|drbd||1
+new-86|128|1024|1|running|Y|node-01-002|node-01-006|drbd||1
+new-87|128|1024|1|running|Y|node-01-001|node-01-005|drbd||1
+new-88|128|1024|1|running|Y|node-01-008|node-01-003|drbd||1
+new-89|128|1024|1|running|Y|node-01-007|node-01-004|drbd||1
+new-90|128|1024|1|running|Y|node-01-006|node-01-001|drbd||1
+new-91|128|1024|1|running|Y|node-01-005|node-01-002|drbd||1
+new-92|128|1024|1|running|Y|node-01-004|node-01-007|drbd||1
+new-93|128|1024|1|running|Y|node-01-003|node-01-008|drbd||1
+new-94|128|1024|1|running|Y|node-01-002|node-01-005|drbd||1
+new-95|128|1024|1|running|Y|node-01-001|node-01-006|drbd||1
+new-96|128|1024|1|running|Y|node-01-008|node-01-002|drbd||1
+new-97|128|1024|1|running|Y|node-01-007|node-01-001|drbd||1
+new-98|128|1024|1|running|Y|node-01-006|node-01-004|drbd||1
+new-99|128|1024|1|running|Y|node-01-005|node-01-003|drbd||1
+new-100|128|1024|1|running|Y|node-01-004|node-01-006|drbd||1
+new-101|128|1024|1|running|Y|node-01-003|node-01-005|drbd||1
+new-102|128|1024|1|running|Y|node-01-002|node-01-008|drbd||1
+new-103|128|1024|1|running|Y|node-01-001|node-01-007|drbd||1
+new-104|128|1024|1|running|Y|node-01-008|node-01-001|drbd||1
+new-105|128|1024|1|running|Y|node-01-007|node-01-002|drbd||1
+new-106|128|1024|1|running|Y|node-01-006|node-01-003|drbd||1
+new-107|128|1024|1|running|Y|node-01-005|node-01-004|drbd||1
+new-108|128|1024|1|running|Y|node-01-004|node-01-005|drbd||1
+new-109|128|1024|1|running|Y|node-01-003|node-01-006|drbd||1
+new-110|128|1024|1|running|Y|node-01-002|node-01-007|drbd||1
+new-111|128|1024|1|running|Y|node-01-001|node-01-008|drbd||1
+new-112|128|1024|1|running|Y|node-01-008|node-01-007|drbd||1
+new-113|128|1024|1|running|Y|node-01-006|node-01-005|drbd||1
+new-114|128|1024|1|running|Y|node-01-004|node-01-003|drbd||1
+new-115|128|1024|1|running|Y|node-01-002|node-01-001|drbd||1
+new-116|128|1024|1|running|Y|node-01-007|node-01-008|drbd||1
+new-117|128|1024|1|running|Y|node-01-005|node-01-006|drbd||1
+new-118|128|1024|1|running|Y|node-01-003|node-01-004|drbd||1
+new-119|128|1024|1|running|Y|node-01-001|node-01-002|drbd||1
+new-120|128|1024|1|running|Y|node-01-008|node-01-006|drbd||1
+new-121|128|1024|1|running|Y|node-01-007|node-01-005|drbd||1
+new-122|128|1024|1|running|Y|node-01-004|node-01-002|drbd||1
+new-123|128|1024|1|running|Y|node-01-003|node-01-001|drbd||1
+new-124|128|1024|1|running|Y|node-01-006|node-01-008|drbd||1
+new-125|128|1024|1|running|Y|node-01-005|node-01-007|drbd||1
+new-126|128|1024|1|running|Y|node-01-002|node-01-004|drbd||1
+new-127|128|1024|1|running|Y|node-01-001|node-01-003|drbd||1
+
+
+|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd|4.0|32.0
+group-01|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd|4.0|32.0
+group-02|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,12|diskless,file,sharedfile,plain,blockdev,drbd,rbd|4.0|32.0
diff --git a/test/data/htools/invalid-node.data b/test/data/htools/invalid-node.data
new file mode 100644 (file)
index 0000000..9682a6c
--- /dev/null
@@ -0,0 +1,10 @@
+group-01|fake-uuid-01|preferred
+
+node-01-001|1024|0|1024|95367|95367|4|N|fake-uuid-01|1
+node-01-002|1024|0|896|95367|94343|4|N|fake-uuid-01|1
+
+new-0|128|1024|1|running|Y|no-such-node||plain|
+
+
+|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,8|diskless,file,sharedfile,plain,blockdev,drbd,rbd|4.0|32.0
+group-01|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,8|diskless,file,sharedfile,plain,blockdev,drbd,rbd|4.0|32.0
diff --git a/test/data/htools/missing-resources.data b/test/data/htools/missing-resources.data
new file mode 100644 (file)
index 0000000..da1e740
--- /dev/null
@@ -0,0 +1,9 @@
+default|fake-uuid-01|preferred
+
+node1|1024|0|1024|95367|95367|4|N|fake-uuid-01|1
+node2|1024|0|0|95367|0|4|N|fake-uuid-01|1
+
+
+
+|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,8|diskless,file,sharedfile,plain,blockdev,drbd,rbd|4.0|32.0
+default|128,1,1024,1,1,1|128,1,1024,1,1,1|32768,8,1048576,16,8,8|diskless,file,sharedfile,plain,blockdev,drbd,rbd|4.0|32.0
diff --git a/test/data/htools/rapi/groups.json b/test/data/htools/rapi/groups.json
new file mode 100644 (file)
index 0000000..226eed7
--- /dev/null
@@ -0,0 +1,55 @@
+[
+  {
+    "uuid": "uuid-group-1",
+    "tags": [],
+    "ipolicy": {
+      "std": {
+        "cpu-count": 1,
+        "nic-count": 1,
+        "disk-size": 1024,
+        "memory-size": 128,
+        "disk-count": 1,
+        "spindle-use": 1
+      },
+      "min": {
+        "cpu-count": 1,
+        "nic-count": 1,
+        "disk-size": 1024,
+        "memory-size": 128,
+        "disk-count": 1,
+        "spindle-use": 1
+      },
+      "max": {
+        "cpu-count": 8,
+        "nic-count": 8,
+        "disk-size": 1048576,
+        "memory-size": 32768,
+        "disk-count": 16,
+        "spindle-use": 8
+      },
+      "vcpu-ratio": 4.0,
+      "disk-templates": [
+        "sharedfile",
+        "diskless",
+        "plain",
+        "blockdev",
+        "drbd",
+        "file",
+        "rbd"
+      ],
+      "spindle-ratio": 32.0
+    },
+    "node_cnt": 4,
+    "serial_no": 15,
+    "node_list": [
+      "node1",
+      "node2",
+      "node3",
+      "node4"
+    ],
+    "ctime": null,
+    "mtime": 1325251614.671967,
+    "alloc_policy": "preferred",
+    "name": "default"
+  }
+]
diff --git a/test/data/htools/rapi/info.json b/test/data/htools/rapi/info.json
new file mode 100644 (file)
index 0000000..821a320
--- /dev/null
@@ -0,0 +1,150 @@
+{
+  "maintain_node_health": true,
+  "hvparams": {
+    "xen-pvm": {
+      "use_bootloader": false,
+      "migration_mode": "live",
+      "kernel_args": "ro",
+      "migration_port": 8002,
+      "bootloader_args": "",
+      "root_path": "/dev/sda1",
+      "blockdev_prefix": "sd",
+      "bootloader_path": "",
+      "cpu_mask": "all",
+      "kernel_path": "/boot/vmlinuz-2.6-xenU",
+      "initrd_path": "",
+      "reboot_behavior": "reboot"
+    },
+    "xen-hvm": {
+      "nic_type": "rtl8139",
+      "use_localtime": false,
+      "migration_mode": "non-live",
+      "boot_order": "cd",
+      "migration_port": 8002,
+      "cpu_mask": "all",
+      "vnc_bind_address": "0.0.0.0",
+      "reboot_behavior": "reboot",
+      "blockdev_prefix": "hd",
+      "cdrom_image_path": "",
+      "device_model": "/usr/lib/xen/bin/qemu-dm",
+      "pae": true,
+      "vnc_password_file": "/etc/ganeti/vnc-cluster-password",
+      "disk_type": "paravirtual",
+      "kernel_path": "/usr/lib/xen/boot/hvmloader",
+      "acpi": true
+    }
+  },
+  "default_hypervisor": "xen-pvm",
+  "uid_pool": [],
+  "prealloc_wipe_disks": false,
+  "primary_ip_version": 4,
+  "mtime": 1331075221.432734,
+  "os_hvp": {
+    "instance-debootstrap": {
+      "xen-pvm": {
+        "root_path": "/dev/xvda1",
+        "kernel_path": "/boot/vmlinuz-2.6.38"
+      }
+    }
+  },
+  "osparams": {
+    "debootstrap": {
+      "dhcp": "no",
+      "partition_style": "none",
+      "packages": "ssh"
+    }
+  },
+  "shared_file_storage_dir": "",
+  "master_netmask": 32,
+  "uuid": "1616c1cc-f793-499c-b1c5-48264c2d2976",
+  "use_external_mip_script": false,
+  "export_version": 0,
+  "hidden_os": [
+    "lenny"
+  ],
+  "os_api_version": 20,
+  "master": "node4",
+  "nicparams": {
+    "default": {
+      "link": "xen-br0",
+      "mode": "bridged"
+    }
+  },
+  "protocol_version": 2050000,
+  "config_version": 2050000,
+  "software_version": "2.5.0~rc5",
+  "tags": [
+    "htools:iextags:test",
+    "htools:iextags:service-group"
+  ],
+  "ipolicy": {
+    "std": {
+      "nic-count": 1,
+      "disk-size": 1024,
+      "disk-count": 1,
+      "memory-size": 128,
+      "cpu-count": 1,
+      "spindle-use": 1
+    },
+    "min": {
+      "nic-count": 1,
+      "disk-size": 128,
+      "disk-count": 1,
+      "memory-size": 128,
+      "cpu-count": 1,
+      "spindle-use": 1
+    },
+    "max": {
+      "nic-count": 8,
+      "disk-size": 1048576,
+      "disk-count": 16,
+      "memory-size": 32768,
+      "cpu-count": 8,
+      "spindle-use": 8
+    },
+    "vcpu-ratio": 4.0,
+    "disk-templates": [
+      "sharedfile",
+      "diskless",
+      "plain",
+      "blockdev",
+      "drbd",
+      "file",
+      "rbd"
+    ],
+    "spindle-ratio": 32.0
+  },
+  "candidate_pool_size": 3,
+  "file_storage_dir": "/srv/ganeti/file-storage",
+  "blacklisted_os": [],
+  "enabled_hypervisors": [
+    "xen-pvm",
+    "xen-hvm"
+  ],
+  "reserved_lvs": [
+    "xenvg/test"
+  ],
+  "drbd_usermode_helper": "/bin/true",
+  "default_iallocator": "hail",
+  "ctime": 1271079848.3199999,
+  "name": "cluster",
+  "master_netdev": "xen-br0",
+  "ndparams": {
+    "spindle_count": 1,
+    "oob_program": null
+  },
+  "architecture": [
+    "64bit",
+    "x86_64"
+  ],
+  "volume_group_name": "xenvg",
+  "beparams": {
+    "default": {
+      "auto_balance": true,
+      "minmem": 128,
+      "vcpus": 1,
+      "always_failover": false,
+      "maxmem": 128
+    }
+  }
+}
diff --git a/test/data/htools/rapi/instances.json b/test/data/htools/rapi/instances.json
new file mode 100644 (file)
index 0000000..001ed97
--- /dev/null
@@ -0,0 +1,804 @@
+[
+  {
+    "disk_usage": 256,
+    "oper_vcpus": 1,
+    "serial_no": 7,
+    "hvparams": {
+      "root_path": "/dev/xvda1",
+      "kernel_args": "ro",
+      "blockdev_prefix": "sd",
+      "use_bootloader": false,
+      "bootloader_args": "",
+      "bootloader_path": "",
+      "cpu_mask": "all",
+      "kernel_path": "/boot/vmlinuz-ganetixenu-2.6.38",
+      "initrd_path": "",
+      "reboot_behavior": "reboot"
+    },
+    "oper_state": true,
+    "disk_template": "drbd",
+    "mtime": 1330349951.511833,
+    "nic.modes": [
+      "bridged"
+    ],
+    "oper_ram": 128,
+    "pnode": "node3",
+    "nic.bridges": [
+      "xen-br0"
+    ],
+    "status": "running",
+    "custom_hvparams": {},
+    "tags": [],
+    "nic.ips": [
+      null
+    ],
+    "snodes": [
+      "node4"
+    ],
+    "nic.macs": [
+      "aa:00:00:73:20:3e"
+    ],
+    "name": "instance2",
+    "network_port": null,
+    "ctime": 1327334413.084552,
+    "custom_beparams": {},
+    "custom_nicparams": [
+      {}
+    ],
+    "uuid": "4b9ff2a2-3399-4141-b4e1-cde418b1dfec",
+    "disk.sizes": [
+      128
+    ],
+    "admin_state": "up",
+    "nic.links": [
+      "xen-br0"
+    ],
+    "os": "debian-image",
+    "beparams": {
+      "auto_balance": true,
+      "minmem": 128,
+      "vcpus": 1,
+      "always_failover": false,
+      "maxmem": 128,
+      "spindle_use": 1
+    }
+  },
+  {
+    "disk_usage": 384,
+    "oper_vcpus": null,
+    "serial_no": 6,
+    "hvparams": {
+      "root_path": "/dev/xvda1",
+      "kernel_args": "ro",
+      "blockdev_prefix": "sd",
+      "use_bootloader": false,
+      "bootloader_args": "",
+      "bootloader_path": "",
+      "cpu_mask": "all",
+      "kernel_path": "/boot/vmlinuz-ganetixenu-2.6.38",
+      "initrd_path": "",
+      "reboot_behavior": "reboot"
+    },
+    "oper_state": false,
+    "disk_template": "plain",
+    "mtime": 1325681489.4059889,
+    "nic.modes": [
+      "bridged"
+    ],
+    "oper_ram": null,
+    "pnode": "node4",
+    "nic.bridges": [
+      "xen-br0"
+    ],
+    "status": "ADMIN_down",
+    "custom_hvparams": {
+      "root_path": "/dev/xvda1",
+      "kernel_args": "ro",
+      "blockdev_prefix": "sd",
+      "use_bootloader": false,
+      "bootloader_args": "",
+      "bootloader_path": "",
+      "kernel_path": "/boot/vmlinuz-ganetixenu-2.6.38",
+      "initrd_path": ""
+    },
+    "tags": [],
+    "nic.ips": [
+      null
+    ],
+    "snodes": [],
+    "nic.macs": [
+      "aa:00:00:ec:e8:a2"
+    ],
+    "name": "instance3",
+    "network_port": null,
+    "ctime": 1312272250.96,
+    "custom_beparams": {
+      "auto_balance": true,
+      "minmem": 128,
+      "vcpus": 1,
+      "maxmem": 128,
+      "spindle_use": 1
+    },
+    "custom_nicparams": [
+      {
+        "link": "xen-br0",
+        "mode": "bridged"
+      }
+    ],
+    "uuid": "3cecca87-eae7-476c-847c-818a28764989",
+    "disk.sizes": [
+      256,
+      128
+    ],
+    "admin_state": "down",
+    "nic.links": [
+      "xen-br0"
+    ],
+    "os": "debian-image",
+    "beparams": {
+      "auto_balance": true,
+      "minmem": 128,
+      "vcpus": 1,
+      "always_failover": false,
+      "maxmem": 128,
+      "spindle_use": 1
+    }
+  },
+  {
+    "disk_usage": 2176,
+    "oper_vcpus": null,
+    "serial_no": 23,
+    "hvparams": {
+      "root_path": "/dev/xvda1",
+      "kernel_args": "ro",
+      "blockdev_prefix": "sd",
+      "use_bootloader": false,
+      "bootloader_args": "",
+      "bootloader_path": "",
+      "cpu_mask": "all",
+      "kernel_path": "/boot/vmlinuz-ganetixenu-2.6.38",
+      "initrd_path": "",
+      "reboot_behavior": "reboot"
+    },
+    "oper_state": false,
+    "disk_template": "drbd",
+    "mtime": 1325681487.384176,
+    "nic.modes": [
+      "bridged"
+    ],
+    "oper_ram": null,
+    "pnode": "node4",
+    "nic.bridges": [
+      "xen-br0"
+    ],
+    "status": "ADMIN_down",
+    "custom_hvparams": {},
+    "tags": [
+      "service-group:dns"
+    ],
+    "nic.ips": [
+      null
+    ],
+    "snodes": [
+      "node3"
+    ],
+    "nic.macs": [
+      "aa:00:00:62:b0:76"
+    ],
+    "name": "instance4",
+    "network_port": null,
+    "ctime": 1274885795.4000001,
+    "custom_beparams": {},
+    "custom_nicparams": [
+      {}
+    ],
+    "uuid": "33f4c063-bb65-41b2-af29-d8a631201bd7",
+    "disk.sizes": [
+      2048
+    ],
+    "admin_state": "down",
+    "nic.links": [
+      "xen-br0"
+    ],
+    "os": "lenny-image",
+    "beparams": {
+      "auto_balance": true,
+      "minmem": 128,
+      "vcpus": 1,
+      "always_failover": false,
+      "maxmem": 128,
+      "spindle_use": 1
+    }
+  },
+  {
+    "disk_usage": 256,
+    "oper_vcpus": null,
+    "serial_no": 9,
+    "hvparams": {
+      "spice_password_file": "",
+      "spice_use_tls": false,
+      "spice_use_vdagent": true,
+      "nic_type": "paravirtual",
+      "vnc_bind_address": "0.0.0.0",
+      "cdrom2_image_path": "",
+      "usb_mouse": "",
+      "spice_streaming_video": "",
+      "use_chroot": false,
+      "spice_tls_ciphers": "HIGH:-DES:-3DES:-EXPORT:-ADH",
+      "migration_downtime": 30,
+      "floppy_image_path": "",
+      "security_model": "none",
+      "cdrom_image_path": "",
+      "spice_ip_version": 0,
+      "vhost_net": false,
+      "cpu_mask": "all",
+      "disk_cache": "default",
+      "kernel_path": "/boot/vmlinuz-2.6.38-gg426-generic",
+      "initrd_path": "/boot/initrd.img-2.6.38-gg426-generic",
+      "spice_jpeg_wan_compression": "",
+      "vnc_tls": false,
+      "cdrom_disk_type": "",
+      "use_localtime": false,
+      "security_domain": "",
+      "serial_console": false,
+      "spice_bind": "",
+      "spice_zlib_glz_wan_compression": "",
+      "kvm_flag": "",
+      "vnc_password_file": "",
+      "disk_type": "paravirtual",
+      "vnc_x509_verify": false,
+      "spice_image_compression": "",
+      "spice_playback_compression": true,
+      "kernel_args": "ro",
+      "root_path": "/dev/vda1",
+      "vnc_x509_path": "",
+      "acpi": true,
+      "keymap": "",
+      "boot_order": "disk",
+      "mem_path": "",
+      "reboot_behavior": "reboot"
+    },
+    "oper_state": false,
+    "disk_template": "plain",
+    "mtime": 1325681492.191576,
+    "nic.modes": [
+      "bridged"
+    ],
+    "oper_ram": null,
+    "pnode": "node4",
+    "nic.bridges": [
+      "xen-br0"
+    ],
+    "status": "ADMIN_down",
+    "custom_hvparams": {},
+    "tags": [],
+    "nic.ips": [
+      null
+    ],
+    "snodes": [],
+    "nic.macs": [
+      "aa:00:00:3f:6d:e3"
+    ],
+    "name": "instance8",
+    "network_port": 12111,
+    "ctime": 1311771325.6600001,
+    "custom_beparams": {},
+    "custom_nicparams": [
+      {}
+    ],
+    "uuid": "1ea53cc3-cc69-43da-b261-f22ac47896ea",
+    "disk.sizes": [
+      256
+    ],
+    "admin_state": "down",
+    "nic.links": [
+      "xen-br0"
+    ],
+    "os": "debian-image",
+    "beparams": {
+      "auto_balance": true,
+      "minmem": 128,
+      "vcpus": 1,
+      "always_failover": false,
+      "maxmem": 128,
+      "spindle_use": 1
+    }
+  },
+  {
+    "disk_usage": 256,
+    "oper_vcpus": null,
+    "serial_no": 31,
+    "hvparams": {
+      "root_path": "/dev/sda1",
+      "kernel_args": "ro",
+      "blockdev_prefix": "sd",
+      "use_bootloader": false,
+      "bootloader_args": "",
+      "bootloader_path": "",
+      "cpu_mask": "all",
+      "kernel_path": "/boot/vmlinuz-2.6-xenU",
+      "initrd_path": "",
+      "reboot_behavior": "reboot"
+    },
+    "oper_state": false,
+    "disk_template": "drbd",
+    "mtime": 1325681490.685926,
+    "nic.modes": [
+      "bridged"
+    ],
+    "oper_ram": null,
+    "pnode": "node3",
+    "nic.bridges": [
+      "xen-br0"
+    ],
+    "status": "ADMIN_down",
+    "custom_hvparams": {
+      "root_path": "/dev/sda1",
+      "kernel_args": "ro",
+      "use_bootloader": false,
+      "bootloader_args": "",
+      "bootloader_path": "",
+      "kernel_path": "/boot/vmlinuz-2.6-xenU",
+      "initrd_path": ""
+    },
+    "tags": [
+      "gogu:test"
+    ],
+    "nic.ips": [
+      null
+    ],
+    "snodes": [
+      "node4"
+    ],
+    "nic.macs": [
+      "aa:00:00:10:d2:01"
+    ],
+    "name": "instance9",
+    "network_port": null,
+    "ctime": 1271937489.76,
+    "custom_beparams": {
+      "auto_balance": true,
+      "minmem": 128,
+      "vcpus": 1,
+      "maxmem": 128,
+      "spindle_use": 1
+    },
+    "custom_nicparams": [
+      {}
+    ],
+    "uuid": "4927ac66-a3c5-45c6-be39-97e2b119557e",
+    "disk.sizes": [
+      128
+    ],
+    "admin_state": "down",
+    "nic.links": [
+      "xen-br0"
+    ],
+    "os": "lenny-image",
+    "beparams": {
+      "auto_balance": true,
+      "minmem": 128,
+      "vcpus": 1,
+      "always_failover": false,
+      "maxmem": 128,
+      "spindle_use": 1
+    }
+  },
+  {
+    "disk_usage": 512,
+    "oper_vcpus": null,
+    "serial_no": 11,
+    "hvparams": {
+      "root_path": "/dev/sda1",
+      "kernel_args": "ro",
+      "blockdev_prefix": "sd",
+      "use_bootloader": false,
+      "bootloader_args": "",
+      "bootloader_path": "",
+      "cpu_mask": "all",
+      "kernel_path": "/boot/vmlinuz-2.6-xenU",
+      "initrd_path": "",
+      "reboot_behavior": "reboot"
+    },
+    "oper_state": false,
+    "disk_template": "plain",
+    "mtime": 1325681493.0002201,
+    "nic.modes": [
+      "bridged"
+    ],
+    "oper_ram": null,
+    "pnode": "node4",
+    "nic.bridges": [
+      "xen-br1"
+    ],
+    "status": "ADMIN_down",
+    "custom_hvparams": {},
+    "tags": [],
+    "nic.ips": [
+      null
+    ],
+    "snodes": [],
+    "nic.macs": [
+      "aa:00:00:7f:8c:9c"
+    ],
+    "name": "instance13",
+    "network_port": null,
+    "ctime": 1305129727.7,
+    "custom_beparams": {},
+    "custom_nicparams": [
+      {
+        "link": "xen-br1"
+      }
+    ],
+    "uuid": "b864e453-f072-41fe-9973-7673c2161e34",
+    "disk.sizes": [
+      512
+    ],
+    "admin_state": "down",
+    "nic.links": [
+      "xen-br1"
+    ],
+    "os": "busybox",
+    "beparams": {
+      "auto_balance": true,
+      "minmem": 128,
+      "vcpus": 1,
+      "always_failover": false,
+      "maxmem": 128,
+      "spindle_use": 1
+    }
+  },
+  {
+    "disk_usage": 256,
+    "oper_vcpus": null,
+    "serial_no": 11,
+    "hvparams": {
+      "root_path": "/dev/xvda1",
+      "kernel_args": "ro",
+      "blockdev_prefix": "sd",
+      "use_bootloader": false,
+      "bootloader_args": "",
+      "bootloader_path": "",
+      "cpu_mask": "all",
+      "kernel_path": "/boot/vmlinuz-ganetixenu-2.6.38",
+      "initrd_path": "",
+      "reboot_behavior": "reboot"
+    },
+    "oper_state": false,
+    "disk_template": "drbd",
+    "mtime": 1325681493.8268771,
+    "nic.modes": [
+      "bridged"
+    ],
+    "oper_ram": null,
+    "pnode": "node3",
+    "nic.bridges": [
+      "xen-br0"
+    ],
+    "status": "ADMIN_down",
+    "custom_hvparams": {},
+    "tags": [],
+    "nic.ips": [
+      null
+    ],
+    "snodes": [
+      "node4"
+    ],
+    "nic.macs": [
+      "aa:00:00:eb:0b:a5"
+    ],
+    "name": "instance14",
+    "network_port": null,
+    "ctime": 1312285580.27,
+    "custom_beparams": {},
+    "custom_nicparams": [
+      {}
+    ],
+    "uuid": "e9dae1c9-b4cb-4f11-b0e9-65931a6b3524",
+    "disk.sizes": [
+      128
+    ],
+    "admin_state": "down",
+    "nic.links": [
+      "xen-br0"
+    ],
+    "os": "debian-image",
+    "beparams": {
+      "auto_balance": true,
+      "minmem": 128,
+      "vcpus": 1,
+      "always_failover": false,
+      "maxmem": 128,
+      "spindle_use": 1
+    }
+  },
+  {
+    "disk_usage": 128,
+    "oper_vcpus": null,
+    "serial_no": 9,
+    "hvparams": {
+      "root_path": "/dev/sda1",
+      "kernel_args": "ro",
+      "blockdev_prefix": "sd",
+      "use_bootloader": false,
+      "bootloader_args": "",
+      "bootloader_path": "",
+      "cpu_mask": "all",
+      "kernel_path": "/boot/vmlinuz-2.6-xenU",
+      "initrd_path": "",
+      "reboot_behavior": "reboot"
+    },
+    "oper_state": false,
+    "disk_template": "plain",
+    "mtime": 1325681491.0986331,
+    "nic.modes": [
+      "bridged"
+    ],
+    "oper_ram": null,
+    "pnode": "node4",
+    "nic.bridges": [
+      "xen-br0"
+    ],
+    "status": "ADMIN_down",
+    "custom_hvparams": {},
+    "tags": [],
+    "nic.ips": [
+      null
+    ],
+    "snodes": [],
+    "nic.macs": [
+      "aa:00:00:55:94:93"
+    ],
+    "name": "instance18",
+    "network_port": null,
+    "ctime": 1297176343.1700001,
+    "custom_beparams": {
+      "minmem": 8192,
+      "maxmem": 8192
+    },
+    "custom_nicparams": [
+      {}
+    ],
+    "uuid": "2f14bc3b-8448-4b2f-a592-d7a216244b22",
+    "disk.sizes": [
+      128
+    ],
+    "admin_state": "down",
+    "nic.links": [
+      "xen-br0"
+    ],
+    "os": "busybox",
+    "beparams": {
+      "auto_balance": true,
+      "minmem": 8192,
+      "vcpus": 1,
+      "always_failover": false,
+      "maxmem": 8192,
+      "spindle_use": 1
+    }
+  },
+  {
+    "disk_usage": 256,
+    "oper_vcpus": null,
+    "serial_no": 10,
+    "hvparams": {
+      "root_path": "/dev/xvda1",
+      "kernel_args": "ro",
+      "blockdev_prefix": "sd",
+      "use_bootloader": false,
+      "bootloader_args": "",
+      "bootloader_path": "",
+      "cpu_mask": "all",
+      "kernel_path": "/boot/vmlinuz-ganetixenu-2.6.38",
+      "initrd_path": "",
+      "reboot_behavior": "reboot"
+    },
+    "oper_state": false,
+    "disk_template": "drbd",
+    "mtime": 1325681491.5785329,
+    "nic.modes": [
+      "bridged"
+    ],
+    "oper_ram": null,
+    "pnode": "node3",
+    "nic.bridges": [
+      "xen-br0"
+    ],
+    "status": "ADMIN_down",
+    "custom_hvparams": {},
+    "tags": [],
+    "nic.ips": [
+      null
+    ],
+    "snodes": [
+      "node4"
+    ],
+    "nic.macs": [
+      "aa:00:00:15:92:6f"
+    ],
+    "name": "instance19",
+    "network_port": null,
+    "ctime": 1312464490.7,
+    "custom_beparams": {},
+    "custom_nicparams": [
+      {}
+    ],
+    "uuid": "624c1844-82a2-474e-bdaf-1bafa820fdcf",
+    "disk.sizes": [
+      128
+    ],
+    "admin_state": "down",
+    "nic.links": [
+      "xen-br0"
+    ],
+    "os": "debian-image",
+    "beparams": {
+      "auto_balance": true,
+      "minmem": 128,
+      "vcpus": 1,
+      "always_failover": false,
+      "maxmem": 128,
+      "spindle_use": 1
+    }
+  },
+  {
+    "disk_usage": 512,
+    "oper_vcpus": null,
+    "serial_no": 14,
+    "hvparams": {
+      "spice_password_file": "",
+      "spice_use_tls": false,
+      "spice_use_vdagent": true,
+      "nic_type": "paravirtual",
+      "vnc_bind_address": "0.0.0.0",
+      "cdrom2_image_path": "",
+      "usb_mouse": "",
+      "spice_streaming_video": "",
+      "use_chroot": false,
+      "spice_tls_ciphers": "HIGH:-DES:-3DES:-EXPORT:-ADH",
+      "migration_downtime": 30,
+      "floppy_image_path": "",
+      "security_model": "none",
+      "cdrom_image_path": "",
+      "spice_ip_version": 0,
+      "vhost_net": false,
+      "cpu_mask": "all",
+      "disk_cache": "default",
+      "kernel_path": "/boot/vmlinuz-2.6.38-gg426-generic",
+      "initrd_path": "/boot/initrd.img-2.6.38-gg426-generic",
+      "spice_jpeg_wan_compression": "",
+      "vnc_tls": false,
+      "cdrom_disk_type": "",
+      "use_localtime": false,
+      "security_domain": "",
+      "serial_console": false,
+      "spice_bind": "",
+      "spice_zlib_glz_wan_compression": "",
+      "kvm_flag": "",
+      "vnc_password_file": "",
+      "disk_type": "paravirtual",
+      "vnc_x509_verify": false,
+      "spice_image_compression": "",
+      "spice_playback_compression": true,
+      "kernel_args": "ro",
+      "root_path": "/dev/vda1",
+      "vnc_x509_path": "",
+      "acpi": true,
+      "keymap": "",
+      "boot_order": "disk",
+      "mem_path": "",
+      "reboot_behavior": "reboot"
+    },
+    "oper_state": false,
+    "disk_template": "plain",
+    "mtime": 1325681494.699162,
+    "nic.modes": [
+      "bridged"
+    ],
+    "oper_ram": null,
+    "pnode": "node4",
+    "nic.bridges": [
+      "xen-br0"
+    ],
+    "status": "ADMIN_down",
+    "custom_hvparams": {},
+    "tags": [],
+    "nic.ips": [
+      null
+    ],
+    "snodes": [],
+    "nic.macs": [
+      "aa:00:00:db:2a:6d"
+    ],
+    "name": "instance20",
+    "network_port": 12107,
+    "ctime": 1305208955.75,
+    "custom_beparams": {},
+    "custom_nicparams": [
+      {
+        "link": "xen-br0"
+      }
+    ],
+    "uuid": "4f65c14d-be87-4303-a8dc-ba1b86e2a3b3",
+    "disk.sizes": [
+      512
+    ],
+    "admin_state": "down",
+    "nic.links": [
+      "xen-br0"
+    ],
+    "os": "lenny-image+default",
+    "beparams": {
+      "auto_balance": true,
+      "minmem": 128,
+      "vcpus": 1,
+      "always_failover": false,
+      "maxmem": 128,
+      "spindle_use": 1
+    }
+  },
+  {
+    "disk_usage": 256,
+    "oper_vcpus": null,
+    "serial_no": 10,
+    "hvparams": {
+      "root_path": "/dev/xvda1",
+      "kernel_args": "ro",
+      "blockdev_prefix": "sd",
+      "use_bootloader": false,
+      "bootloader_args": "",
+      "bootloader_path": "",
+      "cpu_mask": "all",
+      "kernel_path": "/boot/vmlinuz-ganetixenu-2.6.38",
+      "initrd_path": "",
+      "reboot_behavior": "reboot"
+    },
+    "oper_state": false,
+    "disk_template": "drbd",
+    "mtime": 1325681489.0591741,
+    "nic.modes": [
+      "bridged"
+    ],
+    "oper_ram": null,
+    "pnode": "node3",
+    "nic.bridges": [
+      "xen-br0"
+    ],
+    "status": "ADMIN_down",
+    "custom_hvparams": {},
+    "tags": [],
+    "nic.ips": [
+      null
+    ],
+    "snodes": [
+      "node4"
+    ],
+    "nic.macs": [
+      "aa:00:00:cb:96:c1"
+    ],
+    "name": "instance21",
+    "network_port": null,
+    "ctime": 1312552008.1199999,
+    "custom_beparams": {},
+    "custom_nicparams": [
+      {}
+    ],
+    "uuid": "6f2f7824-8392-408e-ac54-c938f4fb0638",
+    "disk.sizes": [
+      128
+    ],
+    "admin_state": "down",
+    "nic.links": [
+      "xen-br0"
+    ],
+    "os": "debian-image",
+    "beparams": {
+      "auto_balance": true,
+      "minmem": 128,
+      "vcpus": 1,
+      "always_failover": false,
+      "maxmem": 128,
+      "spindle_use": 1
+    }
+  }
+]
diff --git a/test/data/htools/rapi/nodes.json b/test/data/htools/rapi/nodes.json
new file mode 100644 (file)
index 0000000..c788728
--- /dev/null
@@ -0,0 +1,156 @@
+[
+  {
+    "cnodes": 2,
+    "csockets": 2,
+    "ctime": 1324472016.2968869,
+    "ctotal": 4,
+    "dfree": 1377280,
+    "drained": false,
+    "dtotal": 1377280,
+    "group.uuid": "uuid-group-1",
+    "master_candidate": true,
+    "master_capable": true,
+    "mfree": 31389,
+    "mnode": 1017,
+    "mtime": 1331075221.432734,
+    "mtotal": 32763,
+    "name": "node1",
+    "offline": false,
+    "pinst_cnt": 0,
+    "pinst_list": [],
+    "pip": "192.168.1.1",
+    "role": "C",
+    "serial_no": 3,
+    "sinst_cnt": 0,
+    "sinst_list": [],
+    "sip": "192.168.1.2",
+    "tags": [],
+    "uuid": "7750ef3d-450f-4724-9d3d-8726d6335417",
+    "vm_capable": true,
+    "ndparams": {
+      "spindle_count": 1,
+      "oob_program": null
+    }
+  },
+  {
+    "cnodes": 2,
+    "csockets": 2,
+    "ctime": 1324472016.2968869,
+    "ctotal": 4,
+    "dfree": 1376640,
+    "drained": false,
+    "dtotal": 1377280,
+    "group.uuid": "uuid-group-1",
+    "master_candidate": true,
+    "master_capable": true,
+    "mfree": 31746,
+    "mnode": 1017,
+    "mtime": 1331075221.432734,
+    "mtotal": 32763,
+    "name": "node2",
+    "offline": false,
+    "pinst_cnt": 0,
+    "pinst_list": [],
+    "pip": "192.168.1.2",
+    "role": "C",
+    "serial_no": 3,
+    "sinst_cnt": 0,
+    "sinst_list": [],
+    "sip": "192.168.2.2",
+    "tags": [],
+    "uuid": "7750ef3d-450f-4724-9d3d-8726d6335417",
+    "vm_capable": true,
+    "ndparams": {
+      "spindle_count": 1,
+      "oob_program": null
+    }
+  },
+  {
+    "cnodes": 2,
+    "dfree": 1373336,
+    "drained": false,
+    "dtotal": 1377304,
+    "mfree": 31234,
+    "mtime": 1331075172.0123219,
+    "pip": "192.168.1.3",
+    "serial_no": 129,
+    "sinst_cnt": 1,
+    "sip": "192.168.2.3",
+    "uuid": "2c7acf04-599d-4707-aba4-bf07a2685f63",
+    "sinst_list": [
+      "instance4"
+    ],
+    "csockets": 2,
+    "role": "C",
+    "ctotal": 4,
+    "offline": false,
+    "vm_capable": true,
+    "pinst_cnt": 5,
+    "mtotal": 32763,
+    "tags": [],
+    "group.uuid": "uuid-group-1",
+    "master_capable": true,
+    "name": "node3",
+    "master_candidate": true,
+    "ctime": 1271425438.5,
+    "mnode": 1017,
+    "pinst_list": [
+      "instance14",
+      "instance19",
+      "instance2",
+      "instance21",
+      "instance9"
+    ],
+    "ndparams": {
+      "spindle_count": 1,
+      "oob_program": null
+    }
+  },
+  {
+    "cnodes": 2,
+    "dfree": 1371520,
+    "drained": false,
+    "dtotal": 1377280,
+    "mfree": 31746,
+    "mtime": 1318339824.54,
+    "pip": "192.168.1.4",
+    "serial_no": 8,
+    "sinst_cnt": 5,
+    "sip": "192.168.2.4",
+    "uuid": "f25357c1-7fee-4471-b8a9-c7f28669e439",
+    "sinst_list": [
+      "instance2",
+      "instance21",
+      "instance14",
+      "instance9",
+      "instance19"
+    ],
+    "csockets": 2,
+    "role": "M",
+    "ctotal": 4,
+    "offline": false,
+    "vm_capable": true,
+    "pinst_cnt": 7,
+    "mtotal": 32763,
+    "tags": [],
+    "group.uuid": "uuid-group-1",
+    "master_capable": true,
+    "name": "node4",
+    "master_candidate": true,
+    "ctime": 1309185898.51,
+    "mnode": 1017,
+    "pinst_list": [
+      "instance20",
+      "instance3",
+      "instance15",
+      "instance4",
+      "instance13",
+      "instance8",
+      "instance18"
+    ],
+    "ndparams": {
+      "spindle_count": 1,
+      "oob_program": null
+    }
+  }
+]
diff --git a/test/data/ovfdata/compr_disk.vmdk.gz b/test/data/ovfdata/compr_disk.vmdk.gz
new file mode 100644 (file)
index 0000000..3fcb2de
Binary files /dev/null and b/test/data/ovfdata/compr_disk.vmdk.gz differ
diff --git a/test/data/ovfdata/config.ini b/test/data/ovfdata/config.ini
new file mode 100644 (file)
index 0000000..7d0c0f5
--- /dev/null
@@ -0,0 +1,27 @@
+[instance]
+disk0_dump = rawdisk.raw
+nic0_mode = routed
+name = ganeti-test-xen
+hypervisor = xen-pvm
+disk_count = 1
+nic0_mac = aa:00:00:d8:2c:1e
+nic_count = 1
+nic0_link = br0
+nic0_ip = None
+disk0_ivname = disk/0
+disk0_size = 0
+
+[hypervisor]
+root-path = /dev/sda
+kernel_args = ro
+
+[export]
+version = 0
+os = lenny-image
+
+[os]
+
+[backend]
+auto_balance = False
+vcpus = 1
+memory = 512
diff --git a/test/data/ovfdata/corrupted_resources.ovf b/test/data/ovfdata/corrupted_resources.ovf
new file mode 100644 (file)
index 0000000..480de2b
--- /dev/null
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--Generated by VMware ovftool 2.0.1 (build-260188), User: , UTC time: 2011-08-17T15:12:11.715742Z-->
+<Envelope vmw:buildId="build-260188" xmlns="http://schemas.dmtf.org/ovf/envelope/1" xmlns:cim="http://schemas.dmtf.org/wbem/wscim/1/common" xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1" xmlns:rasd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData" xmlns:vmw="http://www.vmware.com/schema/ovf" xmlns:vssd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+  <References>
+    <File ovf:href="other_disk.vmdk" ovf:id="file1" ovf:size="761627136"/>
+  </References>
+  <DiskSection>
+    <Info>Virtual disk information</Info>
+    <Disk ovf:capacity="16514" ovf:capacityAllocationUnits="byte * 2^20" ovf:diskId="vmdisk1" ovf:fileRef="file1" ovf:format="http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized" ovf:populatedSize="2042953728"/>
+  </DiskSection>
+  <NetworkSection>
+    <Info>The list of logical networks</Info>
+    <Network ovf:name="bridged">
+      <Description>The bridged network</Description>
+    </Network>
+  </NetworkSection>
+  <VirtualSystem ovf:id="vm">
+    <Info>A virtual machine</Info>
+    <Name>AyertiennaSUSE.x86_64-0.0.2</Name>
+    <OperatingSystemSection ovf:id="83" vmw:osType="suse64Guest">
+      <Info>The kind of installed guest operating system</Info>
+    </OperatingSystemSection>
+    <VirtualHardwareSection>
+      <Info>Virtual hardware requirements</Info>
+      <System>
+        <vssd:ElementName>Virtual Hardware Family</vssd:ElementName>
+        <vssd:InstanceID>0</vssd:InstanceID>
+        <vssd:VirtualSystemIdentifier>AyertiennaSUSE.x86_64-0.0.2</vssd:VirtualSystemIdentifier>
+        <vssd:VirtualSystemType>vmx-04</vssd:VirtualSystemType>
+      </System>
+      <Item>
+        <rasd:AllocationUnits>hertz * 10^6</rasd:AllocationUnits>
+        <rasd:Description>Number of Virtual CPUs</rasd:Description>
+        <rasd:ElementName>1 virtual CPU(s)</rasd:ElementName>
+        <rasd:InstanceID>1</rasd:InstanceID>
+        <rasd:ResourceType>3</rasd:ResourceType>
+        <rasd:VirtualQuantity>1</rasd:VirtualQuantity>
+      </Item>
+      <Item>
+        <rasd:AllocationUnits>byte * 2^20</rasd:AllocationUnits>
+        <rasd:Description>Memory Size</rasd:Description>
+        <rasd:ElementName>512MB of memory</rasd:ElementName>
+        <rasd:InstanceID>2</rasd:InstanceID>
+        <rasd:ResourceType>4</rasd:ResourceType>
+        <rasd:VirtualQuantity>512</rasd:VirtualQuantity>
+      </Item>
+      <Item>
+        <rasd:Address>0</rasd:Address>
+        <rasd:Description>SCSI Controller</rasd:Description>
+        <rasd:ElementName>scsiController0</rasd:ElementName>
+        <rasd:InstanceID>4</rasd:InstanceID>
+        <rasd:ResourceSubType>lsilogic</rasd:ResourceSubType>
+        <rasd:ResourceType>6</rasd:ResourceType>
+      </Item>
+      <Item>
+        <rasd:Address>0</rasd:Address>
+        <rasd:Description>IDE Controller</rasd:Description>
+        <rasd:ElementName>ideController0</rasd:ElementName>
+        <rasd:InstanceID>5</rasd:InstanceID>
+        <rasd:ResourceType>5</rasd:ResourceType>
+      </Item>
+      <Item>
+        <rasd:AddressOnParent>0</rasd:AddressOnParent>
+        <rasd:ElementName>disk1</rasd:ElementName>
+        <rasd:HostResource>ovf:/disk/vmdisk1</rasd:HostResource>
+        <rasd:InstanceID>8</rasd:InstanceID>
+        <rasd:Parent>4</rasd:Parent>
+        <rasd:ResourceType>17</rasd:ResourceType>
+      </Item>
+      <Item>
+        <rasd:AddressOnParent>2</rasd:AddressOnParent>
+        <rasd:AutomaticAllocation>true</rasd:AutomaticAllocation>
+        <rasd:Connection>bridged</rasd:Connection>
+        <rasd:Description>E1000 ethernet adapter on &quot;bridged&quot;</rasd:Description>
+        <rasd:ElementName>ethernet0</rasd:ElementName>
+        <rasd:InstanceID>9</rasd:InstanceID>
+        <rasd:ResourceSubType>E1000</rasd:ResourceSubType>
+        <rasd:ResourceType>10</rasd:ResourceType>
+      </Item>
+    </VirtualHardwareSection>
+  </VirtualSystem>
+</Envelope>
+
diff --git a/test/data/ovfdata/empty.ini b/test/data/ovfdata/empty.ini
new file mode 100644 (file)
index 0000000..1bb2d17
--- /dev/null
@@ -0,0 +1,5 @@
+[instance]
+[hypervisor]
+[export]
+[os]
+[backend]
\ No newline at end of file
diff --git a/test/data/ovfdata/empty.ovf b/test/data/ovfdata/empty.ovf
new file mode 100644 (file)
index 0000000..b1b05f3
--- /dev/null
@@ -0,0 +1,16 @@
+<?xml version="1.0"?>
+<Envelope ovf:version="1.0" xml:lang="en-US" xmlns="http://schemas.dmtf.org/ovf/envelope/1" xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1" xmlns:rasd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData" xmlns:vssd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+  <References>
+  </References>
+  <DiskSection>
+  </DiskSection>
+  <NetworkSection>
+  </NetworkSection>
+  <VirtualSystem>
+    <Info>A virtual machine</Info>
+    <OperatingSystemSection>
+    </OperatingSystemSection>
+    <VirtualHardwareSection>
+    </VirtualHardwareSection>
+  </VirtualSystem>
+</Envelope>
diff --git a/test/data/ovfdata/ganeti.mf b/test/data/ovfdata/ganeti.mf
new file mode 100644 (file)
index 0000000..107ec2b
--- /dev/null
@@ -0,0 +1,2 @@
+SHA1(ganeti.ovf)= d298200d9044c54b0fde13efaa90e564badc5961
+SHA1(new_disk.vmdk)= 711c48f14c934228b8e117d036c913cdb9d63305
diff --git a/test/data/ovfdata/ganeti.ovf b/test/data/ovfdata/ganeti.ovf
new file mode 100644 (file)
index 0000000..e664da8
--- /dev/null
@@ -0,0 +1,93 @@
+<?xml version="1.0"?>
+<Envelope ovf:version="1.0" xml:lang="en-US" xmlns="http://schemas.dmtf.org/ovf/envelope/1" xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1" xmlns:rasd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData" xmlns:vssd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:gnt="http://ganeti">
+  <References>
+    <File ovf:href="new_disk.vmdk" ovf:id="file1"/>
+  </References>
+  <DiskSection>
+    <Info>List of the virtual disks used in the package</Info>
+    <Disk ovf:diskId="vmdisk1" ovf:fileRef="file1" ovf:format="http://www.vmware.com/specifications/vmdk.html#sparse"/>
+  </DiskSection>
+  <gnt:GanetiSection>
+    <gnt:Version>0</gnt:Version>
+    <gnt:AutoBalance>False</gnt:AutoBalance>
+    <gnt:Tags></gnt:Tags>
+    <gnt:DiskTemplate>plain</gnt:DiskTemplate>
+    <gnt:OperatingSystem>
+      <gnt:Name>lenny-image</gnt:Name>
+    </gnt:OperatingSystem>
+    <gnt:Network>
+      <gnt:Nic ovf:name="routed">
+        <gnt:Mode>bridged</gnt:Mode>
+        <gnt:MACAddress>aa:00:00:d8:2c:1e</gnt:MACAddress>
+        <gnt:IPAddress>none</gnt:IPAddress>
+        <gnt:Link>xen-br0</gnt:Link>
+      </gnt:Nic>
+    </gnt:Network>
+    <gnt:Hypervisor>
+      <gnt:Name>xen-pvm</gnt:Name>
+      <gnt:Parameters>
+        <gnt:root-path>/dev/sda</gnt:root-path>
+        <gnt:kernel_args>ro</gnt:kernel_args>
+      </gnt:Parameters>
+    </gnt:Hypervisor>
+  </gnt:GanetiSection>
+  <NetworkSection>
+    <Info>Logical networks used in the package</Info>
+    <Network ovf:name="routed">
+      <Description>Logical network used by this appliance.</Description>
+    </Network>
+  </NetworkSection>
+  <VirtualSystem ovf:id="New-shiny-instance">
+    <Info>A virtual machine</Info>
+    <Name>ganeti-test-xen</Name>
+    <OperatingSystemSection ovf:id="93">
+      <Info>The kind of installed guest operating system</Info>
+      <Description>Ubuntu</Description>
+    </OperatingSystemSection>
+    <VirtualHardwareSection>
+      <Info>Virtual hardware requirements for a virtual machine</Info>
+      <System>
+        <vssd:ElementName>Virtual Hardware Family</vssd:ElementName>
+        <vssd:InstanceID>0</vssd:InstanceID>
+        <vssd:VirtualSystemIdentifier>Ubuntu-freshly-created</vssd:VirtualSystemIdentifier>
+        <vssd:VirtualSystemType>virtualbox-2.2</vssd:VirtualSystemType>
+      </System>
+      <Item>
+        <rasd:Caption>1 virtual CPU</rasd:Caption>
+        <rasd:ElementName>1 virtual CPU</rasd:ElementName>
+        <rasd:Description>Number of virtual CPUs</rasd:Description>
+        <rasd:InstanceID>1</rasd:InstanceID>
+        <rasd:ResourceType>3</rasd:ResourceType>
+        <rasd:VirtualQuantity>1</rasd:VirtualQuantity>
+      </Item>
+      <Item>
+        <rasd:Caption>2048 MB of memory</rasd:Caption>
+        <rasd:ElementName>2048 MB of memory</rasd:ElementName>
+        <rasd:Description>Memory Size</rasd:Description>
+        <rasd:InstanceID>2</rasd:InstanceID>
+        <rasd:ResourceType>4</rasd:ResourceType>
+        <rasd:AllocationUnits>MegaBytes</rasd:AllocationUnits>
+        <rasd:VirtualQuantity>2048</rasd:VirtualQuantity>
+      </Item>
+      <Item>
+        <rasd:Caption>Ethernet adapter on 'NAT'</rasd:Caption>
+        <rasd:ElementName>Ethernet adapter on 'NAT'</rasd:ElementName>
+        <rasd:InstanceID>5</rasd:InstanceID>
+        <rasd:ResourceType>10</rasd:ResourceType>
+        <rasd:ResourceSubType>PCNet32</rasd:ResourceSubType>
+        <rasd:AutomaticAllocation>true</rasd:AutomaticAllocation>
+        <rasd:Connection></rasd:Connection>
+      </Item>
+      <Item>
+        <rasd:Caption>disk1</rasd:Caption>
+        <rasd:ElementName>disk1</rasd:ElementName>
+        <rasd:Description>Disk Image</rasd:Description>
+        <rasd:InstanceID>7</rasd:InstanceID>
+        <rasd:ResourceType>17</rasd:ResourceType>
+        <rasd:HostResource>/disk/vmdisk1</rasd:HostResource>
+        <rasd:Parent>3</rasd:Parent>
+        <rasd:AddressOnParent>0</rasd:AddressOnParent>
+      </Item>
+    </VirtualHardwareSection>
+  </VirtualSystem>
+</Envelope>
diff --git a/test/data/ovfdata/gzip_disk.ovf b/test/data/ovfdata/gzip_disk.ovf
new file mode 100644 (file)
index 0000000..44fc5de
--- /dev/null
@@ -0,0 +1,93 @@
+<?xml version="1.0"?>
+<Envelope ovf:version="1.0" xml:lang="en-US" xmlns="http://schemas.dmtf.org/ovf/envelope/1" xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1" xmlns:rasd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData" xmlns:vssd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:gnt="http://ganeti">
+  <References>
+    <File ovf:href="compr_disk.vmdk.gz"  ovf:compression="gzip" ovf:id="file1"/>
+  </References>
+  <DiskSection>
+    <Info>List of the virtual disks used in the package</Info>
+    <Disk ovf:diskId="vmdisk1" ovf:fileRef="file1" ovf:format="http://www.vmware.com/specifications/vmdk.html#sparse"/>
+  </DiskSection>
+  <gnt:GanetiSection>
+    <gnt:Version>0</gnt:Version>
+    <gnt:AutoBalance>False</gnt:AutoBalance>
+    <gnt:Tags></gnt:Tags>
+    <gnt:DiskTemplate>plain</gnt:DiskTemplate>
+    <gnt:OperatingSystem>
+      <gnt:Name>lenny-image</gnt:Name>
+    </gnt:OperatingSystem>
+    <gnt:Network>
+      <gnt:Nic ovf:name="routed">
+        <gnt:Mode>bridged</gnt:Mode>
+        <gnt:MACAddress>aa:00:00:d8:2c:1e</gnt:MACAddress>
+        <gnt:IPAddress>none</gnt:IPAddress>
+        <gnt:Link>xen-br0</gnt:Link>
+      </gnt:Nic>
+    </gnt:Network>
+    <gnt:Hypervisor>
+      <gnt:Name>xen-pvm</gnt:Name>
+      <gnt:Parameters>
+        <gnt:root-path>/dev/sda</gnt:root-path>
+        <gnt:kernel_args>ro</gnt:kernel_args>
+      </gnt:Parameters>
+    </gnt:Hypervisor>
+  </gnt:GanetiSection>
+  <NetworkSection>
+    <Info>Logical networks used in the package</Info>
+    <Network ovf:name="routed">
+      <Description>Logical network used by this appliance.</Description>
+    </Network>
+  </NetworkSection>
+  <VirtualSystem ovf:id="New-shiny-instance">
+    <Info>A virtual machine</Info>
+    <Name>ganeti-test-xen</Name>
+    <OperatingSystemSection ovf:id="93">
+      <Info>The kind of installed guest operating system</Info>
+      <Description>Ubuntu</Description>
+    </OperatingSystemSection>
+    <VirtualHardwareSection>
+      <Info>Virtual hardware requirements for a virtual machine</Info>
+      <System>
+        <vssd:ElementName>Virtual Hardware Family</vssd:ElementName>
+        <vssd:InstanceID>0</vssd:InstanceID>
+        <vssd:VirtualSystemIdentifier>Ubuntu-freshly-created</vssd:VirtualSystemIdentifier>
+        <vssd:VirtualSystemType>virtualbox-2.2</vssd:VirtualSystemType>
+      </System>
+      <Item>
+        <rasd:Caption>1 virtual CPU</rasd:Caption>
+        <rasd:ElementName>1 virtual CPU</rasd:ElementName>
+        <rasd:Description>Number of virtual CPUs</rasd:Description>
+        <rasd:InstanceID>1</rasd:InstanceID>
+        <rasd:ResourceType>3</rasd:ResourceType>
+        <rasd:VirtualQuantity>1</rasd:VirtualQuantity>
+      </Item>
+      <Item>
+        <rasd:Caption>2048 MB of memory</rasd:Caption>
+        <rasd:ElementName>2048 MB of memory</rasd:ElementName>
+        <rasd:Description>Memory Size</rasd:Description>
+        <rasd:InstanceID>2</rasd:InstanceID>
+        <rasd:ResourceType>4</rasd:ResourceType>
+        <rasd:AllocationUnits>MegaBytes</rasd:AllocationUnits>
+        <rasd:VirtualQuantity>2048</rasd:VirtualQuantity>
+      </Item>
+      <Item>
+        <rasd:Caption>Ethernet adapter on 'NAT'</rasd:Caption>
+        <rasd:ElementName>Ethernet adapter on 'NAT'</rasd:ElementName>
+        <rasd:InstanceID>5</rasd:InstanceID>
+        <rasd:ResourceType>10</rasd:ResourceType>
+        <rasd:ResourceSubType>PCNet32</rasd:ResourceSubType>
+        <rasd:AutomaticAllocation>true</rasd:AutomaticAllocation>
+        <rasd:Connection></rasd:Connection>
+      </Item>
+      <Item>
+        <rasd:Caption>disk1</rasd:Caption>
+        <rasd:ElementName>disk1</rasd:ElementName>
+        <rasd:Description>Disk Image</rasd:Description>
+        <rasd:InstanceID>7</rasd:InstanceID>
+        <rasd:ResourceType>17</rasd:ResourceType>
+        <rasd:HostResource>/disk/vmdisk1</rasd:HostResource>
+        <rasd:Parent>3</rasd:Parent>
+        <rasd:AddressOnParent>0</rasd:AddressOnParent>
+      </Item>
+    </VirtualHardwareSection>
+  </VirtualSystem>
+</Envelope>
diff --git a/test/data/ovfdata/new_disk.vmdk b/test/data/ovfdata/new_disk.vmdk
new file mode 100644 (file)
index 0000000..5047100
Binary files /dev/null and b/test/data/ovfdata/new_disk.vmdk differ
diff --git a/test/data/ovfdata/no_disk.ini b/test/data/ovfdata/no_disk.ini
new file mode 100644 (file)
index 0000000..5916152
--- /dev/null
@@ -0,0 +1,23 @@
+[instance]
+disk0_dump = iamnothere.raw
+nic0_mode = nic
+name = ganeti-test-xen
+disk_count = 1
+nic0_mac = aa:00:00:d8:2c:1e
+nic_count = 1
+nic0_link = xen-br0
+nic0_ip = None
+disk0_ivname = disk/0
+disk0_size = 0
+
+[hypervisor]
+root-path = /dev/sda
+kernel_args = ro
+
+[export]
+version = 0
+
+[os]
+
+[backend]
+auto_balance = False
diff --git a/test/data/ovfdata/no_disk_in_ref.ovf b/test/data/ovfdata/no_disk_in_ref.ovf
new file mode 100644 (file)
index 0000000..1aa8afc
--- /dev/null
@@ -0,0 +1,88 @@
+<?xml version="1.0"?>
+<Envelope ovf:version="1.0" xml:lang="en-US" xmlns="http://schemas.dmtf.org/ovf/envelope/1" xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1" xmlns:rasd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData" xmlns:vssd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+  <References>
+    <File ovf:href="second_disk.vmdk" ovf:id="file2"/>
+  </References>
+  <DiskSection>
+    <Info>List of the virtual disks used in the package</Info>
+    <Disk ovf:diskId="vmdisk1" ovf:fileRef="file1" ovf:format="http://www.vmware.com/specifications/vmdk.html#sparse"/>
+    <Disk ovf:diskId="vmdisk2" ovf:fileRef="file2" ovf:format="http://www.vmware.com/specifications/vmdk.html#sparse"/>
+  </DiskSection>
+  <NetworkSection>
+    <Info>Logical networks used in the package</Info>
+    <Network ovf:name="NAT">
+      <Description>Logical network used by this appliance.</Description>
+    </Network>
+  </NetworkSection>
+  <VirtualSystem ovf:id="Ubuntu-freshly-created">
+    <Info>A virtual machine</Info>
+    <OperatingSystemSection ovf:id="93">
+      <Info>The kind of installed guest operating system</Info>
+      <Description>Ubuntu</Description>
+    </OperatingSystemSection>
+    <VirtualHardwareSection>
+      <Info>Virtual hardware requirements for a virtual machine</Info>
+      <System>
+        <vssd:ElementName>Virtual Hardware Family</vssd:ElementName>
+        <vssd:InstanceID>0</vssd:InstanceID>
+        <vssd:VirtualSystemIdentifier>Ubuntu-freshly-created</vssd:VirtualSystemIdentifier>
+        <vssd:VirtualSystemType>virtualbox-2.2</vssd:VirtualSystemType>
+      </System>
+      <Item>
+        <rasd:Caption>1 virtual CPU</rasd:Caption>
+        <rasd:ElementName>1 virtual CPU</rasd:ElementName>
+        <rasd:Description>Number of virtual CPUs</rasd:Description>
+        <rasd:InstanceID>1</rasd:InstanceID>
+        <rasd:ResourceType>3</rasd:ResourceType>
+        <rasd:VirtualQuantity>1</rasd:VirtualQuantity>
+      </Item>
+      <Item>
+        <rasd:Caption>2048 MB of memory</rasd:Caption>
+        <rasd:ElementName>2048 MB of memory</rasd:ElementName>
+        <rasd:Description>Memory Size</rasd:Description>
+        <rasd:InstanceID>2</rasd:InstanceID>
+        <rasd:ResourceType>4</rasd:ResourceType>
+        <rasd:AllocationUnits>MegaBytes</rasd:AllocationUnits>
+        <rasd:VirtualQuantity>2048</rasd:VirtualQuantity>
+      </Item>
+      <Item>
+        <rasd:Caption>ideController0</rasd:Caption>
+        <rasd:ElementName>ideController0</rasd:ElementName>
+        <rasd:Description>IDE Controller</rasd:Description>
+        <rasd:InstanceID>3</rasd:InstanceID>
+        <rasd:ResourceType>5</rasd:ResourceType>
+        <rasd:ResourceSubType>PIIX4</rasd:ResourceSubType>
+        <rasd:Address>1</rasd:Address>
+      </Item>
+      <Item>
+        <rasd:Caption>Ethernet adapter on 'NAT'</rasd:Caption>
+        <rasd:ElementName>Ethernet adapter on 'NAT'</rasd:ElementName>
+        <rasd:InstanceID>5</rasd:InstanceID>
+        <rasd:ResourceType>10</rasd:ResourceType>
+        <rasd:ResourceSubType>PCNet32</rasd:ResourceSubType>
+        <rasd:AutomaticAllocation>true</rasd:AutomaticAllocation>
+        <rasd:Connection>NAT</rasd:Connection>
+      </Item>
+      <Item>
+        <rasd:Caption>disk1</rasd:Caption>
+        <rasd:ElementName>disk1</rasd:ElementName>
+        <rasd:Description>Disk Image</rasd:Description>
+        <rasd:InstanceID>7</rasd:InstanceID>
+        <rasd:ResourceType>17</rasd:ResourceType>
+        <rasd:HostResource>/disk/vmdisk1</rasd:HostResource>
+        <rasd:Parent>3</rasd:Parent>
+        <rasd:AddressOnParent>0</rasd:AddressOnParent>
+      </Item>
+      <Item>
+        <rasd:Caption>disk1</rasd:Caption>
+        <rasd:ElementName>disk1</rasd:ElementName>
+        <rasd:Description>Disk Image</rasd:Description>
+        <rasd:InstanceID>9</rasd:InstanceID>
+        <rasd:ResourceType>17</rasd:ResourceType>
+        <rasd:HostResource>/disk/vmdisk1</rasd:HostResource>
+        <rasd:Parent>3</rasd:Parent>
+        <rasd:AddressOnParent>0</rasd:AddressOnParent>
+      </Item>
+    </VirtualHardwareSection>
+  </VirtualSystem>
+</Envelope>
diff --git a/test/data/ovfdata/no_os.ini b/test/data/ovfdata/no_os.ini
new file mode 100644 (file)
index 0000000..29439f5
--- /dev/null
@@ -0,0 +1,26 @@
+[instance]
+disk0_dump = rawdisk.raw
+nic0_mode = bridged
+name = ganeti-test-xen
+hypervisor = xen-pvm
+disk_count = 1
+nic0_mac = aa:00:00:d8:2c:1e
+nic_count = 1
+nic0_link = xen-br0
+nic0_ip = None
+disk0_ivname = disk/0
+disk0_size = 0
+
+[hypervisor]
+root-path = /dev/sda
+kernel_args = ro
+
+[export]
+version = 0
+
+[os]
+
+[backend]
+auto_balance = False
+vcpus = 1
+memory = 2048
diff --git a/test/data/ovfdata/no_ovf.ova b/test/data/ovfdata/no_ovf.ova
new file mode 100644 (file)
index 0000000..207b571
Binary files /dev/null and b/test/data/ovfdata/no_ovf.ova differ
diff --git a/test/data/ovfdata/other/rawdisk.raw b/test/data/ovfdata/other/rawdisk.raw
new file mode 100644 (file)
index 0000000..e7f3c2d
Binary files /dev/null and b/test/data/ovfdata/other/rawdisk.raw differ
diff --git a/test/data/ovfdata/ova.ova b/test/data/ovfdata/ova.ova
new file mode 100644 (file)
index 0000000..856de96
Binary files /dev/null and b/test/data/ovfdata/ova.ova differ
diff --git a/test/data/ovfdata/rawdisk.raw b/test/data/ovfdata/rawdisk.raw
new file mode 100644 (file)
index 0000000..e7f3c2d
Binary files /dev/null and b/test/data/ovfdata/rawdisk.raw differ
diff --git a/test/data/ovfdata/second_disk.vmdk b/test/data/ovfdata/second_disk.vmdk
new file mode 100644 (file)
index 0000000..4ba0fff
Binary files /dev/null and b/test/data/ovfdata/second_disk.vmdk differ
diff --git a/test/data/ovfdata/unsafe_path.ini b/test/data/ovfdata/unsafe_path.ini
new file mode 100644 (file)
index 0000000..c95f466
--- /dev/null
@@ -0,0 +1,27 @@
+[instance]
+disk0_dump = other/rawdisk.raw
+nic0_mode = bridged
+name = ganeti-test-xen
+hypervisor = xen-pvm
+disk_count = 1
+nic0_mac = aa:00:00:d8:2c:1e
+nic_count = 1
+nic0_link = xen-br0
+nic0_ip = None
+disk0_ivname = disk/0
+disk0_size = 0
+
+[hypervisor]
+root-path = /dev/sda
+kernel_args = ro
+
+[export]
+version = 0
+os = lenny-image
+
+[os]
+
+[backend]
+auto_balance = False
+vcpus = 1
+memory = 2048
diff --git a/test/data/ovfdata/virtualbox.ovf b/test/data/ovfdata/virtualbox.ovf
new file mode 100644 (file)
index 0000000..dba2919
--- /dev/null
@@ -0,0 +1,89 @@
+<?xml version="1.0"?>
+<Envelope ovf:version="1.0" xml:lang="en-US" xmlns="http://schemas.dmtf.org/ovf/envelope/1" xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1" xmlns:rasd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData" xmlns:vssd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+  <References>
+    <File ovf:href="new_disk.vmdk" ovf:id="file1"/>
+    <File ovf:href="second_disk.vmdk" ovf:id="file2"/>
+  </References>
+  <DiskSection>
+    <Info>List of the virtual disks used in the package</Info>
+    <Disk ovf:diskId="vmdisk1" ovf:fileRef="file1" ovf:format="http://www.vmware.com/specifications/vmdk.html#sparse"/>
+    <Disk ovf:diskId="vmdisk2" ovf:fileRef="file2" ovf:format="http://www.vmware.com/specifications/vmdk.html#sparse"/>
+  </DiskSection>
+  <NetworkSection>
+    <Info>Logical networks used in the package</Info>
+    <Network ovf:name="bridged">
+      <Description>Logical network used by this appliance.</Description>
+    </Network>
+  </NetworkSection>
+  <VirtualSystem ovf:id="Ubuntu-freshly-created">
+    <Info>A virtual machine</Info>
+    <OperatingSystemSection ovf:id="93">
+      <Info>The kind of installed guest operating system</Info>
+      <Description>Ubuntu</Description>
+    </OperatingSystemSection>
+    <VirtualHardwareSection>
+      <Info>Virtual hardware requirements for a virtual machine</Info>
+      <System>
+        <vssd:ElementName>Virtual Hardware Family</vssd:ElementName>
+        <vssd:InstanceID>0</vssd:InstanceID>
+        <vssd:VirtualSystemIdentifier>Ubuntu-freshly-created</vssd:VirtualSystemIdentifier>
+        <vssd:VirtualSystemType>virtualbox-2.2</vssd:VirtualSystemType>
+      </System>
+      <Item>
+        <rasd:Caption>1 virtual CPU</rasd:Caption>
+        <rasd:ElementName>1 virtual CPU</rasd:ElementName>
+        <rasd:Description>Number of virtual CPUs</rasd:Description>
+        <rasd:InstanceID>1</rasd:InstanceID>
+        <rasd:ResourceType>3</rasd:ResourceType>
+        <rasd:VirtualQuantity>1</rasd:VirtualQuantity>
+      </Item>
+      <Item>
+        <rasd:Caption>2048 MB of memory</rasd:Caption>
+        <rasd:ElementName>2048 MB of memory</rasd:ElementName>
+        <rasd:Description>Memory Size</rasd:Description>
+        <rasd:InstanceID>2</rasd:InstanceID>
+        <rasd:ResourceType>4</rasd:ResourceType>
+        <rasd:AllocationUnits>MegaBytes</rasd:AllocationUnits>
+        <rasd:VirtualQuantity>2048</rasd:VirtualQuantity>
+      </Item>
+      <Item>
+        <rasd:Caption>ideController0</rasd:Caption>
+        <rasd:ElementName>ideController0</rasd:ElementName>
+        <rasd:Description>IDE Controller</rasd:Description>
+        <rasd:InstanceID>3</rasd:InstanceID>
+        <rasd:ResourceType>5</rasd:ResourceType>
+        <rasd:ResourceSubType>PIIX4</rasd:ResourceSubType>
+        <rasd:Address>1</rasd:Address>
+      </Item>
+      <Item>
+        <rasd:Caption>Ethernet adapter on 'NAT'</rasd:Caption>
+        <rasd:ElementName>Ethernet adapter on 'NAT'</rasd:ElementName>
+        <rasd:InstanceID>5</rasd:InstanceID>
+        <rasd:ResourceType>10</rasd:ResourceType>
+        <rasd:ResourceSubType>PCNet32</rasd:ResourceSubType>
+        <rasd:AutomaticAllocation>true</rasd:AutomaticAllocation>
+        <rasd:Connection>bridged</rasd:Connection>
+      </Item>
+      <Item>
+        <rasd:Caption>disk1</rasd:Caption>
+        <rasd:ElementName>disk1</rasd:ElementName>
+        <rasd:Description>Disk Image</rasd:Description>
+        <rasd:InstanceID>7</rasd:InstanceID>
+        <rasd:ResourceType>17</rasd:ResourceType>
+        <rasd:HostResource>/disk/vmdisk1</rasd:HostResource>
+        <rasd:Parent>3</rasd:Parent>
+        <rasd:AddressOnParent>0</rasd:AddressOnParent>
+      </Item>
+      <Item>
+        <rasd:Caption>disk1</rasd:Caption>
+        <rasd:ElementName>disk1</rasd:ElementName>
+        <rasd:Description>Disk Image</rasd:Description>
+        <rasd:InstanceID>9</rasd:InstanceID>
+        <rasd:ResourceType>17</rasd:ResourceType>
+        <rasd:HostResource>/disk/vmdisk1</rasd:HostResource>
+        <rasd:Parent>3</rasd:Parent>
+        <rasd:AddressOnParent>0</rasd:AddressOnParent>
+      </Item>
+    </VirtualHardwareSection>
+  </VirtualSystem>
+</Envelope>
diff --git a/test/data/ovfdata/wrong_config.ini b/test/data/ovfdata/wrong_config.ini
new file mode 100644 (file)
index 0000000..0ea02fa
--- /dev/null
@@ -0,0 +1 @@
+It's just wrong
diff --git a/test/data/ovfdata/wrong_extension.ovd b/test/data/ovfdata/wrong_extension.ovd
new file mode 100644 (file)
index 0000000..e1e8709
--- /dev/null
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--Generated by VMware ovftool 2.0.1 (build-260188), User: , UTC time: 2011-08-17T15:12:11.715742Z-->
+<Envelope vmw:buildId="build-260188" xmlns="http://schemas.dmtf.org/ovf/envelope/1" xmlns:cim="http://schemas.dmtf.org/wbem/wscim/1/common" xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1" xmlns:rasd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData" xmlns:vmw="http://www.vmware.com/schema/ovf" xmlns:vssd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+  <References>
+    <File ovf:href="AyertiennaSUSE.x86_64-0.0.2-disk1.vmdk" ovf:id="file1" ovf:size="761627136"/>
+  </References>
+  <DiskSection>
+    <Info>Virtual disk information</Info>
+    <Disk ovf:capacity="16514" ovf:capacityAllocationUnits="byte * 2^20" ovf:diskId="vmdisk1" ovf:fileRef="file1" ovf:format="http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized" ovf:populatedSize="2042953728"/>
+  </DiskSection>
+  <NetworkSection>
+    <Info>The list of logical networks</Info>
+    <Network ovf:name="bridged">
+      <Description>The bridged network</Description>
+    </Network>
+  </NetworkSection>
+  <VirtualSystem ovf:id="vm">
+    <Info>A virtual machine</Info>
+    <Name>AyertiennaSUSE.x86_64-0.0.2</Name>
+    <OperatingSystemSection ovf:id="83" vmw:osType="suse64Guest">
+      <Info>The kind of installed guest operating system</Info>
+    </OperatingSystemSection>
+    <VirtualHardwareSection>
+      <Info>Virtual hardware requirements</Info>
+      <System>
+        <vssd:ElementName>Virtual Hardware Family</vssd:ElementName>
+        <vssd:InstanceID>0</vssd:InstanceID>
+        <vssd:VirtualSystemIdentifier>AyertiennaSUSE.x86_64-0.0.2</vssd:VirtualSystemIdentifier>
+        <vssd:VirtualSystemType>vmx-04</vssd:VirtualSystemType>
+      </System>
+      <Item>
+        <rasd:AllocationUnits>hertz * 10^6</rasd:AllocationUnits>
+        <rasd:Description>Number of Virtual CPUs</rasd:Description>
+        <rasd:ElementName>1 virtual CPU(s)</rasd:ElementName>
+        <rasd:InstanceID>1</rasd:InstanceID>
+        <rasd:ResourceType>3</rasd:ResourceType>
+        <rasd:VirtualQuantity>1</rasd:VirtualQuantity>
+      </Item>
+      <Item>
+        <rasd:AllocationUnits>byte * 2^20</rasd:AllocationUnits>
+        <rasd:Description>Memory Size</rasd:Description>
+        <rasd:ElementName>512MB of memory</rasd:ElementName>
+        <rasd:InstanceID>2</rasd:InstanceID>
+        <rasd:ResourceType>4</rasd:ResourceType>
+        <rasd:VirtualQuantity>512</rasd:VirtualQuantity>
+      </Item>
+      <Item ovf:required="false">
+        <rasd:Address>0</rasd:Address>
+        <rasd:Description>USB Controller</rasd:Description>
+        <rasd:ElementName>usb</rasd:ElementName>
+        <rasd:InstanceID>3</rasd:InstanceID>
+        <rasd:ResourceType>23</rasd:ResourceType>
+      </Item>
+      <Item>
+        <rasd:Address>0</rasd:Address>
+        <rasd:Description>SCSI Controller</rasd:Description>
+        <rasd:ElementName>scsiController0</rasd:ElementName>
+        <rasd:InstanceID>4</rasd:InstanceID>
+        <rasd:ResourceSubType>lsilogic</rasd:ResourceSubType>
+        <rasd:ResourceType>6</rasd:ResourceType>
+      </Item>
+      <Item>
+        <rasd:Address>0</rasd:Address>
+        <rasd:Description>IDE Controller</rasd:Description>
+        <rasd:ElementName>ideController0</rasd:ElementName>
+        <rasd:InstanceID>5</rasd:InstanceID>
+        <rasd:ResourceType>5</rasd:ResourceType>
+      </Item>
+      <Item ovf:required="false">
+        <rasd:AddressOnParent>0</rasd:AddressOnParent>
+        <rasd:AutomaticAllocation>false</rasd:AutomaticAllocation>
+        <rasd:Description>Floppy Drive</rasd:Description>
+        <rasd:ElementName>floppy0</rasd:ElementName>
+        <rasd:InstanceID>6</rasd:InstanceID>
+        <rasd:ResourceType>14</rasd:ResourceType>
+      </Item>
+      <Item ovf:required="false">
+        <rasd:AddressOnParent>0</rasd:AddressOnParent>
+        <rasd:AutomaticAllocation>false</rasd:AutomaticAllocation>
+        <rasd:ElementName>cdrom1</rasd:ElementName>
+        <rasd:InstanceID>7</rasd:InstanceID>
+        <rasd:Parent>5</rasd:Parent>
+        <rasd:ResourceType>15</rasd:ResourceType>
+      </Item>
+      <Item>
+        <rasd:AddressOnParent>0</rasd:AddressOnParent>
+        <rasd:ElementName>disk1</rasd:ElementName>
+        <rasd:HostResource>ovf:/disk/vmdisk1</rasd:HostResource>
+        <rasd:InstanceID>8</rasd:InstanceID>
+        <rasd:Parent>4</rasd:Parent>
+        <rasd:ResourceType>17</rasd:ResourceType>
+      </Item>
+      <Item>
+        <rasd:AddressOnParent>2</rasd:AddressOnParent>
+        <rasd:AutomaticAllocation>true</rasd:AutomaticAllocation>
+        <rasd:Connection>bridged</rasd:Connection>
+        <rasd:Description>E1000 ethernet adapter on &quot;bridged&quot;</rasd:Description>
+        <rasd:ElementName>ethernet0</rasd:ElementName>
+        <rasd:InstanceID>9</rasd:InstanceID>
+        <rasd:ResourceSubType>E1000</rasd:ResourceSubType>
+        <rasd:ResourceType>10</rasd:ResourceType>
+      </Item>
+    </VirtualHardwareSection>
+  </VirtualSystem>
+</Envelope>
diff --git a/test/data/ovfdata/wrong_manifest.mf b/test/data/ovfdata/wrong_manifest.mf
new file mode 100644 (file)
index 0000000..7bd4005
--- /dev/null
@@ -0,0 +1,2 @@
+SHA1(new_disk.vmdk)= 0500304662fb8a6a7925b5a43bc0e05d6a03720d
+SHA1(wrong_manifest.ovf)= 0500304662fb8a6a7965b5a43bc0e05d6a03720d
diff --git a/test/data/ovfdata/wrong_manifest.ovf b/test/data/ovfdata/wrong_manifest.ovf
new file mode 100644 (file)
index 0000000..6883d73
--- /dev/null
@@ -0,0 +1,98 @@
+<?xml version="1.0"?>
+<Envelope ovf:version="1.0" xml:lang="en-US" xmlns="http://schemas.dmtf.org/ovf/envelope/1" xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1" xmlns:rasd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData" xmlns:vssd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:gnt="http://ganeti">
+  <References>
+    <File ovf:href="new_disk.vmdk" ovf:id="file1"/>
+  </References>
+  <DiskSection>
+    <Info>List of the virtual disks used in the package</Info>
+    <Disk ovf:diskId="vmdisk1" ovf:fileRef="file1" ovf:format="http://www.vmware.com/specifications/vmdk.html#sparse"/>
+  </DiskSection>
+  <gnt:GanetiSection>
+    <gnt:VersionId>0</gnt:VersionId>
+    <gnt:AutoBalance>False</gnt:AutoBalance>
+    <gnt:Tags></gnt:Tags>
+    <gnt:OS>
+      <gnt:Name>lenny-image</gnt:Name>
+    </gnt:OS>
+    <gnt:Network>
+      <gnt:Mode>bridged</gnt:Mode>
+      <gnt:MACAddress>aa:00:00:d8:2c:1e</gnt:MACAddress>
+      <gnt:IPAddress>None</gnt:IPAddress>
+      <gnt:Link>xen-br0</gnt:Link>
+    </gnt:Network>
+    <gnt:Hypervisor>
+      <gnt:Name>xen-pvm</gnt:Name>
+      <gnt:Parameters>
+        <gnt:root-path>/dev/sda</gnt:root-path>
+        <gnt:kernel_args>ro</gnt:kernel_args>
+      </gnt:Parameters>
+    </gnt:Hypervisor>
+  </gnt:GanetiSection>
+  <NetworkSection>
+    <Info>Logical networks used in the package</Info>
+    <Network ovf:name="bridged network">
+      <Description>Logical network used by this appliance.</Description>
+    </Network>
+  </NetworkSection>
+  <VirtualSystem ovf:id="New-shiny-instance">
+    <Info>A virtual machine</Info>
+    <OperatingSystemSection ovf:id="93">
+      <Info>The kind of installed guest operating system</Info>
+      <Description>Ubuntu</Description>
+    </OperatingSystemSection>
+    <VirtualHardwareSection>
+      <Info>Virtual hardware requirements for a virtual machine</Info>
+      <System>
+        <vssd:ElementName>Virtual Hardware Family</vssd:ElementName>
+        <vssd:InstanceID>0</vssd:InstanceID>
+        <vssd:VirtualSystemIdentifier>Ubuntu-freshly-created</vssd:VirtualSystemIdentifier>
+        <vssd:VirtualSystemType>virtualbox-2.2</vssd:VirtualSystemType>
+      </System>
+      <Item>
+        <rasd:Caption>1 virtual CPU</rasd:Caption>
+        <rasd:ElementName>1 virtual CPU</rasd:ElementName>
+        <rasd:Description>Number of virtual CPUs</rasd:Description>
+        <rasd:InstanceID>1</rasd:InstanceID>
+        <rasd:ResourceType>3</rasd:ResourceType>
+        <rasd:VirtualQuantity>1</rasd:VirtualQuantity>
+      </Item>
+      <Item>
+        <rasd:Caption>2048 MB of memory</rasd:Caption>
+        <rasd:ElementName>2048 MB of memory</rasd:ElementName>
+        <rasd:Description>Memory Size</rasd:Description>
+        <rasd:InstanceID>2</rasd:InstanceID>
+        <rasd:ResourceType>4</rasd:ResourceType>
+        <rasd:AllocationUnits>MegaBytes</rasd:AllocationUnits>
+        <rasd:VirtualQuantity>2048</rasd:VirtualQuantity>
+      </Item>
+      <Item>
+        <rasd:Caption>ideController0</rasd:Caption>
+        <rasd:ElementName>ideController0</rasd:ElementName>
+        <rasd:Description>IDE Controller</rasd:Description>
+        <rasd:InstanceID>3</rasd:InstanceID>
+        <rasd:ResourceType>5</rasd:ResourceType>
+        <rasd:ResourceSubType>PIIX4</rasd:ResourceSubType>
+        <rasd:Address>1</rasd:Address>
+      </Item>
+      <Item>
+        <rasd:Caption>Ethernet adapter on 'NAT'</rasd:Caption>
+        <rasd:ElementName>Ethernet adapter on 'NAT'</rasd:ElementName>
+        <rasd:InstanceID>5</rasd:InstanceID>
+        <rasd:ResourceType>10</rasd:ResourceType>
+        <rasd:ResourceSubType>PCNet32</rasd:ResourceSubType>
+        <rasd:AutomaticAllocation>true</rasd:AutomaticAllocation>
+        <rasd:Connection>bridged network</rasd:Connection>
+      </Item>
+      <Item>
+        <rasd:Caption>disk1</rasd:Caption>
+        <rasd:ElementName>disk1</rasd:ElementName>
+        <rasd:Description>Disk Image</rasd:Description>
+        <rasd:InstanceID>7</rasd:InstanceID>
+        <rasd:ResourceType>17</rasd:ResourceType>
+        <rasd:HostResource>/disk/vmdisk1</rasd:HostResource>
+        <rasd:Parent>3</rasd:Parent>
+        <rasd:AddressOnParent>0</rasd:AddressOnParent>
+      </Item>
+    </VirtualHardwareSection>
+  </VirtualSystem>
+</Envelope>
diff --git a/test/data/ovfdata/wrong_ova.ova b/test/data/ovfdata/wrong_ova.ova
new file mode 100644 (file)
index 0000000..e1e8709
--- /dev/null
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--Generated by VMware ovftool 2.0.1 (build-260188), User: , UTC time: 2011-08-17T15:12:11.715742Z-->
+<Envelope vmw:buildId="build-260188" xmlns="http://schemas.dmtf.org/ovf/envelope/1" xmlns:cim="http://schemas.dmtf.org/wbem/wscim/1/common" xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1" xmlns:rasd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData" xmlns:vmw="http://www.vmware.com/schema/ovf" xmlns:vssd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+  <References>
+    <File ovf:href="AyertiennaSUSE.x86_64-0.0.2-disk1.vmdk" ovf:id="file1" ovf:size="761627136"/>
+  </References>
+  <DiskSection>
+    <Info>Virtual disk information</Info>
+    <Disk ovf:capacity="16514" ovf:capacityAllocationUnits="byte * 2^20" ovf:diskId="vmdisk1" ovf:fileRef="file1" ovf:format="http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized" ovf:populatedSize="2042953728"/>
+  </DiskSection>
+  <NetworkSection>
+    <Info>The list of logical networks</Info>
+    <Network ovf:name="bridged">
+      <Description>The bridged network</Description>
+    </Network>
+  </NetworkSection>
+  <VirtualSystem ovf:id="vm">
+    <Info>A virtual machine</Info>
+    <Name>AyertiennaSUSE.x86_64-0.0.2</Name>
+    <OperatingSystemSection ovf:id="83" vmw:osType="suse64Guest">
+      <Info>The kind of installed guest operating system</Info>
+    </OperatingSystemSection>
+    <VirtualHardwareSection>
+      <Info>Virtual hardware requirements</Info>
+      <System>
+        <vssd:ElementName>Virtual Hardware Family</vssd:ElementName>
+        <vssd:InstanceID>0</vssd:InstanceID>
+        <vssd:VirtualSystemIdentifier>AyertiennaSUSE.x86_64-0.0.2</vssd:VirtualSystemIdentifier>
+        <vssd:VirtualSystemType>vmx-04</vssd:VirtualSystemType>
+      </System>
+      <Item>
+        <rasd:AllocationUnits>hertz * 10^6</rasd:AllocationUnits>
+        <rasd:Description>Number of Virtual CPUs</rasd:Description>
+        <rasd:ElementName>1 virtual CPU(s)</rasd:ElementName>
+        <rasd:InstanceID>1</rasd:InstanceID>
+        <rasd:ResourceType>3</rasd:ResourceType>
+        <rasd:VirtualQuantity>1</rasd:VirtualQuantity>
+      </Item>
+      <Item>
+        <rasd:AllocationUnits>byte * 2^20</rasd:AllocationUnits>
+        <rasd:Description>Memory Size</rasd:Description>
+        <rasd:ElementName>512MB of memory</rasd:ElementName>
+        <rasd:InstanceID>2</rasd:InstanceID>
+        <rasd:ResourceType>4</rasd:ResourceType>
+        <rasd:VirtualQuantity>512</rasd:VirtualQuantity>
+      </Item>
+      <Item ovf:required="false">
+        <rasd:Address>0</rasd:Address>
+        <rasd:Description>USB Controller</rasd:Description>
+        <rasd:ElementName>usb</rasd:ElementName>
+        <rasd:InstanceID>3</rasd:InstanceID>
+        <rasd:ResourceType>23</rasd:ResourceType>
+      </Item>
+      <Item>
+        <rasd:Address>0</rasd:Address>
+        <rasd:Description>SCSI Controller</rasd:Description>
+        <rasd:ElementName>scsiController0</rasd:ElementName>
+        <rasd:InstanceID>4</rasd:InstanceID>
+        <rasd:ResourceSubType>lsilogic</rasd:ResourceSubType>
+        <rasd:ResourceType>6</rasd:ResourceType>
+      </Item>
+      <Item>
+        <rasd:Address>0</rasd:Address>
+        <rasd:Description>IDE Controller</rasd:Description>
+        <rasd:ElementName>ideController0</rasd:ElementName>
+        <rasd:InstanceID>5</rasd:InstanceID>
+        <rasd:ResourceType>5</rasd:ResourceType>
+      </Item>
+      <Item ovf:required="false">
+        <rasd:AddressOnParent>0</rasd:AddressOnParent>
+        <rasd:AutomaticAllocation>false</rasd:AutomaticAllocation>
+        <rasd:Description>Floppy Drive</rasd:Description>
+        <rasd:ElementName>floppy0</rasd:ElementName>
+        <rasd:InstanceID>6</rasd:InstanceID>
+        <rasd:ResourceType>14</rasd:ResourceType>
+      </Item>
+      <Item ovf:required="false">
+        <rasd:AddressOnParent>0</rasd:AddressOnParent>
+        <rasd:AutomaticAllocation>false</rasd:AutomaticAllocation>
+        <rasd:ElementName>cdrom1</rasd:ElementName>
+        <rasd:InstanceID>7</rasd:InstanceID>
+        <rasd:Parent>5</rasd:Parent>
+        <rasd:ResourceType>15</rasd:ResourceType>
+      </Item>
+      <Item>
+        <rasd:AddressOnParent>0</rasd:AddressOnParent>
+        <rasd:ElementName>disk1</rasd:ElementName>
+        <rasd:HostResource>ovf:/disk/vmdisk1</rasd:HostResource>
+        <rasd:InstanceID>8</rasd:InstanceID>
+        <rasd:Parent>4</rasd:Parent>
+        <rasd:ResourceType>17</rasd:ResourceType>
+      </Item>
+      <Item>
+        <rasd:AddressOnParent>2</rasd:AddressOnParent>
+        <rasd:AutomaticAllocation>true</rasd:AutomaticAllocation>
+        <rasd:Connection>bridged</rasd:Connection>
+        <rasd:Description>E1000 ethernet adapter on &quot;bridged&quot;</rasd:Description>
+        <rasd:ElementName>ethernet0</rasd:ElementName>
+        <rasd:InstanceID>9</rasd:InstanceID>
+        <rasd:ResourceSubType>E1000</rasd:ResourceSubType>
+        <rasd:ResourceType>10</rasd:ResourceType>
+      </Item>
+    </VirtualHardwareSection>
+  </VirtualSystem>
+</Envelope>
diff --git a/test/data/ovfdata/wrong_xml.ovf b/test/data/ovfdata/wrong_xml.ovf
new file mode 100644 (file)
index 0000000..f98b9c8
--- /dev/null
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--Generated by VMware ovftool 2.0.1 (build-260188), User: , UTC time: 2011-08-17T15:12:11.715742Z-->
+<Envelope vmw:buildId="build-260188" xmlns="http://schemas.dmtf.org/ovf/envelope/1" xmlns:cim="http://schemas.dmtf.org/wbem/wscim/1/common" xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1" xmlns:rasd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData" xmlns:vmw="http://www.vmware.com/schema/ovf" xmlns:vssd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+  <References>
+    <File ovf:href="AyertiennaSUSE.x86_64-0.0.2-disk1.vmdk" ovf:id="file1" ovf:size="761627136"/>
+  </References>
+  <DiskSection>
+    <Info>Virtual disk information</Info>
+    <Disk ovf:capacity="16514" ovf:capacityAllocationUnits="byte * 2^20" ovf:diskId="vmdisk1" ovf:fileRef="file1" ovf:format="http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized" ovf:populatedSize="2042953728"/>
+  </DiskSection>
+  <NetworkSection>
+    <Info>The list of logical networks</Info>
+    <Network ovf:name="bridged">
+      <Description>The bridged network</Description>
+    </Network>
+  </NetworkSection>
+  <VirtualSystem ovf:id="vm">
+    <Info>A virtual machine</Info>
+    <Name>AyertiennaSUSE.x86_64-0.0.2</Name>
+    <OperatingSystemSection ovf:id="83" vmw:osType="suse64Guest">
+      <Info>The kind of installed guest operating system</Info>
+    </OperatingSystemSection>
+    <VirtualHardwareSection>
+      <Info>Virtual hardware requirements</Info>
+      <System>
+        <vssd:ElementName>Virtual Hardware Family</vssd:ElementName>
+        <vssd:InstanceID>0</vssd:InstanceID>
+        <vssd:VirtualSystemIdentifier>AyertiennaSUSE.x86_64-0.0.2</vssd:VirtualSystemIdentifier>
+        <vssd:VirtualSystemType>vmx-04</vssd:VirtualSystemType>
+      </System>
+      <Item>
+        <rasd:AllocationUnits>hertz * 10^6</rasd:AllocationUnits>
+        <rasd:Description>Number of Virtual CPUs</rasd:Description>
+        <rasd:ElementName>1 virtual CPU(s)</rasd:ElementName>
+        <rasd:InstanceID>1</rasd:InstanceID>
+        <rasd:ResourceType>3</rasd:ResourceType>
+        <rasd:VirtualQuantity>1</rasd:VirtualQuantity>
+      </Item>
+      <Item>
+        <rasd:AllocationUnits>byte * 2^20</rasd:AllocationUnits>
+        <rasd:Description>Memory Size</rasd:Description>
+        <rasd:ElementName>512MB of memory</rasd:ElementName>
+        <rasd:InstanceID>2</rasd:InstanceID>
+        <rasd:ResourceType>4</rasd:ResourceType>
+        <rasd:VirtualQuantity>512</rasd:VirtualQuantity>
+      </Item>
+      <Item>
+        <rasd:Address>0</rasd:Address>
+        <rasd:Description>SCSI Controller</rasd:Description>
+        <rasd:ElementName>scsiController0</rasd:ElementName>
+        <rasd:InstanceID>4</rasd:InstanceID>
+        <rasd:ResourceSubType>lsilogic</rasd:ResourceSubType>
+        <rasd:ResourceType>6</rasd:ResourceType>
+      </Item>
+      <Item>
+        <rasd:Address>0</rasd:Address>
+        <rasd:Description>IDE Controller</rasd:Description>
+        <rasd:ElementName>ideController0</rasd:ElementName>
+        <rasd:InstanceID>5</rasd:InstanceID>
+        <rasd:ResourceType>5</rasd:ResourceType>
+      </Item>
+      <Item>
+        <rasd:AddressOnParent>0</rasd:AddressOnParent>
+        <rasd:ElementName>disk1</rasd:ElementName>
+        <rasd:HostResource>ovf:/disk/vmdisk1</rasd:HostResource>
+        <rasd:InstanceID>8</rasd:InstanceID>
+        <rasd:Parent>4</rasd:Parent>
+        <rasd:ResourceType>17</rasd:ResourceType>
+      </Item>
+      <Item
+      </Item>
+    </VirtualHardwareSection>
+  </VirtualSystem>
+</Envelope>
+
index 82e5a9d..d91976c 100755 (executable)
@@ -23,6 +23,8 @@
 
 import unittest
 import re
+import itertools
+import operator
 
 from ganeti import _autoconf
 from ganeti import utils
@@ -30,6 +32,10 @@ from ganeti import cmdlib
 from ganeti import build
 from ganeti import compat
 from ganeti import mcpu
+from ganeti import opcodes
+from ganeti import constants
+from ganeti.rapi import baserlib
+from ganeti.rapi import rlib2
 from ganeti.rapi import connector
 
 import testutils
@@ -37,20 +43,59 @@ import testutils
 
 VALID_URI_RE = re.compile(r"^[-/a-z0-9]*$")
 
+RAPI_OPCODE_EXCLUDE = frozenset([
+  # Not yet implemented
+  opcodes.OpBackupQuery,
+  opcodes.OpBackupRemove,
+  opcodes.OpClusterConfigQuery,
+  opcodes.OpClusterRepairDiskSizes,
+  opcodes.OpClusterVerify,
+  opcodes.OpClusterVerifyDisks,
+  opcodes.OpInstanceChangeGroup,
+  opcodes.OpInstanceMove,
+  opcodes.OpNodeQueryvols,
+  opcodes.OpOobCommand,
+  opcodes.OpTagsSearch,
+  opcodes.OpClusterActivateMasterIp,
+  opcodes.OpClusterDeactivateMasterIp,
+
+  # Difficult if not impossible
+  opcodes.OpClusterDestroy,
+  opcodes.OpClusterPostInit,
+  opcodes.OpClusterRename,
+  opcodes.OpNodeAdd,
+  opcodes.OpNodeRemove,
+
+  # Helper opcodes (e.g. submitted by LUs)
+  opcodes.OpClusterVerifyConfig,
+  opcodes.OpClusterVerifyGroup,
+  opcodes.OpGroupEvacuate,
+  opcodes.OpGroupVerifyDisks,
+
+  # Test opcodes
+  opcodes.OpTestAllocator,
+  opcodes.OpTestDelay,
+  opcodes.OpTestDummy,
+  opcodes.OpTestJqueue,
+  ])
+
+
+def _ReadDocFile(filename):
+  return utils.ReadFile("%s/doc/%s" %
+                        (testutils.GetSourceDir(), filename))
+
+
+class TestHooksDocs(unittest.TestCase):
+  HOOK_PATH_OK = frozenset([
+    "master-ip-turnup",
+    "master-ip-turndown",
+    ])
 
-class TestDocs(unittest.TestCase):
-  """Documentation tests"""
-
-  @staticmethod
-  def _ReadDocFile(filename):
-    return utils.ReadFile("%s/doc/%s" %
-                          (testutils.GetSourceDir(), filename))
-
-  def testHookDocs(self):
+  def test(self):
     """Check whether all hooks are documented.
 
     """
-    hooksdoc = self._ReadDocFile("hooks.rst")
+    hooksdoc = _ReadDocFile("hooks.rst")
 
     # Reverse mapping from LU to opcode
     lu2opcode = dict((lu, op)
@@ -58,35 +103,65 @@ class TestDocs(unittest.TestCase):
     assert len(lu2opcode) == len(mcpu.Processor.DISPATCH_TABLE), \
       "Found duplicate entries"
 
+    hooks_paths = frozenset(re.findall("^:directory:\s*(.+)\s*$", hooksdoc,
+                                       re.M))
+    self.assertTrue(self.HOOK_PATH_OK.issubset(hooks_paths),
+                    msg="Whitelisted path not found in documentation")
+
+    raw_hooks_ops = re.findall("^OP_(?!CODE$).+$", hooksdoc, re.M)
+    hooks_ops = set()
+    duplicate_ops = set()
+    for op in raw_hooks_ops:
+      if op in hooks_ops:
+        duplicate_ops.add(op)
+      else:
+        hooks_ops.add(op)
+
+    self.assertFalse(duplicate_ops,
+                     msg="Found duplicate opcode documentation: %s" %
+                         utils.CommaJoin(duplicate_ops))
+
+    seen_paths = set()
+    seen_ops = set()
+
+    self.assertFalse(duplicate_ops,
+                     msg="Found duplicated hook documentation: %s" %
+                         utils.CommaJoin(duplicate_ops))
+
     for name in dir(cmdlib):
-      obj = getattr(cmdlib, name)
+      lucls = getattr(cmdlib, name)
 
-      if (isinstance(obj, type) and
-          issubclass(obj, cmdlib.LogicalUnit) and
-          hasattr(obj, "HPATH")):
-        self._CheckHook(name, obj, hooksdoc, lu2opcode)
+      if (isinstance(lucls, type) and
+          issubclass(lucls, cmdlib.LogicalUnit) and
+          hasattr(lucls, "HPATH")):
+        if lucls.HTYPE is None:
+          continue
 
-  def _CheckHook(self, name, lucls, hooksdoc, lu2opcode):
-    opcls = lu2opcode.get(lucls, None)
+        opcls = lu2opcode.get(lucls, None)
 
-    if lucls.HTYPE is None:
-      return
+        if opcls:
+          seen_ops.add(opcls.OP_ID)
+          self.assertTrue(opcls.OP_ID in hooks_ops,
+                          msg="Missing hook documentation for %s" %
+                              opcls.OP_ID)
+        self.assertTrue(lucls.HPATH in hooks_paths,
+                        msg="Missing documentation for hook %s/%s" %
+                            (lucls.HTYPE, lucls.HPATH))
+        seen_paths.add(lucls.HPATH)
 
-    # TODO: Improve this test (e.g. find hooks documented but no longer
-    # existing)
+    missed_ops = hooks_ops - seen_ops
+    missed_paths = hooks_paths - seen_paths - self.HOOK_PATH_OK
 
-    if opcls:
-      self.assertTrue(re.findall("^%s$" % re.escape(opcls.OP_ID),
-                                 hooksdoc, re.M),
-                      msg=("Missing hook documentation for %s" %
-                           (opcls.OP_ID)))
+    self.assertFalse(missed_ops,
+                     msg="Op documents hook not existing anymore: %s" %
+                         utils.CommaJoin(missed_ops))
 
-    pattern = r"^:directory:\s*%s\s*$" % re.escape(lucls.HPATH)
+    self.assertFalse(missed_paths,
+                     msg="Hook path does not exist in opcode: %s" %
+                         utils.CommaJoin(missed_paths))
 
-    self.assert_(re.findall(pattern, hooksdoc, re.M),
-                 msg=("Missing documentation for hook %s/%s" %
-                      (lucls.HTYPE, lucls.HPATH)))
 
+class TestRapiDocs(unittest.TestCase):
   def _CheckRapiResource(self, uri, fixup, handler):
     docline = "%s resource." % uri
     self.assertEqual(handler.__doc__.splitlines()[0].strip(), docline,
@@ -99,11 +174,11 @@ class TestDocs(unittest.TestCase):
 
     self.assertTrue(VALID_URI_RE.match(uri), msg="Invalid URI %r" % uri)
 
-  def testRapiDocs(self):
+  def test(self):
     """Check whether all RAPI resources are documented.
 
     """
-    rapidoc = self._ReadDocFile("rapi.rst")
+    rapidoc = _ReadDocFile("rapi.rst")
 
     node_name = re.escape("[node_name]")
     instance_name = re.escape("[instance_name]")
@@ -187,6 +262,37 @@ class TestDocs(unittest.TestCase):
                 msg=("URIs matched by more than one resource: %s" %
                      utils.CommaJoin(uri_dups)))
 
+    self._FindRapiMissing(resources.values())
+    self._CheckTagHandlers(resources.values())
+
+  def _FindRapiMissing(self, handlers):
+    used = frozenset(itertools.chain(*map(baserlib.GetResourceOpcodes,
+                                          handlers)))
+
+    unexpected = used & RAPI_OPCODE_EXCLUDE
+    self.assertFalse(unexpected,
+      msg=("Found RAPI resources for excluded opcodes: %s" %
+           utils.CommaJoin(_GetOpIds(unexpected))))
+
+    missing = (frozenset(opcodes.OP_MAPPING.values()) - used -
+               RAPI_OPCODE_EXCLUDE)
+    self.assertFalse(missing,
+      msg=("Missing RAPI resources for opcodes: %s" %
+           utils.CommaJoin(_GetOpIds(missing))))
+
+  def _CheckTagHandlers(self, handlers):
+    tag_handlers = filter(lambda x: issubclass(x, rlib2._R_Tags), handlers)
+    self.assertEqual(frozenset(map(operator.attrgetter("TAG_LEVEL"),
+                                   tag_handlers)),
+                     constants.VALID_TAG_TYPES)
+
+
+def _GetOpIds(ops):
+  """Returns C{OP_ID} for all opcodes in passed sequence.
+
+  """
+  return sorted(opcls.OP_ID for opcls in ops)
+
 
 class TestManpages(unittest.TestCase):
   """Manpage tests"""
diff --git a/test/ganeti-cli.test b/test/ganeti-cli.test
new file mode 100644 (file)
index 0000000..054a618
--- /dev/null
@@ -0,0 +1,36 @@
+# test the various gnt-commands for common options
+$SCRIPTS/ganeti-masterd --help
+>>>/Usage:/
+>>>2
+>>>= 0
+$SCRIPTS/ganeti-masterd --version
+>>>/^ganeti-/
+>>>2
+>>>= 0
+
+$SCRIPTS/ganeti-noded --help
+>>>/Usage:/
+>>>2
+>>>= 0
+$SCRIPTS/ganeti-noded --version
+>>>/^ganeti-/
+>>>2
+>>>= 0
+
+$SCRIPTS/ganeti-rapi --help
+>>>/Usage:/
+>>>2
+>>>= 0
+$SCRIPTS/ganeti-rapi --version
+>>>/^ganeti-/
+>>>2
+>>>= 0
+
+$SCRIPTS/ganeti-watcher --help
+>>>/Usage:/
+>>>2
+>>>= 0
+$SCRIPTS/ganeti-watcher --version
+>>>/^ganeti-/
+>>>2
+>>>= 0
index 5fdaf56..0a376e6 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 #
 
-# Copyright (C) 2010 Google Inc.
+# Copyright (C) 2010, 2012 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
@@ -28,7 +28,7 @@ import tempfile
 import shutil
 
 try:
-  # pylint: disable-msg=E0611
+  # pylint: disable=E0611
   from pyinotify import pyinotify
 except ImportError:
   import pyinotify
@@ -73,6 +73,15 @@ class TestSingleFileEventHandler(testutils.GanetiTestCase):
     # TERM notifier is enabled by default, as we use it to get out of the loop
     self.ihandler[self.NOTIFIER_TERM].enable()
 
+  def tearDown(self):
+    # disable the inotifiers, before removing the files
+    for i in self.ihandler:
+      i.disable()
+    testutils.GanetiTestCase.tearDown(self)
+    # and unregister the fd's being polled
+    for n in self.notifiers:
+      n.del_channel()
+
   class OnInotifyCallback:
     def __init__(self, testobj, i):
       self.testobj = testobj
index 6f8e3d4..e1aeef6 100755 (executable)
@@ -27,6 +27,7 @@ import unittest
 
 from ganeti import bdev
 from ganeti import errors
+from ganeti import constants
 
 import testutils
 
@@ -156,6 +157,75 @@ class TestDRBD8Runner(testutils.GanetiTestCase):
                      "remote_addr" not in result),
                     "Should not find network info")
 
+  def testBarriersOptions(self):
+    """Test class method that generates drbdsetup options for disk barriers"""
+    # Tests that should fail because of wrong version/options combinations
+    should_fail = [
+      (8, 0, 12, "bfd", True),
+      (8, 0, 12, "fd", False),
+      (8, 0, 12, "b", True),
+      (8, 2, 7, "bfd", True),
+      (8, 2, 7, "b", True)
+    ]
+
+    for vmaj, vmin, vrel, opts, meta in should_fail:
+      self.assertRaises(errors.BlockDeviceError,
+                        bdev.DRBD8._ComputeDiskBarrierArgs,
+                        vmaj, vmin, vrel, opts, meta)
+
+    # get the valid options from the frozenset(frozenset()) in constants.
+    valid_options = [list(x)[0] for x in constants.DRBD_VALID_BARRIER_OPT]
+
+    # Versions that do not support anything
+    for vmaj, vmin, vrel in ((8, 0, 0), (8, 0, 11), (8, 2, 6)):
+      for opts in valid_options:
+        self.assertRaises(errors.BlockDeviceError,
+                          bdev.DRBD8._ComputeDiskBarrierArgs,
+                          vmaj, vmin, vrel, opts, True)
+
+    # Versions with partial support (testing only options that are supported)
+    tests = [
+      (8, 0, 12, "n", False, []),
+      (8, 0, 12, "n", True, ["--no-md-flushes"]),
+      (8, 2, 7, "n", False, []),
+      (8, 2, 7, "fd", False, ["--no-disk-flushes", "--no-disk-drain"]),
+      (8, 0, 12, "n", True, ["--no-md-flushes"]),
+      ]
+
+    # Versions that support everything
+    for vmaj, vmin, vrel in ((8, 3, 0), (8, 3, 12)):
+      tests.append((vmaj, vmin, vrel, "bfd", True,
+                    ["--no-disk-barrier", "--no-disk-drain",
+                     "--no-disk-flushes", "--no-md-flushes"]))
+      tests.append((vmaj, vmin, vrel, "n", False, []))
+      tests.append((vmaj, vmin, vrel, "b", True,
+                    ["--no-disk-barrier", "--no-md-flushes"]))
+      tests.append((vmaj, vmin, vrel, "fd", False,
+                    ["--no-disk-flushes", "--no-disk-drain"]))
+      tests.append((vmaj, vmin, vrel, "n", True, ["--no-md-flushes"]))
+
+    # Test execution
+    for test in tests:
+      vmaj, vmin, vrel, disabled_barriers, disable_meta_flush, expected = test
+      args = \
+        bdev.DRBD8._ComputeDiskBarrierArgs(vmaj, vmin, vrel,
+                                           disabled_barriers,
+                                           disable_meta_flush)
+      self.failUnless(set(args) == set(expected),
+                      "For test %s, got wrong results %s" % (test, args))
+
+    # Unsupported or invalid versions
+    for vmaj, vmin, vrel in ((0, 7, 25), (9, 0, 0), (7, 0, 0), (8, 4, 0)):
+      self.assertRaises(errors.BlockDeviceError,
+                        bdev.DRBD8._ComputeDiskBarrierArgs,
+                        vmaj, vmin, vrel, "n", True)
+
+    # Invalid options
+    for option in ("", "c", "whatever", "nbdfc", "nf"):
+      self.assertRaises(errors.BlockDeviceError,
+                        bdev.DRBD8._ComputeDiskBarrierArgs,
+                        8, 3, 11, option, True)
+
 
 class TestDRBD8Status(testutils.GanetiTestCase):
   """Testing case for DRBD8 /proc status"""
@@ -270,5 +340,37 @@ class TestDRBD8Status(testutils.GanetiTestCase):
     self.failUnless(stats.is_in_resync)
     self.failUnless(stats.sync_percent is not None)
 
+
+class TestRADOSBlockDevice(testutils.GanetiTestCase):
+  def test_ParseRbdShowmappedOutput(self):
+    volume_name = "abc9778-8e8ace5b.rbd.disk0"
+    output_ok = \
+      ("0\trbd\te69f28e5-9817.rbd.disk0\t-\t/dev/rbd0\n"
+       "1\t/dev/rbd0\tabc9778-8e8ace5b.rbd.disk0\t-\t/dev/rbd16\n"
+       "line\twith\tfewer\tfields\n"
+       "")
+    output_empty = ""
+    output_no_matches = \
+      ("0\trbd\te69f28e5-9817.rbd.disk0\t-\t/dev/rbd0\n"
+       "1\trbd\tabcdef01-9817.rbd.disk0\t-\t/dev/rbd10\n"
+       "2\trbd\tcdef0123-9817.rbd.disk0\t-\t/dev/rbd12\n"
+       "something\twith\tfewer\tfields"
+       "")
+    output_extra_matches = \
+      ("0\t/dev/rbd0\tabc9778-8e8ace5b.rbd.disk0\t-\t/dev/rbd11\n"
+       "1\trbd\te69f28e5-9817.rbd.disk0\t-\t/dev/rbd0\n"
+       "2\t/dev/rbd0\tabc9778-8e8ace5b.rbd.disk0\t-\t/dev/rbd16\n"
+       "something\twith\tfewer\tfields"
+       "")
+
+    parse_function = bdev.RADOSBlockDevice._ParseRbdShowmappedOutput
+    self.assertEqual(parse_function(output_ok, volume_name), "/dev/rbd16")
+    self.assertRaises(errors.BlockDeviceError, parse_function,
+                      output_empty, volume_name)
+    self.assertEqual(parse_function(output_no_matches, volume_name), None)
+    self.assertRaises(errors.BlockDeviceError, parse_function,
+                      output_extra_matches, volume_name)
+
+
 if __name__ == '__main__':
   testutils.GanetiTestProgram()
index 1f5b3d0..7185e17 100755 (executable)
@@ -22,6 +22,7 @@
 """Script for unittesting the cli module"""
 
 import unittest
+import time
 from cStringIO import StringIO
 
 import ganeti
@@ -85,6 +86,7 @@ class TestSplitKeyVal(unittest.TestCase):
     """Test how we handle splitting an empty string"""
     self.failUnlessEqual(cli._SplitKeyVal("option", ""), {})
 
+
 class TestIdentKeyVal(unittest.TestCase):
   """Testing case for cli.check_ident_key_val"""
 
@@ -102,6 +104,17 @@ class TestIdentKeyVal(unittest.TestCase):
     self.assertEqual(cikv("-foo"), ("foo", None))
     self.assertRaises(ParameterError, cikv, "-foo:a=c")
 
+    # Check negative numbers
+    self.assertEqual(cikv("-1:remove"), ("-1", {
+      "remove": True,
+      }))
+    self.assertEqual(cikv("-29447:add,size=4G"), ("-29447", {
+      "add": True,
+      "size": "4G",
+      }))
+    for i in ["-:", "-"]:
+      self.assertEqual(cikv(i), ("", None))
+
 
 class TestToStream(unittest.TestCase):
   """Test the ToStream functions"""
@@ -765,7 +778,7 @@ class TestGetOnlineNodes(unittest.TestCase):
     def CountPending(self):
       return len(self._query)
 
-    def Query(self, res, fields, filter_):
+    def Query(self, res, fields, qfilter):
       if res != constants.QR_NODE:
         raise Exception("Querying wrong resource")
 
@@ -774,7 +787,7 @@ class TestGetOnlineNodes(unittest.TestCase):
       if exp_fields != fields:
         raise Exception("Expected fields %s, got %s" % (exp_fields, fields))
 
-      if not (filter_ is None or check_filter(filter_)):
+      if not (qfilter is None or check_filter(qfilter)):
         raise Exception("Filter doesn't match expectations")
 
       return objects.QueryResponse(fields=None, data=result)
@@ -804,8 +817,8 @@ class TestGetOnlineNodes(unittest.TestCase):
   def testNoMaster(self):
     cl = self._FakeClient()
 
-    def _CheckFilter(filter_):
-      self.assertEqual(filter_, [qlang.OP_NOT, [qlang.OP_TRUE, "master"]])
+    def _CheckFilter(qfilter):
+      self.assertEqual(qfilter, [qlang.OP_NOT, [qlang.OP_TRUE, "master"]])
       return True
 
     cl.AddQueryResult(["name", "offline", "sip"], _CheckFilter, [
@@ -835,8 +848,8 @@ class TestGetOnlineNodes(unittest.TestCase):
   def testNoMasterFilterNodeName(self):
     cl = self._FakeClient()
 
-    def _CheckFilter(filter_):
-      self.assertEqual(filter_,
+    def _CheckFilter(qfilter):
+      self.assertEqual(qfilter,
         [qlang.OP_AND,
          [qlang.OP_OR] + [[qlang.OP_EQUAL, "name", name]
                           for name in ["node2", "node3"]],
@@ -877,8 +890,8 @@ class TestGetOnlineNodes(unittest.TestCase):
   def testNodeGroup(self):
     cl = self._FakeClient()
 
-    def _CheckFilter(filter_):
-      self.assertEqual(filter_,
+    def _CheckFilter(qfilter):
+      self.assertEqual(qfilter,
         [qlang.OP_OR, [qlang.OP_EQUAL, "group", "foobar"],
                       [qlang.OP_EQUAL, "group.uuid", "foobar"]])
       return True
@@ -896,5 +909,18 @@ class TestGetOnlineNodes(unittest.TestCase):
     self.assertEqual(cl.CountPending(), 0)
 
 
+class TestFormatTimestamp(unittest.TestCase):
+  def testGood(self):
+    self.assertEqual(cli.FormatTimestamp((0, 1)),
+                     time.strftime("%F %T", time.localtime(0)) + ".000001")
+    self.assertEqual(cli.FormatTimestamp((1332944009, 17376)),
+                     (time.strftime("%F %T", time.localtime(1332944009)) +
+                      ".017376"))
+
+  def testWrong(self):
+    for i in [0, [], {}, "", [1]]:
+      self.assertEqual(cli.FormatTimestamp(i), "?")
+
+
 if __name__ == '__main__':
   testutils.GanetiTestProgram()
index cccbf81..7cbd0de 100755 (executable)
@@ -119,5 +119,111 @@ class TestConsole(unittest.TestCase):
     self.assertEqual(len(self._output), 0)
 
 
+class TestConvertNicDiskModifications(unittest.TestCase):
+  def test(self):
+    fn = gnt_instance._ConvertNicDiskModifications
+
+    self.assertEqual(fn([]), [])
+
+    # Error cases
+    self.assertRaises(errors.OpPrereqError, fn, [
+      (constants.DDM_REMOVE, { "param": "value", }),
+      ])
+    self.assertRaises(errors.OpPrereqError, fn, [
+      (0, { constants.DDM_REMOVE: True, "param": "value", }),
+      ])
+    self.assertRaises(errors.OpPrereqError, fn, [
+      ("Hello", {}),
+      ])
+    self.assertRaises(errors.OpPrereqError, fn, [
+      (0, {
+        constants.DDM_REMOVE: True,
+        constants.DDM_ADD: True,
+        }),
+      ])
+
+    # Legacy calls
+    for action in constants.DDMS_VALUES:
+      self.assertEqual(fn([
+        (action, {}),
+        ]), [
+        (action, -1, {}),
+        ])
+
+    self.assertEqual(fn([
+      (constants.DDM_ADD, {
+        constants.IDISK_SIZE: 1024,
+        }),
+      ]), [
+      (constants.DDM_ADD, -1, {
+        constants.IDISK_SIZE: 1024,
+        }),
+      ])
+
+    # New-style calls
+    self.assertEqual(fn([
+      (2, {
+        constants.IDISK_MODE: constants.DISK_RDWR,
+        }),
+      ]), [
+      (constants.DDM_MODIFY, 2, {
+        constants.IDISK_MODE: constants.DISK_RDWR,
+        }),
+      ])
+
+    self.assertEqual(fn([
+      (0, {
+        constants.DDM_ADD: True,
+        constants.IDISK_SIZE: 4096,
+        }),
+      ]), [
+      (constants.DDM_ADD, 0, {
+        constants.IDISK_SIZE: 4096,
+        }),
+      ])
+
+    self.assertEqual(fn([
+      (-1, {
+        constants.DDM_REMOVE: True,
+        }),
+      ]), [
+      (constants.DDM_REMOVE, -1, {}),
+      ])
+
+
+class TestParseDiskSizes(unittest.TestCase):
+  def test(self):
+    fn = gnt_instance._ParseDiskSizes
+
+    self.assertEqual(fn([]), [])
+
+    # Missing size parameter
+    self.assertRaises(errors.OpPrereqError, fn, [
+      (constants.DDM_ADD, 0, {}),
+      ])
+
+    # Converting disk size
+    self.assertEqual(fn([
+      (constants.DDM_ADD, 11, {
+        constants.IDISK_SIZE: "9G",
+        }),
+      ]), [
+        (constants.DDM_ADD, 11, {
+          constants.IDISK_SIZE: 9216,
+          }),
+        ])
+
+    # No size parameter
+    self.assertEqual(fn([
+      (constants.DDM_REMOVE, 11, {
+        "other": "24M",
+        }),
+      ]), [
+        (constants.DDM_REMOVE, 11, {
+          "other": "24M",
+          }),
+        ])
+
+
 if __name__ == "__main__":
   testutils.GanetiTestProgram()
index 08cbc61..485161f 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 #
 
-# Copyright (C) 2008, 2011 Google Inc.
+# Copyright (C) 2008, 2011, 2012 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
@@ -16,7 +16,7 @@
 # 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
-# 0.0510-1301, USA.
+# 02110-1301, USA.
 
 
 """Script for unittesting the cmdlib module"""
@@ -29,6 +29,7 @@ import tempfile
 import shutil
 import operator
 import itertools
+import copy
 
 from ganeti import constants
 from ganeti import mcpu
@@ -41,6 +42,7 @@ from ganeti import ht
 from ganeti import objects
 from ganeti import compat
 from ganeti import rpc
+from ganeti.hypervisor import hv_xen
 
 import testutils
 import mocks
@@ -273,9 +275,9 @@ class TestClusterVerifySsh(unittest.TestCase):
 class TestClusterVerifyFiles(unittest.TestCase):
   @staticmethod
   def _FakeErrorIf(errors, cond, ecode, item, msg, *args, **kwargs):
-    assert ((ecode == cmdlib.LUClusterVerifyGroup.ENODEFILECHECK and
+    assert ((ecode == constants.CV_ENODEFILECHECK and
              ht.TNonEmptyString(item)) or
-            (ecode == cmdlib.LUClusterVerifyGroup.ECLUSTERFILECHECK and
+            (ecode == constants.CV_ECLUSTERFILECHECK and
              item is None))
 
     if args:
@@ -290,11 +292,12 @@ class TestClusterVerifyFiles(unittest.TestCase):
     errors = []
     master_name = "master.example.com"
     nodeinfo = [
-      objects.Node(name=master_name, offline=False),
-      objects.Node(name="node2.example.com", offline=False),
-      objects.Node(name="node3.example.com", master_candidate=True),
-      objects.Node(name="node4.example.com", offline=False),
-      objects.Node(name="nodata.example.com"),
+      objects.Node(name=master_name, offline=False, vm_capable=True),
+      objects.Node(name="node2.example.com", offline=False, vm_capable=True),
+      objects.Node(name="node3.example.com", master_candidate=True,
+                   vm_capable=False),
+      objects.Node(name="node4.example.com", offline=False, vm_capable=True),
+      objects.Node(name="nodata.example.com", offline=False, vm_capable=True),
       objects.Node(name="offline.example.com", offline=True),
       ]
     cluster = objects.Cluster(modify_etc_hosts=True,
@@ -302,24 +305,34 @@ class TestClusterVerifyFiles(unittest.TestCase):
     files_all = set([
       constants.CLUSTER_DOMAIN_SECRET_FILE,
       constants.RAPI_CERT_FILE,
+      constants.RAPI_USERS_FILE,
       ])
-    files_all_opt = set([
+    files_opt = set([
       constants.RAPI_USERS_FILE,
+      hv_xen.XL_CONFIG_FILE,
+      constants.VNC_PASSWORD_FILE,
       ])
     files_mc = set([
       constants.CLUSTER_CONF_FILE,
       ])
-    files_vm = set()
+    files_vm = set([
+      hv_xen.XEND_CONFIG_FILE,
+      hv_xen.XL_CONFIG_FILE,
+      constants.VNC_PASSWORD_FILE,
+      ])
     nvinfo = {
       master_name: rpc.RpcResult(data=(True, {
         constants.NV_FILELIST: {
           constants.CLUSTER_CONF_FILE: "82314f897f38b35f9dab2f7c6b1593e0",
           constants.RAPI_CERT_FILE: "babbce8f387bc082228e544a2146fee4",
           constants.CLUSTER_DOMAIN_SECRET_FILE: "cds-47b5b3f19202936bb4",
+          hv_xen.XEND_CONFIG_FILE: "b4a8a824ab3cac3d88839a9adeadf310",
+          hv_xen.XL_CONFIG_FILE: "77935cee92afd26d162f9e525e3d49b9"
         }})),
       "node2.example.com": rpc.RpcResult(data=(True, {
         constants.NV_FILELIST: {
           constants.RAPI_CERT_FILE: "97f0356500e866387f4b84233848cc4a",
+          hv_xen.XEND_CONFIG_FILE: "b4a8a824ab3cac3d88839a9adeadf310",
           }
         })),
       "node3.example.com": rpc.RpcResult(data=(True, {
@@ -334,6 +347,7 @@ class TestClusterVerifyFiles(unittest.TestCase):
           constants.CLUSTER_CONF_FILE: "conf-a6d4b13e407867f7a7b4f0f232a8f527",
           constants.CLUSTER_DOMAIN_SECRET_FILE: "cds-47b5b3f19202936bb4",
           constants.RAPI_USERS_FILE: "rapiusers-ea3271e8d810ef3",
+          hv_xen.XL_CONFIG_FILE: "77935cee92afd26d162f9e525e3d49b9"
           }
         })),
       "nodata.example.com": rpc.RpcResult(data=(True, {})),
@@ -343,7 +357,7 @@ class TestClusterVerifyFiles(unittest.TestCase):
 
     self._VerifyFiles(compat.partial(self._FakeErrorIf, errors), nodeinfo,
                       master_name, nvinfo,
-                      (files_all, files_all_opt, files_mc, files_vm))
+                      (files_all, files_opt, files_mc, files_vm))
     self.assertEqual(sorted(errors), sorted([
       (None, ("File %s found with 2 different checksums (variant 1 on"
               " node2.example.com, node3.example.com, node4.example.com;"
@@ -352,6 +366,8 @@ class TestClusterVerifyFiles(unittest.TestCase):
               constants.CLUSTER_DOMAIN_SECRET_FILE)),
       (None, ("File %s should not exist on node(s) node4.example.com" %
               constants.CLUSTER_CONF_FILE)),
+      (None, ("File %s is missing from node(s) node4.example.com" %
+              hv_xen.XEND_CONFIG_FILE)),
       (None, ("File %s is missing from node(s) node3.example.com" %
               constants.CLUSTER_CONF_FILE)),
       (None, ("File %s found with 2 different checksums (variant 1 on"
@@ -360,14 +376,18 @@ class TestClusterVerifyFiles(unittest.TestCase):
       (None, ("File %s is optional, but it must exist on all or no nodes (not"
               " found on master.example.com, node2.example.com,"
               " node3.example.com)" % constants.RAPI_USERS_FILE)),
+      (None, ("File %s is optional, but it must exist on all or no nodes (not"
+              " found on node2.example.com)" % hv_xen.XL_CONFIG_FILE)),
       ("nodata.example.com", "Node did not return file checksum data"),
       ]))
 
 
 class _FakeLU:
-  def __init__(self):
+  def __init__(self, cfg=NotImplemented, proc=NotImplemented):
     self.warning_log = []
     self.info_log = []
+    self.cfg = cfg
+    self.proc = proc
 
   def LogWarning(self, text, *args):
     self.warning_log.append((text, args))
@@ -430,5 +450,791 @@ class TestLoadNodeEvacResult(unittest.TestCase):
     self.assertFalse(lu.warning_log)
 
 
+class TestUpdateAndVerifySubDict(unittest.TestCase):
+  def setUp(self):
+    self.type_check = {
+        "a": constants.VTYPE_INT,
+        "b": constants.VTYPE_STRING,
+        "c": constants.VTYPE_BOOL,
+        "d": constants.VTYPE_STRING,
+        }
+
+  def test(self):
+    old_test = {
+      "foo": {
+        "d": "blubb",
+        "a": 321,
+        },
+      "baz": {
+        "a": 678,
+        "b": "678",
+        "c": True,
+        },
+      }
+    test = {
+      "foo": {
+        "a": 123,
+        "b": "123",
+        "c": True,
+        },
+      "bar": {
+        "a": 321,
+        "b": "321",
+        "c": False,
+        },
+      }
+
+    mv = {
+      "foo": {
+        "a": 123,
+        "b": "123",
+        "c": True,
+        "d": "blubb"
+        },
+      "bar": {
+        "a": 321,
+        "b": "321",
+        "c": False,
+        },
+      "baz": {
+        "a": 678,
+        "b": "678",
+        "c": True,
+        },
+      }
+
+    verified = cmdlib._UpdateAndVerifySubDict(old_test, test, self.type_check)
+    self.assertEqual(verified, mv)
+
+  def testWrong(self):
+    test = {
+      "foo": {
+        "a": "blubb",
+        "b": "123",
+        "c": True,
+        },
+      "bar": {
+        "a": 321,
+        "b": "321",
+        "c": False,
+        },
+      }
+
+    self.assertRaises(errors.TypeEnforcementError,
+                      cmdlib._UpdateAndVerifySubDict, {}, test, self.type_check)
+
+
+class TestHvStateHelper(unittest.TestCase):
+  def testWithoutOpData(self):
+    self.assertEqual(cmdlib._MergeAndVerifyHvState(None, NotImplemented), None)
+
+  def testWithoutOldData(self):
+    new = {
+      constants.HT_XEN_PVM: {
+        constants.HVST_MEMORY_TOTAL: 4096,
+        },
+      }
+    self.assertEqual(cmdlib._MergeAndVerifyHvState(new, None), new)
+
+  def testWithWrongHv(self):
+    new = {
+      "i-dont-exist": {
+        constants.HVST_MEMORY_TOTAL: 4096,
+        },
+      }
+    self.assertRaises(errors.OpPrereqError, cmdlib._MergeAndVerifyHvState, new,
+                      None)
+
+class TestDiskStateHelper(unittest.TestCase):
+  def testWithoutOpData(self):
+    self.assertEqual(cmdlib._MergeAndVerifyDiskState(None, NotImplemented),
+                     None)
+
+  def testWithoutOldData(self):
+    new = {
+      constants.LD_LV: {
+        "xenvg": {
+          constants.DS_DISK_RESERVED: 1024,
+          },
+        },
+      }
+    self.assertEqual(cmdlib._MergeAndVerifyDiskState(new, None), new)
+
+  def testWithWrongStorageType(self):
+    new = {
+      "i-dont-exist": {
+        "xenvg": {
+          constants.DS_DISK_RESERVED: 1024,
+          },
+        },
+      }
+    self.assertRaises(errors.OpPrereqError, cmdlib._MergeAndVerifyDiskState,
+                      new, None)
+
+
+class TestComputeMinMaxSpec(unittest.TestCase):
+  def setUp(self):
+    self.ipolicy = {
+      constants.ISPECS_MAX: {
+        constants.ISPEC_MEM_SIZE: 512,
+        constants.ISPEC_DISK_SIZE: 1024,
+        },
+      constants.ISPECS_MIN: {
+        constants.ISPEC_MEM_SIZE: 128,
+        constants.ISPEC_DISK_COUNT: 1,
+        },
+      }
+
+  def testNoneValue(self):
+    self.assertTrue(cmdlib._ComputeMinMaxSpec(constants.ISPEC_MEM_SIZE, None,
+                                              self.ipolicy, None) is None)
+
+  def testAutoValue(self):
+    self.assertTrue(cmdlib._ComputeMinMaxSpec(constants.ISPEC_MEM_SIZE, None,
+                                              self.ipolicy,
+                                              constants.VALUE_AUTO) is None)
+
+  def testNotDefined(self):
+    self.assertTrue(cmdlib._ComputeMinMaxSpec(constants.ISPEC_NIC_COUNT, None,
+                                              self.ipolicy, 3) is None)
+
+  def testNoMinDefined(self):
+    self.assertTrue(cmdlib._ComputeMinMaxSpec(constants.ISPEC_DISK_SIZE, None,
+                                              self.ipolicy, 128) is None)
+
+  def testNoMaxDefined(self):
+    self.assertTrue(cmdlib._ComputeMinMaxSpec(constants.ISPEC_DISK_COUNT, None,
+                                                self.ipolicy, 16) is None)
+
+  def testOutOfRange(self):
+    for (name, val) in ((constants.ISPEC_MEM_SIZE, 64),
+                        (constants.ISPEC_MEM_SIZE, 768),
+                        (constants.ISPEC_DISK_SIZE, 4096),
+                        (constants.ISPEC_DISK_COUNT, 0)):
+      min_v = self.ipolicy[constants.ISPECS_MIN].get(name, val)
+      max_v = self.ipolicy[constants.ISPECS_MAX].get(name, val)
+      self.assertEqual(cmdlib._ComputeMinMaxSpec(name, None,
+                                                 self.ipolicy, val),
+                       "%s value %s is not in range [%s, %s]" %
+                       (name, val,min_v, max_v))
+      self.assertEqual(cmdlib._ComputeMinMaxSpec(name, "1",
+                                                 self.ipolicy, val),
+                       "%s/1 value %s is not in range [%s, %s]" %
+                       (name, val,min_v, max_v))
+
+  def test(self):
+    for (name, val) in ((constants.ISPEC_MEM_SIZE, 256),
+                        (constants.ISPEC_MEM_SIZE, 128),
+                        (constants.ISPEC_MEM_SIZE, 512),
+                        (constants.ISPEC_DISK_SIZE, 1024),
+                        (constants.ISPEC_DISK_SIZE, 0),
+                        (constants.ISPEC_DISK_COUNT, 1),
+                        (constants.ISPEC_DISK_COUNT, 5)):
+      self.assertTrue(cmdlib._ComputeMinMaxSpec(name, None, self.ipolicy, val)
+                      is None)
+
+
+def _ValidateComputeMinMaxSpec(name, *_):
+  assert name in constants.ISPECS_PARAMETERS
+  return None
+
+
+class _SpecWrapper:
+  def __init__(self, spec):
+    self.spec = spec
+
+  def ComputeMinMaxSpec(self, *args):
+    return self.spec.pop(0)
+
+
+class TestComputeIPolicySpecViolation(unittest.TestCase):
+  def test(self):
+    compute_fn = _ValidateComputeMinMaxSpec
+    ret = cmdlib._ComputeIPolicySpecViolation(NotImplemented, 1024, 1, 1, 1,
+                                              [1024], 1, _compute_fn=compute_fn)
+    self.assertEqual(ret, [])
+
+  def testInvalidArguments(self):
+    self.assertRaises(AssertionError, cmdlib._ComputeIPolicySpecViolation,
+                      NotImplemented, 1024, 1, 1, 1, [], 1)
+
+  def testInvalidSpec(self):
+    spec = _SpecWrapper([None, False, "foo", None, "bar", None])
+    compute_fn = spec.ComputeMinMaxSpec
+    ret = cmdlib._ComputeIPolicySpecViolation(NotImplemented, 1024, 1, 1, 1,
+                                              [1024], 1, _compute_fn=compute_fn)
+    self.assertEqual(ret, ["foo", "bar"])
+    self.assertFalse(spec.spec)
+
+
+class _StubComputeIPolicySpecViolation:
+  def __init__(self, mem_size, cpu_count, disk_count, nic_count, disk_sizes,
+               spindle_use):
+    self.mem_size = mem_size
+    self.cpu_count = cpu_count
+    self.disk_count = disk_count
+    self.nic_count = nic_count
+    self.disk_sizes = disk_sizes
+    self.spindle_use = spindle_use
+
+  def __call__(self, _, mem_size, cpu_count, disk_count, nic_count, disk_sizes,
+               spindle_use):
+    assert self.mem_size == mem_size
+    assert self.cpu_count == cpu_count
+    assert self.disk_count == disk_count
+    assert self.nic_count == nic_count
+    assert self.disk_sizes == disk_sizes
+    assert self.spindle_use == spindle_use
+
+    return []
+
+
+class TestComputeIPolicyInstanceViolation(unittest.TestCase):
+  def test(self):
+    beparams = {
+      constants.BE_MAXMEM: 2048,
+      constants.BE_VCPUS: 2,
+      constants.BE_SPINDLE_USE: 4,
+      }
+    disks = [objects.Disk(size=512)]
+    instance = objects.Instance(beparams=beparams, disks=disks, nics=[])
+    stub = _StubComputeIPolicySpecViolation(2048, 2, 1, 0, [512], 4)
+    ret = cmdlib._ComputeIPolicyInstanceViolation(NotImplemented, instance,
+                                                  _compute_fn=stub)
+    self.assertEqual(ret, [])
+
+
+class TestComputeIPolicyInstanceSpecViolation(unittest.TestCase):
+  def test(self):
+    ispec = {
+      constants.ISPEC_MEM_SIZE: 2048,
+      constants.ISPEC_CPU_COUNT: 2,
+      constants.ISPEC_DISK_COUNT: 1,
+      constants.ISPEC_DISK_SIZE: [512],
+      constants.ISPEC_NIC_COUNT: 0,
+      constants.ISPEC_SPINDLE_USE: 1,
+      }
+    stub = _StubComputeIPolicySpecViolation(2048, 2, 1, 0, [512], 1)
+    ret = cmdlib._ComputeIPolicyInstanceSpecViolation(NotImplemented, ispec,
+                                                      _compute_fn=stub)
+    self.assertEqual(ret, [])
+
+
+class _CallRecorder:
+  def __init__(self, return_value=None):
+    self.called = False
+    self.return_value = return_value
+
+  def __call__(self, *args):
+    self.called = True
+    return self.return_value
+
+
+class TestComputeIPolicyNodeViolation(unittest.TestCase):
+  def setUp(self):
+    self.recorder = _CallRecorder(return_value=[])
+
+  def testSameGroup(self):
+    ret = cmdlib._ComputeIPolicyNodeViolation(NotImplemented, NotImplemented,
+                                              "foo", "foo",
+                                              _compute_fn=self.recorder)
+    self.assertFalse(self.recorder.called)
+    self.assertEqual(ret, [])
+
+  def testDifferentGroup(self):
+    ret = cmdlib._ComputeIPolicyNodeViolation(NotImplemented, NotImplemented,
+                                              "foo", "bar",
+                                              _compute_fn=self.recorder)
+    self.assertTrue(self.recorder.called)
+    self.assertEqual(ret, [])
+
+
+class _FakeConfigForTargetNodeIPolicy:
+  def __init__(self, node_info=NotImplemented):
+    self._node_info = node_info
+
+  def GetNodeInfo(self, _):
+    return self._node_info
+
+
+class TestCheckTargetNodeIPolicy(unittest.TestCase):
+  def setUp(self):
+    self.instance = objects.Instance(primary_node="blubb")
+    self.target_node = objects.Node(group="bar")
+    node_info = objects.Node(group="foo")
+    fake_cfg = _FakeConfigForTargetNodeIPolicy(node_info=node_info)
+    self.lu = _FakeLU(cfg=fake_cfg)
+
+  def testNoViolation(self):
+    compute_recoder = _CallRecorder(return_value=[])
+    cmdlib._CheckTargetNodeIPolicy(self.lu, NotImplemented, self.instance,
+                                   self.target_node,
+                                   _compute_fn=compute_recoder)
+    self.assertTrue(compute_recoder.called)
+    self.assertEqual(self.lu.warning_log, [])
+
+  def testNoIgnore(self):
+    compute_recoder = _CallRecorder(return_value=["mem_size not in range"])
+    self.assertRaises(errors.OpPrereqError, cmdlib._CheckTargetNodeIPolicy,
+                      self.lu, NotImplemented, self.instance, self.target_node,
+                      _compute_fn=compute_recoder)
+    self.assertTrue(compute_recoder.called)
+    self.assertEqual(self.lu.warning_log, [])
+
+  def testIgnoreViolation(self):
+    compute_recoder = _CallRecorder(return_value=["mem_size not in range"])
+    cmdlib._CheckTargetNodeIPolicy(self.lu, NotImplemented, self.instance,
+                                   self.target_node, ignore=True,
+                                   _compute_fn=compute_recoder)
+    self.assertTrue(compute_recoder.called)
+    msg = ("Instance does not meet target node group's (bar) instance policy:"
+           " mem_size not in range")
+    self.assertEqual(self.lu.warning_log, [(msg, ())])
+
+
+class TestApplyContainerMods(unittest.TestCase):
+  def testEmptyContainer(self):
+    container = []
+    chgdesc = []
+    cmdlib.ApplyContainerMods("test", container, chgdesc, [], None, None, None)
+    self.assertEqual(container, [])
+    self.assertEqual(chgdesc, [])
+
+  def testAdd(self):
+    container = []
+    chgdesc = []
+    mods = cmdlib.PrepareContainerMods([
+      (constants.DDM_ADD, -1, "Hello"),
+      (constants.DDM_ADD, -1, "World"),
+      (constants.DDM_ADD, 0, "Start"),
+      (constants.DDM_ADD, -1, "End"),
+      ], None)
+    cmdlib.ApplyContainerMods("test", container, chgdesc, mods,
+                              None, None, None)
+    self.assertEqual(container, ["Start", "Hello", "World", "End"])
+    self.assertEqual(chgdesc, [])
+
+    mods = cmdlib.PrepareContainerMods([
+      (constants.DDM_ADD, 0, "zero"),
+      (constants.DDM_ADD, 3, "Added"),
+      (constants.DDM_ADD, 5, "four"),
+      (constants.DDM_ADD, 7, "xyz"),
+      ], None)
+    cmdlib.ApplyContainerMods("test", container, chgdesc, mods,
+                              None, None, None)
+    self.assertEqual(container,
+                     ["zero", "Start", "Hello", "Added", "World", "four",
+                      "End", "xyz"])
+    self.assertEqual(chgdesc, [])
+
+    for idx in [-2, len(container) + 1]:
+      mods = cmdlib.PrepareContainerMods([
+        (constants.DDM_ADD, idx, "error"),
+        ], None)
+      self.assertRaises(IndexError, cmdlib.ApplyContainerMods,
+                        "test", container, None, mods, None, None, None)
+
+  def testRemoveError(self):
+    for idx in [0, 1, 2, 100, -1, -4]:
+      mods = cmdlib.PrepareContainerMods([
+        (constants.DDM_REMOVE, idx, None),
+        ], None)
+      self.assertRaises(IndexError, cmdlib.ApplyContainerMods,
+                        "test", [], None, mods, None, None, None)
+
+    mods = cmdlib.PrepareContainerMods([
+      (constants.DDM_REMOVE, 0, object()),
+      ], None)
+    self.assertRaises(AssertionError, cmdlib.ApplyContainerMods,
+                      "test", [""], None, mods, None, None, None)
+
+  def testAddError(self):
+    for idx in range(-100, -1) + [100]:
+      mods = cmdlib.PrepareContainerMods([
+        (constants.DDM_ADD, idx, None),
+        ], None)
+      self.assertRaises(IndexError, cmdlib.ApplyContainerMods,
+                        "test", [], None, mods, None, None, None)
+
+  def testRemove(self):
+    container = ["item 1", "item 2"]
+    mods = cmdlib.PrepareContainerMods([
+      (constants.DDM_ADD, -1, "aaa"),
+      (constants.DDM_REMOVE, -1, None),
+      (constants.DDM_ADD, -1, "bbb"),
+      ], None)
+    chgdesc = []
+    cmdlib.ApplyContainerMods("test", container, chgdesc, mods,
+                              None, None, None)
+    self.assertEqual(container, ["item 1", "item 2", "bbb"])
+    self.assertEqual(chgdesc, [
+      ("test/2", "remove"),
+      ])
+
+  def testModify(self):
+    container = ["item 1", "item 2"]
+    mods = cmdlib.PrepareContainerMods([
+      (constants.DDM_MODIFY, -1, "a"),
+      (constants.DDM_MODIFY, 0, "b"),
+      (constants.DDM_MODIFY, 1, "c"),
+      ], None)
+    chgdesc = []
+    cmdlib.ApplyContainerMods("test", container, chgdesc, mods,
+                              None, None, None)
+    self.assertEqual(container, ["item 1", "item 2"])
+    self.assertEqual(chgdesc, [])
+
+    for idx in [-2, len(container) + 1]:
+      mods = cmdlib.PrepareContainerMods([
+        (constants.DDM_MODIFY, idx, "error"),
+        ], None)
+      self.assertRaises(IndexError, cmdlib.ApplyContainerMods,
+                        "test", container, None, mods, None, None, None)
+
+  class _PrivateData:
+    def __init__(self):
+      self.data = None
+
+  @staticmethod
+  def _CreateTestFn(idx, params, private):
+    private.data = ("add", idx, params)
+    return ((100 * idx, params), [
+      ("test/%s" % idx, hex(idx)),
+      ])
+
+  @staticmethod
+  def _ModifyTestFn(idx, item, params, private):
+    private.data = ("modify", idx, params)
+    return [
+      ("test/%s" % idx, "modify %s" % params),
+      ]
+
+  @staticmethod
+  def _RemoveTestFn(idx, item, private):
+    private.data = ("remove", idx, item)
+
+  def testAddWithCreateFunction(self):
+    container = []
+    chgdesc = []
+    mods = cmdlib.PrepareContainerMods([
+      (constants.DDM_ADD, -1, "Hello"),
+      (constants.DDM_ADD, -1, "World"),
+      (constants.DDM_ADD, 0, "Start"),
+      (constants.DDM_ADD, -1, "End"),
+      (constants.DDM_REMOVE, 2, None),
+      (constants.DDM_MODIFY, -1, "foobar"),
+      (constants.DDM_REMOVE, 2, None),
+      (constants.DDM_ADD, 1, "More"),
+      ], self._PrivateData)
+    cmdlib.ApplyContainerMods("test", container, chgdesc, mods,
+      self._CreateTestFn, self._ModifyTestFn, self._RemoveTestFn)
+    self.assertEqual(container, [
+      (000, "Start"),
+      (100, "More"),
+      (000, "Hello"),
+      ])
+    self.assertEqual(chgdesc, [
+      ("test/0", "0x0"),
+      ("test/1", "0x1"),
+      ("test/0", "0x0"),
+      ("test/3", "0x3"),
+      ("test/2", "remove"),
+      ("test/2", "modify foobar"),
+      ("test/2", "remove"),
+      ("test/1", "0x1")
+      ])
+    self.assertTrue(compat.all(op == private.data[0]
+                               for (op, _, _, private) in mods))
+    self.assertEqual([private.data for (op, _, _, private) in mods], [
+      ("add", 0, "Hello"),
+      ("add", 1, "World"),
+      ("add", 0, "Start"),
+      ("add", 3, "End"),
+      ("remove", 2, (100, "World")),
+      ("modify", 2, "foobar"),
+      ("remove", 2, (300, "End")),
+      ("add", 1, "More"),
+      ])
+
+
+class _FakeConfigForGenDiskTemplate:
+  def __init__(self):
+    self._unique_id = itertools.count()
+    self._drbd_minor = itertools.count(20)
+    self._port = itertools.count(constants.FIRST_DRBD_PORT)
+    self._secret = itertools.count()
+
+  def GetVGName(self):
+    return "testvg"
+
+  def GenerateUniqueID(self, ec_id):
+    return "ec%s-uq%s" % (ec_id, self._unique_id.next())
+
+  def AllocateDRBDMinor(self, nodes, instance):
+    return [self._drbd_minor.next()
+            for _ in nodes]
+
+  def AllocatePort(self):
+    return self._port.next()
+
+  def GenerateDRBDSecret(self, ec_id):
+    return "ec%s-secret%s" % (ec_id, self._secret.next())
+
+  def GetInstanceInfo(self, _):
+    return "foobar"
+
+
+class _FakeProcForGenDiskTemplate:
+  def GetECId(self):
+    return 0
+
+
+class TestGenerateDiskTemplate(unittest.TestCase):
+  def setUp(self):
+    nodegroup = objects.NodeGroup(name="ng")
+    nodegroup.UpgradeConfig()
+
+    cfg = _FakeConfigForGenDiskTemplate()
+    proc = _FakeProcForGenDiskTemplate()
+
+    self.lu = _FakeLU(cfg=cfg, proc=proc)
+    self.nodegroup = nodegroup
+
+  @staticmethod
+  def GetDiskParams():
+    return copy.deepcopy(constants.DISK_DT_DEFAULTS)
+
+  def testWrongDiskTemplate(self):
+    gdt = cmdlib._GenerateDiskTemplate
+    disk_template = "##unknown##"
+
+    assert disk_template not in constants.DISK_TEMPLATES
+
+    self.assertRaises(errors.ProgrammerError, gdt, self.lu, disk_template,
+                      "inst26831.example.com", "node30113.example.com", [], [],
+                      NotImplemented, NotImplemented, 0, self.lu.LogInfo,
+                      self.GetDiskParams())
+
+  def testDiskless(self):
+    gdt = cmdlib._GenerateDiskTemplate
+
+    result = gdt(self.lu, constants.DT_DISKLESS, "inst27734.example.com",
+                 "node30113.example.com", [], [],
+                 NotImplemented, NotImplemented, 0, self.lu.LogInfo,
+                 self.GetDiskParams())
+    self.assertEqual(result, [])
+
+  def _TestTrivialDisk(self, template, disk_info, base_index, exp_dev_type,
+                       file_storage_dir=NotImplemented,
+                       file_driver=NotImplemented,
+                       req_file_storage=NotImplemented,
+                       req_shr_file_storage=NotImplemented):
+    gdt = cmdlib._GenerateDiskTemplate
+
+    map(lambda params: utils.ForceDictType(params,
+                                           constants.IDISK_PARAMS_TYPES),
+        disk_info)
+
+    # Check if non-empty list of secondaries is rejected
+    self.assertRaises(errors.ProgrammerError, gdt, self.lu,
+                      template, "inst25088.example.com",
+                      "node185.example.com", ["node323.example.com"], [],
+                      NotImplemented, NotImplemented, base_index,
+                      self.lu.LogInfo, self.GetDiskParams(),
+                      _req_file_storage=req_file_storage,
+                      _req_shr_file_storage=req_shr_file_storage)
+
+    result = gdt(self.lu, template, "inst21662.example.com",
+                 "node21741.example.com", [],
+                 disk_info, file_storage_dir, file_driver, base_index,
+                 self.lu.LogInfo, self.GetDiskParams(),
+                 _req_file_storage=req_file_storage,
+                 _req_shr_file_storage=req_shr_file_storage)
+
+    for (idx, disk) in enumerate(result):
+      self.assertTrue(isinstance(disk, objects.Disk))
+      self.assertEqual(disk.dev_type, exp_dev_type)
+      self.assertEqual(disk.size, disk_info[idx][constants.IDISK_SIZE])
+      self.assertEqual(disk.mode, disk_info[idx][constants.IDISK_MODE])
+      self.assertTrue(disk.children is None)
+
+    self._CheckIvNames(result, base_index, base_index + len(disk_info))
+    cmdlib._UpdateIvNames(base_index, result)
+    self._CheckIvNames(result, base_index, base_index + len(disk_info))
+
+    return result
+
+  def _CheckIvNames(self, disks, base_index, end_index):
+    self.assertEqual(map(operator.attrgetter("iv_name"), disks),
+                     ["disk/%s" % i for i in range(base_index, end_index)])
+
+  def testPlain(self):
+    disk_info = [{
+      constants.IDISK_SIZE: 1024,
+      constants.IDISK_MODE: constants.DISK_RDWR,
+      }, {
+      constants.IDISK_SIZE: 4096,
+      constants.IDISK_VG: "othervg",
+      constants.IDISK_MODE: constants.DISK_RDWR,
+      }]
+
+    result = self._TestTrivialDisk(constants.DT_PLAIN, disk_info, 3,
+                                   constants.LD_LV)
+
+    self.assertEqual(map(operator.attrgetter("logical_id"), result), [
+      ("testvg", "ec0-uq0.disk3"),
+      ("othervg", "ec0-uq1.disk4"),
+      ])
+
+  @staticmethod
+  def _AllowFileStorage():
+    pass
+
+  @staticmethod
+  def _ForbidFileStorage():
+    raise errors.OpPrereqError("Disallowed in test")
+
+  def testFile(self):
+    self.assertRaises(errors.OpPrereqError, self._TestTrivialDisk,
+                      constants.DT_FILE, [], 0, NotImplemented,
+                      req_file_storage=self._ForbidFileStorage)
+    self.assertRaises(errors.OpPrereqError, self._TestTrivialDisk,
+                      constants.DT_SHARED_FILE, [], 0, NotImplemented,
+                      req_shr_file_storage=self._ForbidFileStorage)
+
+    for disk_template in [constants.DT_FILE, constants.DT_SHARED_FILE]:
+      disk_info = [{
+        constants.IDISK_SIZE: 80 * 1024,
+        constants.IDISK_MODE: constants.DISK_RDONLY,
+        }, {
+        constants.IDISK_SIZE: 4096,
+        constants.IDISK_MODE: constants.DISK_RDWR,
+        }, {
+        constants.IDISK_SIZE: 6 * 1024,
+        constants.IDISK_MODE: constants.DISK_RDWR,
+        }]
+
+      result = self._TestTrivialDisk(disk_template, disk_info, 2,
+        constants.LD_FILE, file_storage_dir="/tmp",
+        file_driver=constants.FD_BLKTAP,
+        req_file_storage=self._AllowFileStorage,
+        req_shr_file_storage=self._AllowFileStorage)
+
+      self.assertEqual(map(operator.attrgetter("logical_id"), result), [
+        (constants.FD_BLKTAP, "/tmp/disk2"),
+        (constants.FD_BLKTAP, "/tmp/disk3"),
+        (constants.FD_BLKTAP, "/tmp/disk4"),
+        ])
+
+  def testBlock(self):
+    disk_info = [{
+      constants.IDISK_SIZE: 8 * 1024,
+      constants.IDISK_MODE: constants.DISK_RDWR,
+      constants.IDISK_ADOPT: "/tmp/some/block/dev",
+      }]
+
+    result = self._TestTrivialDisk(constants.DT_BLOCK, disk_info, 10,
+                                   constants.LD_BLOCKDEV)
+
+    self.assertEqual(map(operator.attrgetter("logical_id"), result), [
+      (constants.BLOCKDEV_DRIVER_MANUAL, "/tmp/some/block/dev"),
+      ])
+
+  def testRbd(self):
+    disk_info = [{
+      constants.IDISK_SIZE: 8 * 1024,
+      constants.IDISK_MODE: constants.DISK_RDONLY,
+      }, {
+      constants.IDISK_SIZE: 100 * 1024,
+      constants.IDISK_MODE: constants.DISK_RDWR,
+      }]
+
+    result = self._TestTrivialDisk(constants.DT_RBD, disk_info, 0,
+                                   constants.LD_RBD)
+
+    self.assertEqual(map(operator.attrgetter("logical_id"), result), [
+      ("rbd", "ec0-uq0.rbd.disk0"),
+      ("rbd", "ec0-uq1.rbd.disk1"),
+      ])
+
+  def testDrbd8(self):
+    gdt = cmdlib._GenerateDiskTemplate
+    drbd8_defaults = constants.DISK_LD_DEFAULTS[constants.LD_DRBD8]
+    drbd8_default_metavg = drbd8_defaults[constants.LDP_DEFAULT_METAVG]
+
+    disk_info = [{
+      constants.IDISK_SIZE: 1024,
+      constants.IDISK_MODE: constants.DISK_RDWR,
+      }, {
+      constants.IDISK_SIZE: 100 * 1024,
+      constants.IDISK_MODE: constants.DISK_RDONLY,
+      constants.IDISK_METAVG: "metavg",
+      }, {
+      constants.IDISK_SIZE: 4096,
+      constants.IDISK_MODE: constants.DISK_RDWR,
+      constants.IDISK_VG: "vgxyz",
+      },
+      ]
+
+    exp_logical_ids = [[
+      (self.lu.cfg.GetVGName(), "ec0-uq0.disk0_data"),
+      (drbd8_default_metavg, "ec0-uq0.disk0_meta"),
+      ], [
+      (self.lu.cfg.GetVGName(), "ec0-uq1.disk1_data"),
+      ("metavg", "ec0-uq1.disk1_meta"),
+      ], [
+      ("vgxyz", "ec0-uq2.disk2_data"),
+      (drbd8_default_metavg, "ec0-uq2.disk2_meta"),
+      ]]
+
+    assert len(exp_logical_ids) == len(disk_info)
+
+    map(lambda params: utils.ForceDictType(params,
+                                           constants.IDISK_PARAMS_TYPES),
+        disk_info)
+
+    # Check if empty list of secondaries is rejected
+    self.assertRaises(errors.ProgrammerError, gdt, self.lu, constants.DT_DRBD8,
+                      "inst827.example.com", "node1334.example.com", [],
+                      disk_info, NotImplemented, NotImplemented, 0,
+                      self.lu.LogInfo, self.GetDiskParams())
+
+    result = gdt(self.lu, constants.DT_DRBD8, "inst827.example.com",
+                 "node1334.example.com", ["node12272.example.com"],
+                 disk_info, NotImplemented, NotImplemented, 0, self.lu.LogInfo,
+                 self.GetDiskParams())
+
+    for (idx, disk) in enumerate(result):
+      self.assertTrue(isinstance(disk, objects.Disk))
+      self.assertEqual(disk.dev_type, constants.LD_DRBD8)
+      self.assertEqual(disk.size, disk_info[idx][constants.IDISK_SIZE])
+      self.assertEqual(disk.mode, disk_info[idx][constants.IDISK_MODE])
+
+      for child in disk.children:
+        self.assertTrue(isinstance(disk, objects.Disk))
+        self.assertEqual(child.dev_type, constants.LD_LV)
+        self.assertTrue(child.children is None)
+
+      self.assertEqual(map(operator.attrgetter("logical_id"), disk.children),
+                       exp_logical_ids[idx])
+
+      self.assertEqual(len(disk.children), 2)
+      self.assertEqual(disk.children[0].size, disk.size)
+      self.assertEqual(disk.children[1].size, cmdlib.DRBD_META_SIZE)
+
+    self._CheckIvNames(result, 0, len(disk_info))
+    cmdlib._UpdateIvNames(0, result)
+    self._CheckIvNames(result, 0, len(disk_info))
+
+    self.assertEqual(map(operator.attrgetter("logical_id"), result), [
+      ("node1334.example.com", "node12272.example.com",
+       constants.FIRST_DRBD_PORT, 20, 21, "ec0-secret0"),
+      ("node1334.example.com", "node12272.example.com",
+       constants.FIRST_DRBD_PORT + 1, 22, 23, "ec0-secret1"),
+      ("node1334.example.com", "node12272.example.com",
+       constants.FIRST_DRBD_PORT + 2, 24, 25, "ec0-secret2"),
+      ])
+
+
 if __name__ == "__main__":
   testutils.GanetiTestProgram()
index d816d16..0afec60 100755 (executable)
@@ -16,7 +16,7 @@
 # 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
-# 0.0510-1301, USA.
+# 02110-1301, USA.
 
 
 """Script for unittesting the confd client module"""
index e82872c..153a2fe 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 #
 
-# Copyright (C) 2006, 2007, 2010, 2011 Google Inc.
+# Copyright (C) 2006, 2007, 2010, 2011, 2012 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
@@ -39,6 +39,7 @@ from ganeti import objects
 from ganeti import utils
 from ganeti import netutils
 from ganeti import compat
+from ganeti import cmdlib
 
 from ganeti.config import TemporaryReservationManager
 
@@ -199,6 +200,7 @@ class TestConfigRunner(unittest.TestCase):
   def testGetNdParamsModifiedNode(self):
     my_ndparams = {
         constants.ND_OOB_PROGRAM: "/bin/node-oob",
+        constants.ND_SPINDLE_COUNT: 1,
         }
 
     cfg = self._get_object()
@@ -382,5 +384,28 @@ class TestTRM(unittest.TestCase):
     self.assertFalse(t.Reserved("a"))
 
 
+class TestCheckInstanceDiskIvNames(unittest.TestCase):
+  @staticmethod
+  def _MakeDisks(names):
+    return [objects.Disk(iv_name=name) for name in names]
+
+  def testNoError(self):
+    disks = self._MakeDisks(["disk/0", "disk/1"])
+    self.assertEqual(config._CheckInstanceDiskIvNames(disks), [])
+    cmdlib._UpdateIvNames(0, disks)
+    self.assertEqual(config._CheckInstanceDiskIvNames(disks), [])
+
+  def testWrongNames(self):
+    disks = self._MakeDisks(["disk/1", "disk/3", "disk/2"])
+    self.assertEqual(config._CheckInstanceDiskIvNames(disks), [
+      (0, "disk/0", "disk/1"),
+      (1, "disk/1", "disk/3"),
+      ])
+
+    # Fix names
+    cmdlib._UpdateIvNames(0, disks)
+    self.assertEqual(config._CheckInstanceDiskIvNames(disks), [])
+
+
 if __name__ == '__main__':
   testutils.GanetiTestProgram()
index 9aab10f..0b7736c 100755 (executable)
@@ -16,7 +16,7 @@
 # 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
-# 0.0510-1301, USA.
+# 02110-1301, USA.
 
 
 """Script for unittesting the constants module"""
 
 import unittest
 import re
+import itertools
 
 from ganeti import constants
 from ganeti import locking
+from ganeti import utils
 
 import testutils
 
@@ -77,6 +79,31 @@ class TestConstants(unittest.TestCase):
     self.failUnless(constants.OP_PRIO_NORMAL > constants.OP_PRIO_HIGH)
     self.failUnless(constants.OP_PRIO_HIGH > constants.OP_PRIO_HIGHEST)
 
+  def testDiskDefaults(self):
+    self.failUnless(set(constants.DISK_LD_DEFAULTS.keys()) ==
+                    constants.LOGICAL_DISK_TYPES)
+    self.failUnless(set(constants.DISK_DT_DEFAULTS.keys()) ==
+                    constants.DISK_TEMPLATES)
+
+
+class TestExportedNames(unittest.TestCase):
+  _VALID_NAME_RE = re.compile(r"^[A-Z][A-Z0-9_]+$")
+  _BUILTIN_NAME_RE = re.compile(r"^__\w+__$")
+  _EXCEPTIONS = frozenset([
+    "SplitVersion",
+    "BuildVersion",
+    ])
+
+  def test(self):
+    wrong = \
+      set(itertools.ifilterfalse(self._BUILTIN_NAME_RE.match,
+            itertools.ifilterfalse(self._VALID_NAME_RE.match,
+                                   dir(constants))))
+    wrong -= self._EXCEPTIONS
+    self.assertFalse(wrong,
+                     msg=("Invalid names exported from constants module: %s" %
+                          utils.CommaJoin(sorted(wrong))))
+
 
 class TestParameterNames(unittest.TestCase):
   """HV/BE parameter tests"""
index abbb7c1..ddeb105 100755 (executable)
@@ -204,7 +204,7 @@ def FakeHooksRpcSuccess(node_list, hpath, phase, env):
 
   """
   rr = rpc.RpcResult
-  return dict([(node, rr(True, [("utest", constants.HKR_SUCCESS, "ok")],
+  return dict([(node, rr((True, [("utest", constants.HKR_SUCCESS, "ok")]),
                          node=node, call='FakeScriptOk'))
                for node in node_list])
 
@@ -249,14 +249,14 @@ class TestHooksMaster(unittest.TestCase):
 
   def testTotalFalse(self):
     """Test complete rpc failure"""
-    hm = mcpu.HooksMaster(self._call_false, self.lu)
+    hm = mcpu.HooksMaster.BuildFromLu(self._call_false, self.lu)
     self.failUnlessRaises(errors.HooksFailure,
                           hm.RunPhase, constants.HOOKS_PHASE_PRE)
     hm.RunPhase(constants.HOOKS_PHASE_POST)
 
   def testIndividualFalse(self):
     """Test individual node failure"""
-    hm = mcpu.HooksMaster(self._call_nodes_false, self.lu)
+    hm = mcpu.HooksMaster.BuildFromLu(self._call_nodes_false, self.lu)
     hm.RunPhase(constants.HOOKS_PHASE_PRE)
     #self.failUnlessRaises(errors.HooksFailure,
     #                      hm.RunPhase, constants.HOOKS_PHASE_PRE)
@@ -264,14 +264,14 @@ class TestHooksMaster(unittest.TestCase):
 
   def testScriptFalse(self):
     """Test individual rpc failure"""
-    hm = mcpu.HooksMaster(self._call_script_fail, self.lu)
+    hm = mcpu.HooksMaster.BuildFromLu(self._call_script_fail, self.lu)
     self.failUnlessRaises(errors.HooksAbort,
                           hm.RunPhase, constants.HOOKS_PHASE_PRE)
     hm.RunPhase(constants.HOOKS_PHASE_POST)
 
   def testScriptSucceed(self):
     """Test individual rpc failure"""
-    hm = mcpu.HooksMaster(FakeHooksRpcSuccess, self.lu)
+    hm = mcpu.HooksMaster.BuildFromLu(FakeHooksRpcSuccess, self.lu)
     for phase in (constants.HOOKS_PHASE_PRE, constants.HOOKS_PHASE_POST):
       hm.RunPhase(phase)
 
@@ -322,7 +322,7 @@ class TestHooksRunnerEnv(unittest.TestCase):
   def testEmptyEnv(self):
     # Check pre-phase hook
     self.lu.hook_env = {}
-    hm = mcpu.HooksMaster(self._HooksRpc, self.lu)
+    hm = mcpu.HooksMaster.BuildFromLu(self._HooksRpc, self.lu)
     hm.RunPhase(constants.HOOKS_PHASE_PRE)
 
     (node_list, hpath, phase, env) = self._rpcs.pop(0)
@@ -348,7 +348,7 @@ class TestHooksRunnerEnv(unittest.TestCase):
     self.lu.hook_env = {
       "FOO": "pre-foo-value",
       }
-    hm = mcpu.HooksMaster(self._HooksRpc, self.lu)
+    hm = mcpu.HooksMaster.BuildFromLu(self._HooksRpc, self.lu)
     hm.RunPhase(constants.HOOKS_PHASE_PRE)
 
     (node_list, hpath, phase, env) = self._rpcs.pop(0)
@@ -395,7 +395,7 @@ class TestHooksRunnerEnv(unittest.TestCase):
       self.lu.hook_env = { name: "value" }
 
       # Test using a clean HooksMaster instance
-      hm = mcpu.HooksMaster(self._HooksRpc, self.lu)
+      hm = mcpu.HooksMaster.BuildFromLu(self._HooksRpc, self.lu)
 
       for phase in [constants.HOOKS_PHASE_PRE, constants.HOOKS_PHASE_POST]:
         self.assertRaises(AssertionError, hm.RunPhase, phase)
@@ -403,7 +403,7 @@ class TestHooksRunnerEnv(unittest.TestCase):
 
   def testNoNodes(self):
     self.lu.hook_env = {}
-    hm = mcpu.HooksMaster(self._HooksRpc, self.lu)
+    hm = mcpu.HooksMaster.BuildFromLu(self._HooksRpc, self.lu)
     hm.RunPhase(constants.HOOKS_PHASE_PRE, nodes=[])
     self.assertRaises(IndexError, self._rpcs.pop)
 
@@ -415,7 +415,7 @@ class TestHooksRunnerEnv(unittest.TestCase):
       "node93782.example.net",
       ]
 
-    hm = mcpu.HooksMaster(self._HooksRpc, self.lu)
+    hm = mcpu.HooksMaster.BuildFromLu(self._HooksRpc, self.lu)
 
     for phase in [constants.HOOKS_PHASE_PRE, constants.HOOKS_PHASE_POST]:
       hm.RunPhase(phase, nodes=nodes)
@@ -433,7 +433,7 @@ class TestHooksRunnerEnv(unittest.TestCase):
       "FOO": "value",
       }
 
-    hm = mcpu.HooksMaster(self._HooksRpc, self.lu)
+    hm = mcpu.HooksMaster.BuildFromLu(self._HooksRpc, self.lu)
     hm.RunConfigUpdate()
 
     (node_list, hpath, phase, env) = self._rpcs.pop(0)
@@ -452,7 +452,7 @@ class TestHooksRunnerEnv(unittest.TestCase):
       "FOO": "value",
       }
 
-    hm = mcpu.HooksMaster(self._HooksRpc, self.lu)
+    hm = mcpu.HooksMaster.BuildFromLu(self._HooksRpc, self.lu)
     hm.RunPhase(constants.HOOKS_PHASE_POST)
 
     (node_list, hpath, phase, env) = self._rpcs.pop(0)
@@ -470,7 +470,7 @@ class TestHooksRunnerEnv(unittest.TestCase):
     self.assertRaises(AssertionError, self.lu.BuildHooksEnv)
     self.assertRaises(AssertionError, self.lu.BuildHooksNodes)
 
-    hm = mcpu.HooksMaster(self._HooksRpc, self.lu)
+    hm = mcpu.HooksMaster.BuildFromLu(self._HooksRpc, self.lu)
     self.assertEqual(hm.pre_env, {})
     self.assertRaises(IndexError, self._rpcs.pop)
 
index 76577ea..9cbbf13 100755 (executable)
@@ -26,9 +26,13 @@ import os
 import unittest
 import time
 import tempfile
+import pycurl
+import itertools
+import threading
 from cStringIO import StringIO
 
 from ganeti import http
+from ganeti import compat
 
 import ganeti.http.server
 import ganeti.http.client
@@ -330,6 +334,14 @@ class TestClientRequest(unittest.TestCase):
     self.assertEqual(cr.headers, [])
     self.assertEqual(cr.url, "https://localhost:1234/version")
 
+  def testPlainAddressIPv4(self):
+    cr = http.client.HttpClientRequest("192.0.2.9", 19956, "GET", "/version")
+    self.assertEqual(cr.url, "https://192.0.2.9:19956/version")
+
+  def testPlainAddressIPv6(self):
+    cr = http.client.HttpClientRequest("2001:db8::cafe", 15110, "GET", "/info")
+    self.assertEqual(cr.url, "https://[2001:db8::cafe]:15110/info")
+
   def testOldStyleHeaders(self):
     headers = {
       "Content-type": "text/plain",
@@ -365,27 +377,374 @@ class TestClientRequest(unittest.TestCase):
     cr = http.client.HttpClientRequest("localhost", 1234, "GET", "/version")
     self.assertEqual(cr.post_data, "")
 
-  def testIdentity(self):
-    # These should all use different connections, hence also have a different
-    # identity
-    cr1 = http.client.HttpClientRequest("localhost", 1234, "GET", "/version")
-    cr2 = http.client.HttpClientRequest("localhost", 9999, "GET", "/version")
-    cr3 = http.client.HttpClientRequest("node1", 1234, "GET", "/version")
-    cr4 = http.client.HttpClientRequest("node1", 9999, "GET", "/version")
+  def testCompletionCallback(self):
+    for argname in ["completion_cb", "curl_config_fn"]:
+      kwargs = {
+        argname: NotImplementedError,
+        }
+      cr = http.client.HttpClientRequest("localhost", 14038, "GET", "/version",
+                                         **kwargs)
+      self.assertEqual(getattr(cr, argname), NotImplementedError)
+
+      for fn in [NotImplemented, {}, 1]:
+        kwargs = {
+          argname: fn,
+          }
+        self.assertRaises(AssertionError, http.client.HttpClientRequest,
+                          "localhost", 23150, "GET", "/version", **kwargs)
+
+
+class _FakeCurl:
+  def __init__(self):
+    self.opts = {}
+    self.info = NotImplemented
+
+  def setopt(self, opt, value):
+    assert opt not in self.opts, "Option set more than once"
+    self.opts[opt] = value
+
+  def getinfo(self, info):
+    return self.info.pop(info)
+
+
+class TestClientStartRequest(unittest.TestCase):
+  @staticmethod
+  def _TestCurlConfig(curl):
+    curl.setopt(pycurl.SSLKEYTYPE, "PEM")
+
+  def test(self):
+    for method in [http.HTTP_GET, http.HTTP_PUT, "CUSTOM"]:
+      for port in [8761, 29796, 19528]:
+        for curl_config_fn in [None, self._TestCurlConfig]:
+          for read_timeout in [None, 0, 1, 123, 36000]:
+            self._TestInner(method, port, curl_config_fn, read_timeout)
+
+  def _TestInner(self, method, port, curl_config_fn, read_timeout):
+    for response_code in [http.HTTP_OK, http.HttpNotFound.code,
+                          http.HTTP_NOT_MODIFIED]:
+      for response_body in [None, "Hello World",
+                            "Very Long\tContent here\n" * 171]:
+        for errmsg in [None, "error"]:
+          req = http.client.HttpClientRequest("localhost", port, method,
+                                              "/version",
+                                              curl_config_fn=curl_config_fn,
+                                              read_timeout=read_timeout)
+          curl = _FakeCurl()
+          pending = http.client._StartRequest(curl, req)
+          self.assertEqual(pending.GetCurlHandle(), curl)
+          self.assertEqual(pending.GetCurrentRequest(), req)
+
+          # Check options
+          opts = curl.opts
+          self.assertEqual(opts.pop(pycurl.CUSTOMREQUEST), method)
+          self.assertEqual(opts.pop(pycurl.URL),
+                           "https://localhost:%s/version" % port)
+          if read_timeout is None:
+            self.assertEqual(opts.pop(pycurl.TIMEOUT), 0)
+          else:
+            self.assertEqual(opts.pop(pycurl.TIMEOUT), read_timeout)
+          self.assertFalse(opts.pop(pycurl.VERBOSE))
+          self.assertTrue(opts.pop(pycurl.NOSIGNAL))
+          self.assertEqual(opts.pop(pycurl.USERAGENT),
+                           http.HTTP_GANETI_VERSION)
+          self.assertEqual(opts.pop(pycurl.PROXY), "")
+          self.assertFalse(opts.pop(pycurl.POSTFIELDS))
+          self.assertFalse(opts.pop(pycurl.HTTPHEADER))
+          write_fn = opts.pop(pycurl.WRITEFUNCTION)
+          self.assertTrue(callable(write_fn))
+          if hasattr(pycurl, "SSL_SESSIONID_CACHE"):
+            self.assertFalse(opts.pop(pycurl.SSL_SESSIONID_CACHE))
+          if curl_config_fn:
+            self.assertEqual(opts.pop(pycurl.SSLKEYTYPE), "PEM")
+          else:
+            self.assertFalse(pycurl.SSLKEYTYPE in opts)
+          self.assertFalse(opts)
+
+          if response_body is not None:
+            offset = 0
+            while offset < len(response_body):
+              piece = response_body[offset:offset + 10]
+              write_fn(piece)
+              offset += len(piece)
+
+          curl.info = {
+            pycurl.RESPONSE_CODE: response_code,
+            }
+
+          # Finalize request
+          pending.Done(errmsg)
+
+          self.assertFalse(curl.info)
+
+          # Can only finalize once
+          self.assertRaises(AssertionError, pending.Done, True)
+
+          if errmsg:
+            self.assertFalse(req.success)
+          else:
+            self.assertTrue(req.success)
+          self.assertEqual(req.error, errmsg)
+          self.assertEqual(req.resp_status_code, response_code)
+          if response_body is None:
+            self.assertEqual(req.resp_body, "")
+          else:
+            self.assertEqual(req.resp_body, response_body)
+
+          # Check if resetting worked
+          assert not hasattr(curl, "reset")
+          opts = curl.opts
+          self.assertFalse(opts.pop(pycurl.POSTFIELDS))
+          self.assertTrue(callable(opts.pop(pycurl.WRITEFUNCTION)))
+          self.assertFalse(opts)
+
+          self.assertFalse(curl.opts,
+                           msg="Previous checks did not consume all options")
+          assert id(opts) == id(curl.opts)
+
+  def _TestWrongTypes(self, *args, **kwargs):
+    req = http.client.HttpClientRequest(*args, **kwargs)
+    self.assertRaises(AssertionError, http.client._StartRequest,
+                      _FakeCurl(), req)
+
+  def testWrongHostType(self):
+    self._TestWrongTypes(unicode("localhost"), 8080, "GET", "/version")
+
+  def testWrongUrlType(self):
+    self._TestWrongTypes("localhost", 8080, "GET", unicode("/version"))
+
+  def testWrongMethodType(self):
+    self._TestWrongTypes("localhost", 8080, unicode("GET"), "/version")
+
+  def testWrongHeaderType(self):
+    self._TestWrongTypes("localhost", 8080, "GET", "/version",
+                         headers={
+                           unicode("foo"): "bar",
+                           })
+
+  def testWrongPostDataType(self):
+    self._TestWrongTypes("localhost", 8080, "GET", "/version",
+                         post_data=unicode("verylongdata" * 100))
+
+
+class _EmptyCurlMulti:
+  def perform(self):
+    return (pycurl.E_MULTI_OK, 0)
 
-    self.assertEqual(len(set([cr1.identity, cr2.identity,
-                              cr3.identity, cr4.identity])), 4)
+  def info_read(self):
+    return (0, [], [])
 
-    # But this one should have the same
-    cr1vglist = http.client.HttpClientRequest("localhost", 1234,
-                                              "GET", "/vg_list")
-    self.assertEqual(cr1.identity, cr1vglist.identity)
 
+class TestClientProcessRequests(unittest.TestCase):
+  def testEmpty(self):
+    requests = []
+    http.client.ProcessRequests(requests, _curl=NotImplemented,
+                                _curl_multi=_EmptyCurlMulti)
+    self.assertEqual(requests, [])
+
+
+class TestProcessCurlRequests(unittest.TestCase):
+  class _FakeCurlMulti:
+    def __init__(self):
+      self.handles = []
+      self.will_fail = []
+      self._expect = ["perform"]
+      self._counter = itertools.count()
+
+    def add_handle(self, curl):
+      assert curl not in self.handles
+      self.handles.append(curl)
+      if self._counter.next() % 3 == 0:
+        self.will_fail.append(curl)
+
+    def remove_handle(self, curl):
+      self.handles.remove(curl)
+
+    def perform(self):
+      assert self._expect.pop(0) == "perform"
+
+      if self._counter.next() % 2 == 0:
+        self._expect.append("perform")
+        return (pycurl.E_CALL_MULTI_PERFORM, None)
+
+      self._expect.append("info_read")
+
+      return (pycurl.E_MULTI_OK, len(self.handles))
+
+    def info_read(self):
+      assert self._expect.pop(0) == "info_read"
+      successful = []
+      failed = []
+      if self.handles:
+        if self._counter.next() % 17 == 0:
+          curl = self.handles[0]
+          if curl in self.will_fail:
+            failed.append((curl, -1, "test error"))
+          else:
+            successful.append(curl)
+        remaining_messages = len(self.handles) % 3
+        if remaining_messages > 0:
+          self._expect.append("info_read")
+        else:
+          self._expect.append("select")
+      else:
+        remaining_messages = 0
+        self._expect.append("select")
+      return (remaining_messages, successful, failed)
+
+    def select(self, timeout):
+      # Never compare floats for equality
+      assert timeout >= 0.95 and timeout <= 1.05
+      assert self._expect.pop(0) == "select"
+      self._expect.append("perform")
 
-class TestClient(unittest.TestCase):
   def test(self):
-    pool = http.client.HttpClientPool(None)
-    self.assertFalse(pool._pool)
+    requests = [_FakeCurl() for _ in range(10)]
+    multi = self._FakeCurlMulti()
+    for (curl, errmsg) in http.client._ProcessCurlRequests(multi, requests):
+      self.assertTrue(curl not in multi.handles)
+      if curl in multi.will_fail:
+        self.assertTrue("test error" in errmsg)
+      else:
+        self.assertTrue(errmsg is None)
+    self.assertFalse(multi.handles)
+    self.assertEqual(multi._expect, ["select"])
+
+
+class TestProcessRequests(unittest.TestCase):
+  class _DummyCurlMulti:
+    pass
+
+  def testNoMonitor(self):
+    self._Test(False)
+
+  def testWithMonitor(self):
+    self._Test(True)
+
+  class _MonitorChecker:
+    def __init__(self):
+      self._monitor = None
+
+    def GetMonitor(self):
+      return self._monitor
+
+    def __call__(self, monitor):
+      assert callable(monitor.GetLockInfo)
+      self._monitor = monitor
+
+  def _Test(self, use_monitor):
+    def cfg_fn(port, curl):
+      curl.opts["__port__"] = port
+
+    def _LockCheckReset(monitor, req):
+      self.assertTrue(monitor._lock.is_owned(shared=0),
+                      msg="Lock must be owned in exclusive mode")
+      assert not hasattr(req, "lockcheck__")
+      setattr(req, "lockcheck__", True)
+
+    def _BuildNiceName(port, default=None):
+      if port % 5 == 0:
+        return "nicename%s" % port
+      else:
+        # Use standard name
+        return default
+
+    requests = \
+      [http.client.HttpClientRequest("localhost", i, "POST", "/version%s" % i,
+                                     curl_config_fn=compat.partial(cfg_fn, i),
+                                     completion_cb=NotImplementedError,
+                                     nicename=_BuildNiceName(i))
+       for i in range(15176, 15501)]
+    requests_count = len(requests)
+
+    if use_monitor:
+      lock_monitor_cb = self._MonitorChecker()
+    else:
+      lock_monitor_cb = None
+
+    def _ProcessRequests(multi, handles):
+      self.assertTrue(isinstance(multi, self._DummyCurlMulti))
+      self.assertEqual(len(requests), len(handles))
+      self.assertTrue(compat.all(isinstance(curl, _FakeCurl)
+                                 for curl in handles))
+
+      # Prepare for lock check
+      for req in requests:
+        assert req.completion_cb is NotImplementedError
+        if use_monitor:
+          req.completion_cb = \
+            compat.partial(_LockCheckReset, lock_monitor_cb.GetMonitor())
+
+      for idx, curl in enumerate(handles):
+        try:
+          port = curl.opts["__port__"]
+        except KeyError:
+          self.fail("Per-request config function was not called")
+
+        if use_monitor:
+          # Check if lock information is correct
+          lock_info = lock_monitor_cb.GetMonitor().GetLockInfo(None)
+          expected = \
+            [("rpc/%s" % (_BuildNiceName(handle.opts["__port__"],
+                                         default=("localhost/version%s" %
+                                                  handle.opts["__port__"]))),
+              None,
+              [threading.currentThread().getName()], None)
+             for handle in handles[idx:]]
+          self.assertEqual(sorted(lock_info), sorted(expected))
+
+        if port % 3 == 0:
+          response_code = http.HTTP_OK
+          msg = None
+        else:
+          response_code = http.HttpNotFound.code
+          msg = "test error"
+
+        curl.info = {
+          pycurl.RESPONSE_CODE: response_code,
+          }
+
+        # Prepare for reset
+        self.assertFalse(curl.opts.pop(pycurl.POSTFIELDS))
+        self.assertTrue(callable(curl.opts.pop(pycurl.WRITEFUNCTION)))
+
+        yield (curl, msg)
+
+      if use_monitor:
+        self.assertTrue(compat.all(req.lockcheck__ for req in requests))
+
+    if use_monitor:
+      self.assertEqual(lock_monitor_cb.GetMonitor(), None)
+
+    http.client.ProcessRequests(requests, lock_monitor_cb=lock_monitor_cb,
+                                _curl=_FakeCurl,
+                                _curl_multi=self._DummyCurlMulti,
+                                _curl_process=_ProcessRequests)
+    for req in requests:
+      if req.port % 3 == 0:
+        self.assertTrue(req.success)
+        self.assertEqual(req.error, None)
+      else:
+        self.assertFalse(req.success)
+        self.assertTrue("test error" in req.error)
+
+    # See if monitor was disabled
+    if use_monitor:
+      monitor = lock_monitor_cb.GetMonitor()
+      self.assertEqual(monitor._pending_fn, None)
+      self.assertEqual(monitor.GetLockInfo(None), [])
+    else:
+      self.assertEqual(lock_monitor_cb, None)
+
+    self.assertEqual(len(requests), requests_count)
+
+  def testBadRequest(self):
+    bad_request = http.client.HttpClientRequest("localhost", 27784,
+                                                "POST", "/version")
+    bad_request.success = False
+
+    self.assertRaises(AssertionError, http.client.ProcessRequests,
+                      [bad_request], _curl=NotImplemented,
+                      _curl_multi=NotImplemented, _curl_process=NotImplemented)
 
 
 if __name__ == '__main__':
index 20c8489..2c12e46 100755 (executable)
 
 """Script for testing the hypervisor.hv_kvm module"""
 
+import threading
+import tempfile
 import unittest
+import socket
+import os
 
+from ganeti import serializer
 from ganeti import constants
 from ganeti import compat
 from ganeti import objects
@@ -34,6 +39,149 @@ from ganeti.hypervisor import hv_kvm
 import testutils
 
 
+class QmpStub(threading.Thread):
+  """Stub for a QMP endpoint for a KVM instance
+
+  """
+  _QMP_BANNER_DATA = {
+    "QMP": {
+      "version": {
+        "package": "",
+        "qemu": {
+          "micro": 50,
+          "minor": 13,
+          "major": 0,
+          },
+        "capabilities": [],
+        },
+      }
+    }
+  _EMPTY_RESPONSE = {
+    "return": [],
+    }
+
+  def __init__(self, socket_filename, server_responses):
+    """Creates a QMP stub
+
+    @type socket_filename: string
+    @param socket_filename: filename of the UNIX socket that will be created
+                            this class and used for the communication
+    @type server_responses: list
+    @param server_responses: list of responses that the server sends in response
+                             to whatever it receives
+    """
+    threading.Thread.__init__(self)
+    self.socket_filename = socket_filename
+    self.script = server_responses
+
+    self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+    self.socket.bind(self.socket_filename)
+    self.socket.listen(1)
+
+  def run(self):
+    # Hypothesis: the messages we receive contain only a complete QMP message
+    # encoded in JSON.
+    conn, addr = self.socket.accept()
+
+    # Send the banner as the first thing
+    conn.send(self.encode_string(self._QMP_BANNER_DATA))
+
+    # Expect qmp_capabilities and return an empty response
+    conn.recv(4096)
+    conn.send(self.encode_string(self._EMPTY_RESPONSE))
+
+    while True:
+      # We ignore the expected message, as the purpose of this object is not
+      # to verify the correctness of the communication but to act as a
+      # partner for the SUT (System Under Test, that is QmpConnection)
+      msg = conn.recv(4096)
+      if not msg:
+        break
+
+      if not self.script:
+        break
+      response = self.script.pop(0)
+      if isinstance(response, str):
+        conn.send(response)
+      elif isinstance(response, list):
+        for chunk in response:
+          conn.send(chunk)
+      else:
+        raise errors.ProgrammerError("Unknown response type for %s" % response)
+
+    conn.close()
+
+  def encode_string(self, message):
+    return (serializer.DumpJson(message) +
+            hv_kvm.QmpConnection._MESSAGE_END_TOKEN)
+
+
+class TestQmpMessage(testutils.GanetiTestCase):
+  def testSerialization(self):
+    test_data = {
+      "execute": "command",
+      "arguments": ["a", "b", "c"],
+      }
+    message = hv_kvm.QmpMessage(test_data)
+
+    for k, v in test_data.items():
+      self.assertEqual(message[k], v)
+
+    serialized = str(message)
+    self.assertEqual(len(serialized.splitlines()), 1,
+                     msg="Got multi-line message")
+
+    rebuilt_message = hv_kvm.QmpMessage.BuildFromJsonString(serialized)
+    self.assertEqual(rebuilt_message, message)
+
+
+class TestQmp(testutils.GanetiTestCase):
+  def testQmp(self):
+    requests = [
+      {"execute": "query-kvm", "arguments": []},
+      {"execute": "eject", "arguments": {"device": "ide1-cd0"}},
+      {"execute": "query-status", "arguments": []},
+      {"execute": "query-name", "arguments": []},
+      ]
+
+    server_responses = [
+      # One message, one send()
+      '{"return": {"enabled": true, "present": true}}\r\n',
+
+      # Message sent using multiple send()
+      ['{"retur', 'n": {}}\r\n'],
+
+      # Multiple messages sent using one send()
+      '{"return": [{"name": "quit"}, {"name": "eject"}]}\r\n'
+      '{"return": {"running": true, "singlestep": false}}\r\n',
+      ]
+
+    expected_responses = [
+      {"return": {"enabled": True, "present": True}},
+      {"return": {}},
+      {"return": [{"name": "quit"}, {"name": "eject"}]},
+      {"return": {"running": True, "singlestep": False}},
+      ]
+
+    # Set up the stub
+    socket_file = tempfile.NamedTemporaryFile()
+    os.remove(socket_file.name)
+    qmp_stub = QmpStub(socket_file.name, server_responses)
+    qmp_stub.start()
+
+    # Set up the QMP connection
+    qmp_connection = hv_kvm.QmpConnection(socket_file.name)
+    qmp_connection.connect()
+
+    # Format the script
+    for request, expected_response in zip(requests, expected_responses):
+      response = qmp_connection.Execute(request)
+      msg = hv_kvm.QmpMessage(expected_response)
+      self.assertEqual(len(str(msg).splitlines()), 1,
+                       msg="Got multi-line message")
+      self.assertEqual(response, msg)
+
+
 class TestConsole(unittest.TestCase):
   def _Test(self, instance, hvparams):
     cons = hv_kvm.KVMHypervisor.GetInstanceConsole(instance, hvparams, {})
@@ -46,6 +194,7 @@ class TestConsole(unittest.TestCase):
     hvparams = {
       constants.HV_SERIAL_CONSOLE: True,
       constants.HV_VNC_BIND_ADDRESS: None,
+      constants.HV_KVM_SPICE_BIND: None,
       }
     cons = self._Test(instance, hvparams)
     self.assertEqual(cons.kind, constants.CONS_SSH)
@@ -60,6 +209,7 @@ class TestConsole(unittest.TestCase):
     hvparams = {
       constants.HV_SERIAL_CONSOLE: False,
       constants.HV_VNC_BIND_ADDRESS: "192.0.2.1",
+      constants.HV_KVM_SPICE_BIND: None,
       }
     cons = self._Test(instance, hvparams)
     self.assertEqual(cons.kind, constants.CONS_VNC)
@@ -67,6 +217,20 @@ class TestConsole(unittest.TestCase):
     self.assertEqual(cons.port, constants.VNC_BASE_PORT + 10)
     self.assertEqual(cons.display, 10)
 
+  def testSpice(self):
+    instance = objects.Instance(name="kvm.example.com",
+                                primary_node="node7235",
+                                network_port=11000)
+    hvparams = {
+      constants.HV_SERIAL_CONSOLE: False,
+      constants.HV_VNC_BIND_ADDRESS: None,
+      constants.HV_KVM_SPICE_BIND: "192.0.2.1",
+      }
+    cons = self._Test(instance, hvparams)
+    self.assertEqual(cons.kind, constants.CONS_SPICE)
+    self.assertEqual(cons.host, "192.0.2.1")
+    self.assertEqual(cons.port, 11000)
+
   def testNoConsole(self):
     instance = objects.Instance(name="kvm.example.com",
                                 primary_node="node24325",
@@ -74,6 +238,7 @@ class TestConsole(unittest.TestCase):
     hvparams = {
       constants.HV_SERIAL_CONSOLE: False,
       constants.HV_VNC_BIND_ADDRESS: None,
+      constants.HV_KVM_SPICE_BIND: None,
       }
     cons = self._Test(instance, hvparams)
     self.assertEqual(cons.kind, constants.CONS_MESSAGE)
index f2ffc8a..dcbad14 100755 (executable)
@@ -38,6 +38,7 @@ from ganeti import opcodes
 from ganeti import compat
 from ganeti import mcpu
 from ganeti import query
+from ganeti import workerpool
 
 import testutils
 
@@ -304,7 +305,7 @@ class TestQueuedJob(unittest.TestCase):
       self.assertEqual(len(job.ops), len(ops))
       self.assert_(compat.all(inp.__getstate__() == op.input.__getstate__()
                               for (inp, op) in zip(ops, job.ops)))
-      self.assertRaises(errors.OpExecError, job.GetInfo,
+      self.assertRaises(errors.OpPrereqError, job.GetInfo,
                         ["unknown-field"])
       self.assertEqual(job.GetInfo(["summary"]),
                        [[op.input.Summary() for op in job.ops]])
@@ -673,7 +674,7 @@ class TestJobProcessor(unittest.TestCase, _JobProcessorTestUtils):
              for i in range(opcount)]
 
       # Create job
-      job = self._CreateJob(queue, job_id, ops)
+      job = self._CreateJob(queue, str(job_id), ops)
 
       opexec = _FakeExecOpCodeForProc(queue, None, None)
 
@@ -701,7 +702,7 @@ class TestJobProcessor(unittest.TestCase, _JobProcessorTestUtils):
 
       # Check job status
       self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_ERROR)
-      self.assertEqual(job.GetInfo(["id"]), [job_id])
+      self.assertEqual(job.GetInfo(["id"]), [str(job_id)])
       self.assertEqual(job.GetInfo(["status"]), [constants.JOB_STATUS_ERROR])
 
       # Check opcode status
@@ -925,7 +926,7 @@ class TestJobProcessor(unittest.TestCase, _JobProcessorTestUtils):
            for i in range(3)]
 
     # Create job
-    job_id = 28492
+    job_id = str(28492)
     job = self._CreateJob(queue, job_id, ops)
 
     self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_QUEUED)
@@ -1625,6 +1626,43 @@ class TestJobProcessor(unittest.TestCase, _JobProcessorTestUtils):
     self.assertRaises(IndexError, queue.GetNextUpdate)
 
 
+class TestEvaluateJobProcessorResult(unittest.TestCase):
+  def testFinished(self):
+    depmgr = _FakeDependencyManager()
+    job = _IdOnlyFakeJob(30953)
+    jqueue._EvaluateJobProcessorResult(depmgr, job,
+                                       jqueue._JobProcessor.FINISHED)
+    self.assertEqual(depmgr.GetNextNotification(), job.id)
+    self.assertRaises(IndexError, depmgr.GetNextNotification)
+
+  def testDefer(self):
+    depmgr = _FakeDependencyManager()
+    job = _IdOnlyFakeJob(11326, priority=5463)
+    try:
+      jqueue._EvaluateJobProcessorResult(depmgr, job,
+                                         jqueue._JobProcessor.DEFER)
+    except workerpool.DeferTask, err:
+      self.assertEqual(err.priority, 5463)
+    else:
+      self.fail("Didn't raise exception")
+    self.assertRaises(IndexError, depmgr.GetNextNotification)
+
+  def testWaitdep(self):
+    depmgr = _FakeDependencyManager()
+    job = _IdOnlyFakeJob(21317)
+    jqueue._EvaluateJobProcessorResult(depmgr, job,
+                                       jqueue._JobProcessor.WAITDEP)
+    self.assertRaises(IndexError, depmgr.GetNextNotification)
+
+  def testOther(self):
+    depmgr = _FakeDependencyManager()
+    job = _IdOnlyFakeJob(5813)
+    self.assertRaises(errors.ProgrammerError,
+                      jqueue._EvaluateJobProcessorResult,
+                      depmgr, job, "Other result")
+    self.assertRaises(IndexError, depmgr.GetNextNotification)
+
+
 class _FakeTimeoutStrategy:
   def __init__(self, timeouts):
     self.timeouts = timeouts
@@ -1858,11 +1896,16 @@ class TestJobProcessorTimeouts(unittest.TestCase, _JobProcessorTestUtils):
     self.assertRaises(IndexError, self.queue.GetNextUpdate)
 
 
-class TestJobDependencyManager(unittest.TestCase):
-  class _FakeJob:
-    def __init__(self, job_id):
-      self.id = str(job_id)
+class _IdOnlyFakeJob:
+  def __init__(self, job_id, priority=NotImplemented):
+    self.id = str(job_id)
+    self._priority = priority
+
+  def CalcPriority(self):
+    return self._priority
 
+
+class TestJobDependencyManager(unittest.TestCase):
   def setUp(self):
     self._status = []
     self._queue = []
@@ -1880,7 +1923,7 @@ class TestJobDependencyManager(unittest.TestCase):
     self._queue.append(jobs)
 
   def testNotFinalizedThenCancel(self):
-    job = self._FakeJob(17697)
+    job = _IdOnlyFakeJob(17697)
     job_id = str(28625)
 
     self._status.append((job_id, constants.JOB_STATUS_RUNNING))
@@ -1905,7 +1948,7 @@ class TestJobDependencyManager(unittest.TestCase):
     self.assertFalse(self.jdm.GetLockInfo([query.LQ_PENDING]))
 
   def testRequireCancel(self):
-    job = self._FakeJob(5278)
+    job = _IdOnlyFakeJob(5278)
     job_id = str(9610)
     dep_status = [constants.JOB_STATUS_CANCELED]
 
@@ -1931,7 +1974,7 @@ class TestJobDependencyManager(unittest.TestCase):
     self.assertFalse(self.jdm.GetLockInfo([query.LQ_PENDING]))
 
   def testRequireError(self):
-    job = self._FakeJob(21459)
+    job = _IdOnlyFakeJob(21459)
     job_id = str(25519)
     dep_status = [constants.JOB_STATUS_ERROR]
 
@@ -1957,7 +2000,7 @@ class TestJobDependencyManager(unittest.TestCase):
     dep_status = list(constants.JOBS_FINALIZED)
 
     for end_status in dep_status:
-      job = self._FakeJob(21343)
+      job = _IdOnlyFakeJob(21343)
       job_id = str(14609)
 
       self._status.append((job_id, constants.JOB_STATUS_WAITING))
@@ -1982,7 +2025,7 @@ class TestJobDependencyManager(unittest.TestCase):
       self.assertFalse(self.jdm.GetLockInfo([query.LQ_PENDING]))
 
   def testNotify(self):
-    job = self._FakeJob(8227)
+    job = _IdOnlyFakeJob(8227)
     job_id = str(4113)
 
     self._status.append((job_id, constants.JOB_STATUS_RUNNING))
@@ -2002,7 +2045,7 @@ class TestJobDependencyManager(unittest.TestCase):
     self.assertEqual(self._queue, [set([job])])
 
   def testWrongStatus(self):
-    job = self._FakeJob(10102)
+    job = _IdOnlyFakeJob(10102)
     job_id = str(1271)
 
     self._status.append((job_id, constants.JOB_STATUS_QUEUED))
@@ -2025,7 +2068,7 @@ class TestJobDependencyManager(unittest.TestCase):
     self.assertFalse(self.jdm.JobWaiting(job))
 
   def testCorrectStatus(self):
-    job = self._FakeJob(24273)
+    job = _IdOnlyFakeJob(24273)
     job_id = str(23885)
 
     self._status.append((job_id, constants.JOB_STATUS_QUEUED))
@@ -2048,7 +2091,7 @@ class TestJobDependencyManager(unittest.TestCase):
     self.assertFalse(self.jdm.JobWaiting(job))
 
   def testFinalizedRightAway(self):
-    job = self._FakeJob(224)
+    job = _IdOnlyFakeJob(224)
     job_id = str(3081)
 
     self._status.append((job_id, constants.JOB_STATUS_SUCCESS))
@@ -2075,7 +2118,7 @@ class TestJobDependencyManager(unittest.TestCase):
     job_ids = map(str, rnd.sample(range(1, 10000), 150))
 
     waiters = dict((job_ids.pop(),
-                    set(map(self._FakeJob,
+                    set(map(_IdOnlyFakeJob,
                             [job_ids.pop()
                              for _ in range(rnd.randint(1, 20))])))
                    for _ in range(10))
@@ -2135,14 +2178,14 @@ class TestJobDependencyManager(unittest.TestCase):
     assert not waiters
 
   def testSelfDependency(self):
-    job = self._FakeJob(18937)
+    job = _IdOnlyFakeJob(18937)
 
     self._status.append((job.id, constants.JOB_STATUS_SUCCESS))
     (result, _) = self.jdm.CheckAndRegister(job, job.id, [])
     self.assertEqual(result, self.jdm.ERROR)
 
   def testJobDisappears(self):
-    job = self._FakeJob(30540)
+    job = _IdOnlyFakeJob(30540)
     job_id = str(23769)
 
     def _FakeStatus(_):
index fe19e47..2706a72 100755 (executable)
@@ -16,7 +16,7 @@
 # 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
-# 0.0510-1301, USA.
+# 02110-1301, USA.
 
 
 """Script for unittesting the locking module"""
@@ -269,25 +269,25 @@ class TestSharedLock(_ThreadedTestCase):
     self.sl = locking.SharedLock("TestSharedLock")
 
   def testSequenceAndOwnership(self):
-    self.assertFalse(self.sl._is_owned())
+    self.assertFalse(self.sl.is_owned())
     self.sl.acquire(shared=1)
-    self.assert_(self.sl._is_owned())
-    self.assert_(self.sl._is_owned(shared=1))
-    self.assertFalse(self.sl._is_owned(shared=0))
+    self.assert_(self.sl.is_owned())
+    self.assert_(self.sl.is_owned(shared=1))
+    self.assertFalse(self.sl.is_owned(shared=0))
     self.sl.release()
-    self.assertFalse(self.sl._is_owned())
+    self.assertFalse(self.sl.is_owned())
     self.sl.acquire()
-    self.assert_(self.sl._is_owned())
-    self.assertFalse(self.sl._is_owned(shared=1))
-    self.assert_(self.sl._is_owned(shared=0))
+    self.assert_(self.sl.is_owned())
+    self.assertFalse(self.sl.is_owned(shared=1))
+    self.assert_(self.sl.is_owned(shared=0))
     self.sl.release()
-    self.assertFalse(self.sl._is_owned())
+    self.assertFalse(self.sl.is_owned())
     self.sl.acquire(shared=1)
-    self.assert_(self.sl._is_owned())
-    self.assert_(self.sl._is_owned(shared=1))
-    self.assertFalse(self.sl._is_owned(shared=0))
+    self.assert_(self.sl.is_owned())
+    self.assert_(self.sl.is_owned(shared=1))
+    self.assertFalse(self.sl.is_owned(shared=0))
     self.sl.release()
-    self.assertFalse(self.sl._is_owned())
+    self.assertFalse(self.sl.is_owned())
 
   def testBooleanValue(self):
     # semaphores are supposed to return a true value on a successful acquire
@@ -433,7 +433,31 @@ class TestSharedLock(_ThreadedTestCase):
     self.assertRaises(errors.LockError, self.sl.delete)
 
   def testDeleteTimeout(self):
-    self.sl.delete(timeout=60)
+    self.assertTrue(self.sl.delete(timeout=60))
+
+  def testDeleteTimeoutFail(self):
+    ready = threading.Event()
+    finish = threading.Event()
+
+    def fn():
+      self.sl.acquire(shared=0)
+      ready.set()
+
+      finish.wait()
+      self.sl.release()
+
+    self._addThread(target=fn)
+    ready.wait()
+
+    # Test if deleting a lock owned in exclusive mode by another thread fails
+    # to delete when a timeout is used
+    self.assertFalse(self.sl.delete(timeout=0.02))
+
+    finish.set()
+    self._waitThreads()
+
+    self.assertTrue(self.sl.delete())
+    self.assertRaises(errors.LockError, self.sl.acquire)
 
   def testNoDeleteIfSharer(self):
     self.sl.acquire(shared=1)
@@ -618,45 +642,45 @@ class TestSharedLock(_ThreadedTestCase):
 
     # Acquire in shared mode, downgrade should be no-op
     self.assertTrue(self.sl.acquire(shared=1))
-    self.assertTrue(self.sl._is_owned(shared=1))
+    self.assertTrue(self.sl.is_owned(shared=1))
     self.assertTrue(self.sl.downgrade())
-    self.assertTrue(self.sl._is_owned(shared=1))
+    self.assertTrue(self.sl.is_owned(shared=1))
     self.sl.release()
 
   def testDowngrade(self):
     self.assertTrue(self.sl.acquire())
-    self.assertTrue(self.sl._is_owned(shared=0))
+    self.assertTrue(self.sl.is_owned(shared=0))
     self.assertTrue(self.sl.downgrade())
-    self.assertTrue(self.sl._is_owned(shared=1))
+    self.assertTrue(self.sl.is_owned(shared=1))
     self.sl.release()
 
   @_Repeat
   def testDowngradeJumpsAheadOfExclusive(self):
     def _KeepExclusive(ev_got, ev_downgrade, ev_release):
       self.assertTrue(self.sl.acquire())
-      self.assertTrue(self.sl._is_owned(shared=0))
+      self.assertTrue(self.sl.is_owned(shared=0))
       ev_got.set()
       ev_downgrade.wait()
-      self.assertTrue(self.sl._is_owned(shared=0))
+      self.assertTrue(self.sl.is_owned(shared=0))
       self.assertTrue(self.sl.downgrade())
-      self.assertTrue(self.sl._is_owned(shared=1))
+      self.assertTrue(self.sl.is_owned(shared=1))
       ev_release.wait()
-      self.assertTrue(self.sl._is_owned(shared=1))
+      self.assertTrue(self.sl.is_owned(shared=1))
       self.sl.release()
 
     def _KeepExclusive2(ev_started, ev_release):
       self.assertTrue(self.sl.acquire(test_notify=ev_started.set))
-      self.assertTrue(self.sl._is_owned(shared=0))
+      self.assertTrue(self.sl.is_owned(shared=0))
       ev_release.wait()
-      self.assertTrue(self.sl._is_owned(shared=0))
+      self.assertTrue(self.sl.is_owned(shared=0))
       self.sl.release()
 
     def _KeepShared(ev_started, ev_got, ev_release):
       self.assertTrue(self.sl.acquire(shared=1, test_notify=ev_started.set))
-      self.assertTrue(self.sl._is_owned(shared=1))
+      self.assertTrue(self.sl.is_owned(shared=1))
       ev_got.set()
       ev_release.wait()
-      self.assertTrue(self.sl._is_owned(shared=1))
+      self.assertTrue(self.sl.is_owned(shared=1))
       self.sl.release()
 
     # Acquire lock in exclusive mode
@@ -929,6 +953,84 @@ class TestSharedLock(_ThreadedTestCase):
                                        for i in sorted(perprio.keys())]
                       for (shared, _, threads) in acquires])
 
+  class _FakeTimeForSpuriousNotifications:
+    def __init__(self, now, check_end):
+      self.now = now
+      self.check_end = check_end
+
+      # Deterministic random number generator
+      self.rnd = random.Random(15086)
+
+    def time(self):
+      # Advance time if the random number generator thinks so (this is to test
+      # multiple notifications without advancing the time)
+      if self.rnd.random() < 0.3:
+        self.now += self.rnd.random()
+
+      self.check_end(self.now)
+
+      return self.now
+
+  @_Repeat
+  def testAcquireTimeoutWithSpuriousNotifications(self):
+    ready = threading.Event()
+    locked = threading.Event()
+    req = Queue.Queue(0)
+
+    epoch = 4000.0
+    timeout = 60.0
+
+    def check_end(now):
+      self.assertFalse(locked.isSet())
+
+      # If we waited long enough (in virtual time), tell main thread to release
+      # lock, otherwise tell it to notify once more
+      req.put(now < (epoch + (timeout * 0.8)))
+
+    time_fn = self._FakeTimeForSpuriousNotifications(epoch, check_end).time
+
+    sl = locking.SharedLock("test", _time_fn=time_fn)
+
+    # Acquire in exclusive mode
+    sl.acquire(shared=0)
+
+    def fn():
+      self.assertTrue(sl.acquire(shared=0, timeout=timeout,
+                                 test_notify=ready.set))
+      locked.set()
+      sl.release()
+      self.done.put("success")
+
+    # Start acquire with timeout and wait for it to be ready
+    self._addThread(target=fn)
+    ready.wait()
+
+    # The separate thread is now waiting to acquire the lock, so start sending
+    # spurious notifications.
+
+    # Wait for separate thread to ask for another notification
+    count = 0
+    while req.get():
+      # After sending the notification, the lock will take a short amount of
+      # time to notice and to retrieve the current time
+      sl._notify_topmost()
+      count += 1
+
+    self.assertTrue(count > 100, "Not enough notifications were sent")
+
+    self.assertFalse(locked.isSet())
+
+    # Some notifications have been sent, now actually release the lock
+    sl.release()
+
+    # Wait for lock to be acquired
+    locked.wait()
+
+    self._waitThreads()
+
+    self.assertEqual(self.done.get_nowait(), "success")
+    self.assertRaises(Queue.Empty, self.done.get_nowait)
+
 
 class TestSharedLockInCondition(_ThreadedTestCase):
   """SharedLock as a condition lock tests"""
@@ -943,14 +1045,14 @@ class TestSharedLockInCondition(_ThreadedTestCase):
 
   def testKeepMode(self):
     self.cond.acquire(shared=1)
-    self.assert_(self.sl._is_owned(shared=1))
+    self.assert_(self.sl.is_owned(shared=1))
     self.cond.wait(0)
-    self.assert_(self.sl._is_owned(shared=1))
+    self.assert_(self.sl.is_owned(shared=1))
     self.cond.release()
     self.cond.acquire(shared=0)
-    self.assert_(self.sl._is_owned(shared=0))
+    self.assert_(self.sl.is_owned(shared=0))
     self.cond.wait(0)
-    self.assert_(self.sl._is_owned(shared=0))
+    self.assert_(self.sl.is_owned(shared=0))
     self.cond.release()
 
 
@@ -969,19 +1071,19 @@ class TestSSynchronizedDecorator(_ThreadedTestCase):
 
   @locking.ssynchronized(_decoratorlock)
   def _doItExclusive(self):
-    self.assert_(_decoratorlock._is_owned())
+    self.assert_(_decoratorlock.is_owned())
     self.done.put('EXC')
 
   @locking.ssynchronized(_decoratorlock, shared=1)
   def _doItSharer(self):
-    self.assert_(_decoratorlock._is_owned(shared=1))
+    self.assert_(_decoratorlock.is_owned(shared=1))
     self.done.put('SHR')
 
   def testDecoratedFunctions(self):
     self._doItExclusive()
-    self.assertFalse(_decoratorlock._is_owned())
+    self.assertFalse(_decoratorlock.is_owned())
     self._doItSharer()
-    self.assertFalse(_decoratorlock._is_owned())
+    self.assertFalse(_decoratorlock.is_owned())
 
   def testSharersCanCoexist(self):
     _decoratorlock.acquire(shared=1)
@@ -1035,27 +1137,61 @@ class TestLockSet(_ThreadedTestCase):
     newls = locking.LockSet([], "TestLockSet.testResources")
     self.assertEquals(newls._names(), set())
 
+  def testCheckOwnedUnknown(self):
+    self.assertFalse(self.ls.check_owned("certainly-not-owning-this-one"))
+    for shared in [-1, 0, 1, 6378, 24255]:
+      self.assertFalse(self.ls.check_owned("certainly-not-owning-this-one",
+                                           shared=shared))
+
+  def testCheckOwnedUnknownWhileHolding(self):
+    self.assertFalse(self.ls.check_owned([]))
+    self.ls.acquire("one", shared=1)
+    self.assertRaises(errors.LockError, self.ls.check_owned, "nonexist")
+    self.assertTrue(self.ls.check_owned("one", shared=1))
+    self.assertFalse(self.ls.check_owned("one", shared=0))
+    self.assertFalse(self.ls.check_owned(["one", "two"]))
+    self.assertRaises(errors.LockError, self.ls.check_owned,
+                      ["one", "nonexist"])
+    self.assertRaises(errors.LockError, self.ls.check_owned, "")
+    self.ls.release()
+    self.assertFalse(self.ls.check_owned([]))
+    self.assertFalse(self.ls.check_owned("one"))
+
   def testAcquireRelease(self):
+    self.assertFalse(self.ls.check_owned(self.ls._names()))
     self.assert_(self.ls.acquire('one'))
-    self.assertEquals(self.ls._list_owned(), set(['one']))
+    self.assertEquals(self.ls.list_owned(), set(['one']))
+    self.assertTrue(self.ls.check_owned("one"))
+    self.assertTrue(self.ls.check_owned("one", shared=0))
+    self.assertFalse(self.ls.check_owned("one", shared=1))
     self.ls.release()
-    self.assertEquals(self.ls._list_owned(), set())
+    self.assertEquals(self.ls.list_owned(), set())
+    self.assertFalse(self.ls.check_owned(self.ls._names()))
     self.assertEquals(self.ls.acquire(['one']), set(['one']))
-    self.assertEquals(self.ls._list_owned(), set(['one']))
+    self.assertEquals(self.ls.list_owned(), set(['one']))
     self.ls.release()
-    self.assertEquals(self.ls._list_owned(), set())
+    self.assertEquals(self.ls.list_owned(), set())
     self.ls.acquire(['one', 'two', 'three'])
-    self.assertEquals(self.ls._list_owned(), set(['one', 'two', 'three']))
+    self.assertEquals(self.ls.list_owned(), set(['one', 'two', 'three']))
+    self.assertTrue(self.ls.check_owned(self.ls._names()))
+    self.assertTrue(self.ls.check_owned(self.ls._names(), shared=0))
+    self.assertFalse(self.ls.check_owned(self.ls._names(), shared=1))
     self.ls.release('one')
-    self.assertEquals(self.ls._list_owned(), set(['two', 'three']))
+    self.assertFalse(self.ls.check_owned(["one"]))
+    self.assertTrue(self.ls.check_owned(["two", "three"]))
+    self.assertTrue(self.ls.check_owned(["two", "three"], shared=0))
+    self.assertFalse(self.ls.check_owned(["two", "three"], shared=1))
+    self.assertEquals(self.ls.list_owned(), set(['two', 'three']))
     self.ls.release(['three'])
-    self.assertEquals(self.ls._list_owned(), set(['two']))
+    self.assertEquals(self.ls.list_owned(), set(['two']))
     self.ls.release()
-    self.assertEquals(self.ls._list_owned(), set())
+    self.assertEquals(self.ls.list_owned(), set())
     self.assertEquals(self.ls.acquire(['one', 'three']), set(['one', 'three']))
-    self.assertEquals(self.ls._list_owned(), set(['one', 'three']))
+    self.assertEquals(self.ls.list_owned(), set(['one', 'three']))
     self.ls.release()
-    self.assertEquals(self.ls._list_owned(), set())
+    self.assertEquals(self.ls.list_owned(), set())
+    for name in self.ls._names():
+      self.assertFalse(self.ls.check_owned(name))
 
   def testNoDoubleAcquire(self):
     self.ls.acquire('one')
@@ -1075,31 +1211,31 @@ class TestLockSet(_ThreadedTestCase):
 
   def testAddRemove(self):
     self.ls.add('four')
-    self.assertEquals(self.ls._list_owned(), set())
+    self.assertEquals(self.ls.list_owned(), set())
     self.assert_('four' in self.ls._names())
     self.ls.add(['five', 'six', 'seven'], acquired=1)
     self.assert_('five' in self.ls._names())
     self.assert_('six' in self.ls._names())
     self.assert_('seven' in self.ls._names())
-    self.assertEquals(self.ls._list_owned(), set(['five', 'six', 'seven']))
+    self.assertEquals(self.ls.list_owned(), set(['five', 'six', 'seven']))
     self.assertEquals(self.ls.remove(['five', 'six']), ['five', 'six'])
     self.assert_('five' not in self.ls._names())
     self.assert_('six' not in self.ls._names())
-    self.assertEquals(self.ls._list_owned(), set(['seven']))
+    self.assertEquals(self.ls.list_owned(), set(['seven']))
     self.assertRaises(AssertionError, self.ls.add, 'eight', acquired=1)
     self.ls.remove('seven')
     self.assert_('seven' not in self.ls._names())
-    self.assertEquals(self.ls._list_owned(), set([]))
+    self.assertEquals(self.ls.list_owned(), set([]))
     self.ls.acquire(None, shared=1)
     self.assertRaises(AssertionError, self.ls.add, 'eight')
     self.ls.release()
     self.ls.acquire(None)
     self.ls.add('eight', acquired=1)
     self.assert_('eight' in self.ls._names())
-    self.assert_('eight' in self.ls._list_owned())
+    self.assert_('eight' in self.ls.list_owned())
     self.ls.add('nine')
     self.assert_('nine' in self.ls._names())
-    self.assert_('nine' not in self.ls._list_owned())
+    self.assert_('nine' not in self.ls.list_owned())
     self.ls.release()
     self.ls.remove(['two'])
     self.assert_('two' not in self.ls._names())
@@ -1132,8 +1268,8 @@ class TestLockSet(_ThreadedTestCase):
   def testAcquireSetLock(self):
     # acquire the set-lock exclusively
     self.assertEquals(self.ls.acquire(None), set(['one', 'two', 'three']))
-    self.assertEquals(self.ls._list_owned(), set(['one', 'two', 'three']))
-    self.assertEquals(self.ls._is_owned(), True)
+    self.assertEquals(self.ls.list_owned(), set(['one', 'two', 'three']))
+    self.assertEquals(self.ls.is_owned(), True)
     self.assertEquals(self.ls._names(), set(['one', 'two', 'three']))
     # I can still add/remove elements...
     self.assertEquals(self.ls.remove(['two', 'three']), ['two', 'three'])
@@ -1149,17 +1285,17 @@ class TestLockSet(_ThreadedTestCase):
     self.assertEquals(self.ls.acquire(['two', 'two', 'three'], shared=1),
                       set(['two', 'two', 'three']))
     self.ls.release(['two', 'two'])
-    self.assertEquals(self.ls._list_owned(), set(['three']))
+    self.assertEquals(self.ls.list_owned(), set(['three']))
 
   def testEmptyAcquire(self):
     # Acquire an empty list of locks...
     self.assertEquals(self.ls.acquire([]), set())
-    self.assertEquals(self.ls._list_owned(), set())
+    self.assertEquals(self.ls.list_owned(), set())
     # New locks can still be addded
     self.assert_(self.ls.add('six'))
     # "re-acquiring" is not an issue, since we had really acquired nothing
     self.assertEquals(self.ls.acquire([], shared=1), set())
-    self.assertEquals(self.ls._list_owned(), set())
+    self.assertEquals(self.ls.list_owned(), set())
     # We haven't really acquired anything, so we cannot release
     self.assertRaises(AssertionError, self.ls.release)
 
@@ -1258,8 +1394,8 @@ class TestLockSet(_ThreadedTestCase):
           self.ls.release()
         else:
           self.assert_(acquired is None)
-          self.assertFalse(self.ls._list_owned())
-          self.assertFalse(self.ls._is_owned())
+          self.assertFalse(self.ls.list_owned())
+          self.assertFalse(self.ls.is_owned())
           self.done.put("not acquired")
 
       self._addThread(target=_AcquireOne)
@@ -1331,7 +1467,7 @@ class TestLockSet(_ThreadedTestCase):
 
         self.ls.release(names=name)
 
-      self.assertFalse(self.ls._list_owned())
+      self.assertFalse(self.ls.list_owned())
 
       self._waitThreads()
 
@@ -1446,9 +1582,9 @@ class TestLockSet(_ThreadedTestCase):
     self.ls.add('four')
     self.ls.add('five', acquired=1)
     self.ls.add('six', acquired=1, shared=1)
-    self.assertEquals(self.ls._list_owned(),
+    self.assertEquals(self.ls.list_owned(),
       set(['one', 'two', 'three', 'five', 'six']))
-    self.assertEquals(self.ls._is_owned(), True)
+    self.assertEquals(self.ls.is_owned(), True)
     self.assertEquals(self.ls._names(),
       set(['one', 'two', 'three', 'four', 'five', 'six']))
     self.ls.release()
@@ -1489,55 +1625,80 @@ class TestLockSet(_ThreadedTestCase):
 
   def testAcquireWithNamesDowngrade(self):
     self.assertEquals(self.ls.acquire("two", shared=0), set(["two"]))
-    self.assertTrue(self.ls._is_owned())
-    self.assertFalse(self.ls._get_lock()._is_owned())
+    self.assertTrue(self.ls.is_owned())
+    self.assertFalse(self.ls._get_lock().is_owned())
     self.ls.release()
-    self.assertFalse(self.ls._is_owned())
-    self.assertFalse(self.ls._get_lock()._is_owned())
+    self.assertFalse(self.ls.is_owned())
+    self.assertFalse(self.ls._get_lock().is_owned())
     # Can't downgrade after releasing
     self.assertRaises(AssertionError, self.ls.downgrade, "two")
 
   def testDowngrade(self):
     # Not owning anything, must raise an exception
-    self.assertFalse(self.ls._is_owned())
+    self.assertFalse(self.ls.is_owned())
     self.assertRaises(AssertionError, self.ls.downgrade)
 
-    self.assertFalse(compat.any(i._is_owned()
+    self.assertFalse(compat.any(i.is_owned()
                                 for i in self.ls._get_lockdict().values()))
+    self.assertFalse(self.ls.check_owned(self.ls._names()))
+    for name in self.ls._names():
+      self.assertFalse(self.ls.check_owned(name))
 
     self.assertEquals(self.ls.acquire(None, shared=0),
                       set(["one", "two", "three"]))
     self.assertRaises(AssertionError, self.ls.downgrade, "unknown lock")
 
-    self.assertTrue(self.ls._get_lock()._is_owned(shared=0))
-    self.assertTrue(compat.all(i._is_owned(shared=0)
+    self.assertTrue(self.ls.check_owned(self.ls._names(), shared=0))
+    for name in self.ls._names():
+      self.assertTrue(self.ls.check_owned(name))
+      self.assertTrue(self.ls.check_owned(name, shared=0))
+      self.assertFalse(self.ls.check_owned(name, shared=1))
+
+    self.assertTrue(self.ls._get_lock().is_owned(shared=0))
+    self.assertTrue(compat.all(i.is_owned(shared=0)
                                for i in self.ls._get_lockdict().values()))
 
     # Start downgrading locks
     self.assertTrue(self.ls.downgrade(names=["one"]))
-    self.assertTrue(self.ls._get_lock()._is_owned(shared=0))
-    self.assertTrue(compat.all(lock._is_owned(shared=[0, 1][int(name == "one")])
+    self.assertTrue(self.ls._get_lock().is_owned(shared=0))
+    self.assertTrue(compat.all(lock.is_owned(shared=[0, 1][int(name == "one")])
                                for name, lock in
                                  self.ls._get_lockdict().items()))
 
+    self.assertFalse(self.ls.check_owned("one", shared=0))
+    self.assertTrue(self.ls.check_owned("one", shared=1))
+    self.assertTrue(self.ls.check_owned("two", shared=0))
+    self.assertTrue(self.ls.check_owned("three", shared=0))
+
+    # Downgrade second lock
     self.assertTrue(self.ls.downgrade(names="two"))
-    self.assertTrue(self.ls._get_lock()._is_owned(shared=0))
+    self.assertTrue(self.ls._get_lock().is_owned(shared=0))
     should_share = lambda name: [0, 1][int(name in ("one", "two"))]
-    self.assertTrue(compat.all(lock._is_owned(shared=should_share(name))
+    self.assertTrue(compat.all(lock.is_owned(shared=should_share(name))
                                for name, lock in
                                  self.ls._get_lockdict().items()))
 
+    self.assertFalse(self.ls.check_owned("one", shared=0))
+    self.assertTrue(self.ls.check_owned("one", shared=1))
+    self.assertFalse(self.ls.check_owned("two", shared=0))
+    self.assertTrue(self.ls.check_owned("two", shared=1))
+    self.assertTrue(self.ls.check_owned("three", shared=0))
+
     # Downgrading the last exclusive lock to shared must downgrade the
     # lockset-internal lock too
     self.assertTrue(self.ls.downgrade(names="three"))
-    self.assertTrue(self.ls._get_lock()._is_owned(shared=1))
-    self.assertTrue(compat.all(i._is_owned(shared=1)
+    self.assertTrue(self.ls._get_lock().is_owned(shared=1))
+    self.assertTrue(compat.all(i.is_owned(shared=1)
                                for i in self.ls._get_lockdict().values()))
 
+    # Verify owned locks
+    for name in self.ls._names():
+      self.assertTrue(self.ls.check_owned(name, shared=1))
+
     # Downgrading a shared lock must be a no-op
     self.assertTrue(self.ls.downgrade(names=["one", "three"]))
-    self.assertTrue(self.ls._get_lock()._is_owned(shared=1))
-    self.assertTrue(compat.all(i._is_owned(shared=1)
+    self.assertTrue(self.ls._get_lock().is_owned(shared=1))
+    self.assertTrue(compat.all(i.is_owned(shared=1)
                                for i in self.ls._get_lockdict().values()))
 
     self.ls.release()
@@ -1653,38 +1814,41 @@ class TestGanetiLockManager(_ThreadedTestCase):
 
   def testAcquireRelease(self):
     self.GL.acquire(locking.LEVEL_CLUSTER, ['BGL'], shared=1)
-    self.assertEquals(self.GL._list_owned(locking.LEVEL_CLUSTER), set(['BGL']))
+    self.assertEquals(self.GL.list_owned(locking.LEVEL_CLUSTER), set(['BGL']))
     self.GL.acquire(locking.LEVEL_INSTANCE, ['i1'])
     self.GL.acquire(locking.LEVEL_NODEGROUP, ['g2'])
     self.GL.acquire(locking.LEVEL_NODE, ['n1', 'n2'], shared=1)
+    self.assertTrue(self.GL.check_owned(locking.LEVEL_NODE, ["n1", "n2"],
+                                        shared=1))
+    self.assertFalse(self.GL.check_owned(locking.LEVEL_INSTANCE, ["i1", "i3"]))
     self.GL.release(locking.LEVEL_NODE, ['n2'])
-    self.assertEquals(self.GL._list_owned(locking.LEVEL_NODE), set(['n1']))
-    self.assertEquals(self.GL._list_owned(locking.LEVEL_NODEGROUP), set(['g2']))
-    self.assertEquals(self.GL._list_owned(locking.LEVEL_INSTANCE), set(['i1']))
+    self.assertEquals(self.GL.list_owned(locking.LEVEL_NODE), set(['n1']))
+    self.assertEquals(self.GL.list_owned(locking.LEVEL_NODEGROUP), set(['g2']))
+    self.assertEquals(self.GL.list_owned(locking.LEVEL_INSTANCE), set(['i1']))
     self.GL.release(locking.LEVEL_NODE)
-    self.assertEquals(self.GL._list_owned(locking.LEVEL_NODE), set())
-    self.assertEquals(self.GL._list_owned(locking.LEVEL_NODEGROUP), set(['g2']))
-    self.assertEquals(self.GL._list_owned(locking.LEVEL_INSTANCE), set(['i1']))
+    self.assertEquals(self.GL.list_owned(locking.LEVEL_NODE), set())
+    self.assertEquals(self.GL.list_owned(locking.LEVEL_NODEGROUP), set(['g2']))
+    self.assertEquals(self.GL.list_owned(locking.LEVEL_INSTANCE), set(['i1']))
     self.GL.release(locking.LEVEL_NODEGROUP)
     self.GL.release(locking.LEVEL_INSTANCE)
     self.assertRaises(errors.LockError, self.GL.acquire,
                       locking.LEVEL_INSTANCE, ['i5'])
     self.GL.acquire(locking.LEVEL_INSTANCE, ['i3'], shared=1)
-    self.assertEquals(self.GL._list_owned(locking.LEVEL_INSTANCE), set(['i3']))
+    self.assertEquals(self.GL.list_owned(locking.LEVEL_INSTANCE), set(['i3']))
 
   def testAcquireWholeSets(self):
     self.GL.acquire(locking.LEVEL_CLUSTER, ['BGL'], shared=1)
     self.assertEquals(self.GL.acquire(locking.LEVEL_INSTANCE, None),
                       set(self.instances))
-    self.assertEquals(self.GL._list_owned(locking.LEVEL_INSTANCE),
+    self.assertEquals(self.GL.list_owned(locking.LEVEL_INSTANCE),
                       set(self.instances))
     self.assertEquals(self.GL.acquire(locking.LEVEL_NODEGROUP, None),
                       set(self.nodegroups))
-    self.assertEquals(self.GL._list_owned(locking.LEVEL_NODEGROUP),
+    self.assertEquals(self.GL.list_owned(locking.LEVEL_NODEGROUP),
                       set(self.nodegroups))
     self.assertEquals(self.GL.acquire(locking.LEVEL_NODE, None, shared=1),
                       set(self.nodes))
-    self.assertEquals(self.GL._list_owned(locking.LEVEL_NODE),
+    self.assertEquals(self.GL.list_owned(locking.LEVEL_NODE),
                       set(self.nodes))
     self.GL.release(locking.LEVEL_NODE)
     self.GL.release(locking.LEVEL_NODEGROUP)
@@ -1695,11 +1859,11 @@ class TestGanetiLockManager(_ThreadedTestCase):
     self.GL.acquire(locking.LEVEL_CLUSTER, ['BGL'], shared=1)
     self.assertEquals(self.GL.acquire(locking.LEVEL_INSTANCE, None),
                       set(self.instances))
-    self.assertEquals(self.GL._list_owned(locking.LEVEL_INSTANCE),
+    self.assertEquals(self.GL.list_owned(locking.LEVEL_INSTANCE),
                       set(self.instances))
     self.assertEquals(self.GL.acquire(locking.LEVEL_NODE, ['n2'], shared=1),
                       set(['n2']))
-    self.assertEquals(self.GL._list_owned(locking.LEVEL_NODE),
+    self.assertEquals(self.GL.list_owned(locking.LEVEL_NODE),
                       set(['n2']))
     self.GL.release(locking.LEVEL_NODE)
     self.GL.release(locking.LEVEL_INSTANCE)
index 28a799e..4dda147 100755 (executable)
@@ -31,7 +31,7 @@ from ganeti import utils
 from ganeti import masterd
 
 from ganeti.masterd.instance import \
-  ImportExportTimeouts, _TimeoutExpired, _DiskImportExportBase, \
+  ImportExportTimeouts, _DiskImportExportBase, \
   ComputeRemoteExportHandshake, CheckRemoteExportHandshake, \
   ComputeRemoteImportDiskInfo, CheckRemoteExportDiskInfo, \
   FormatProgress
@@ -60,10 +60,10 @@ class TestMisc(unittest.TestCase):
     self.assertEqual(tmo.progress, 5)
 
   def testTimeoutExpired(self):
-    self.assert_(_TimeoutExpired(100, 300, _time_fn=lambda: 500))
-    self.assertFalse(_TimeoutExpired(100, 300, _time_fn=lambda: 0))
-    self.assertFalse(_TimeoutExpired(100, 300, _time_fn=lambda: 100))
-    self.assertFalse(_TimeoutExpired(100, 300, _time_fn=lambda: 400))
+    self.assert_(utils.TimeoutExpired(100, 300, _time_fn=lambda: 500))
+    self.assertFalse(utils.TimeoutExpired(100, 300, _time_fn=lambda: 0))
+    self.assertFalse(utils.TimeoutExpired(100, 300, _time_fn=lambda: 100))
+    self.assertFalse(utils.TimeoutExpired(100, 300, _time_fn=lambda: 400))
 
   def testDiskImportExportBaseDirect(self):
     self.assertRaises(AssertionError, _DiskImportExportBase,
index 2070762..0678140 100755 (executable)
 
 
 import unittest
+import itertools
 
 from ganeti import mcpu
 from ganeti import opcodes
+from ganeti import cmdlib
+from ganeti import constants
 from ganeti.constants import \
     LOCK_ATTEMPTS_TIMEOUT, \
     LOCK_ATTEMPTS_MAXWAIT, \
@@ -34,12 +37,30 @@ from ganeti.constants import \
 import testutils
 
 
+REQ_BGL_WHITELIST = frozenset([
+  opcodes.OpClusterActivateMasterIp,
+  opcodes.OpClusterDeactivateMasterIp,
+  opcodes.OpClusterDestroy,
+  opcodes.OpClusterPostInit,
+  opcodes.OpClusterRename,
+  opcodes.OpInstanceRename,
+  opcodes.OpNodeAdd,
+  opcodes.OpNodeRemove,
+  opcodes.OpTestAllocator,
+  ])
+
+
 class TestLockAttemptTimeoutStrategy(unittest.TestCase):
   def testConstants(self):
     tpa = mcpu.LockAttemptTimeoutStrategy._TIMEOUT_PER_ATTEMPT
     self.assert_(len(tpa) > LOCK_ATTEMPTS_TIMEOUT / LOCK_ATTEMPTS_MAXWAIT)
     self.assert_(sum(tpa) >= LOCK_ATTEMPTS_TIMEOUT)
 
+    self.assertTrue(LOCK_ATTEMPTS_TIMEOUT >= 1800,
+                    msg="Waiting less than half an hour per priority")
+    self.assertTrue(LOCK_ATTEMPTS_TIMEOUT <= 3600,
+                    msg="Waiting more than an hour per priority")
+
   def testSimple(self):
     strat = mcpu.LockAttemptTimeoutStrategy(_random_fn=lambda: 0.5,
                                             _time_fn=lambda: 0.0)
@@ -67,6 +88,83 @@ class TestDispatchTable(unittest.TestCase):
       self.assertTrue(opcls in mcpu.Processor.DISPATCH_TABLE,
                       msg="%s missing handler class" % opcls)
 
+      # Check against BGL whitelist
+      lucls = mcpu.Processor.DISPATCH_TABLE[opcls]
+      if lucls.REQ_BGL:
+        self.assertTrue(opcls in REQ_BGL_WHITELIST,
+                        msg=("%s not whitelisted for BGL" % opcls.OP_ID))
+      else:
+        self.assertFalse(opcls in REQ_BGL_WHITELIST,
+                         msg=("%s whitelisted for BGL, but doesn't use it" %
+                              opcls.OP_ID))
+
+
+class TestProcessResult(unittest.TestCase):
+  def setUp(self):
+    self._submitted = []
+    self._count = itertools.count(200)
+
+  def _Submit(self, jobs):
+    job_ids = [self._count.next() for _ in jobs]
+    self._submitted.extend(zip(job_ids, jobs))
+    return job_ids
+
+  def testNoJobs(self):
+    for i in [object(), [], False, True, None, 1, 929, {}]:
+      self.assertEqual(mcpu._ProcessResult(NotImplemented, NotImplemented, i),
+                       i)
+
+  def testDefaults(self):
+    src = opcodes.OpTestDummy()
+
+    res = mcpu._ProcessResult(self._Submit, src, cmdlib.ResultWithJobs([[
+      opcodes.OpTestDelay(),
+      opcodes.OpTestDelay(),
+      ], [
+      opcodes.OpTestDelay(),
+      ]]))
+
+    self.assertEqual(res, {
+      constants.JOB_IDS_KEY: [200, 201],
+      })
+
+    (_, (op1, op2)) = self._submitted.pop(0)
+    (_, (op3, )) = self._submitted.pop(0)
+    self.assertRaises(IndexError, self._submitted.pop)
+
+    for op in [op1, op2, op3]:
+      self.assertTrue("OP_TEST_DUMMY" in op.comment)
+      self.assertFalse(hasattr(op, "priority"))
+      self.assertFalse(hasattr(op, "debug_level"))
+
+  def testParams(self):
+    src = opcodes.OpTestDummy(priority=constants.OP_PRIO_HIGH,
+                              debug_level=3)
+
+    res = mcpu._ProcessResult(self._Submit, src, cmdlib.ResultWithJobs([[
+      opcodes.OpTestDelay(priority=constants.OP_PRIO_LOW),
+      ], [
+      opcodes.OpTestDelay(comment="foobar", debug_level=10),
+      ]], other=True, value=range(10)))
+
+    self.assertEqual(res, {
+      constants.JOB_IDS_KEY: [200, 201],
+      "other": True,
+      "value": range(10),
+      })
+
+    (_, (op1, )) = self._submitted.pop(0)
+    (_, (op2, )) = self._submitted.pop(0)
+    self.assertRaises(IndexError, self._submitted.pop)
+
+    self.assertEqual(op1.priority, constants.OP_PRIO_LOW)
+    self.assertTrue("OP_TEST_DUMMY" in op1.comment)
+    self.assertEqual(op1.debug_level, 3)
+
+    self.assertEqual(op2.priority, constants.OP_PRIO_HIGH)
+    self.assertEqual(op2.comment, "foobar")
+    self.assertEqual(op2.debug_level, 3)
+
 
 if __name__ == "__main__":
   testutils.GanetiTestProgram()
index dd7cd47..dcb7c4e 100755 (executable)
@@ -161,6 +161,33 @@ class TestIPAddress(unittest.TestCase):
     self.assertEqual(fn("2001:db8::1"), socket.AF_INET6)
     self.assertRaises(errors.IPAddressError, fn, "0")
 
+  def testValidateNetmask(self):
+    for netmask in [0, 33]:
+      self.assertFalse(netutils.IP4Address.ValidateNetmask(netmask))
+
+    for netmask in [1, 32]:
+      self.assertTrue(netutils.IP4Address.ValidateNetmask(netmask))
+
+    for netmask in [0, 129]:
+      self.assertFalse(netutils.IP6Address.ValidateNetmask(netmask))
+
+    for netmask in [1, 128]:
+      self.assertTrue(netutils.IP6Address.ValidateNetmask(netmask))
+
+  def testGetClassFromX(self):
+    self.assert_(
+        netutils.IPAddress.GetClassFromIpVersion(constants.IP4_VERSION) ==
+        netutils.IP4Address)
+    self.assert_(
+        netutils.IPAddress.GetClassFromIpVersion(constants.IP6_VERSION) ==
+        netutils.IP6Address)
+    self.assert_(
+        netutils.IPAddress.GetClassFromIpFamily(socket.AF_INET) ==
+        netutils.IP4Address)
+    self.assert_(
+        netutils.IPAddress.GetClassFromIpFamily(socket.AF_INET6) ==
+        netutils.IP6Address)
+
   def testOwnLoopback(self):
     # FIXME: In a pure IPv6 environment this is no longer true
     self.assert_(netutils.IPAddress.Own("127.0.0.1"),
index 996a50d..95978c5 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 #
 
-# Copyright (C) 2006, 2007, 2008, 2010 Google Inc.
+# Copyright (C) 2006, 2007, 2008, 2010, 2012 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
@@ -16,7 +16,7 @@
 # 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
-# 0.0510-1301, USA.
+# 02110-1301, USA.
 
 
 """Script for unittesting the objects module"""
@@ -78,7 +78,8 @@ class TestClusterObject(unittest.TestCase):
         },
       }
     ndparams = {
-        constants.ND_OOB_PROGRAM: "/bin/cluster-oob"
+        constants.ND_OOB_PROGRAM: "/bin/cluster-oob",
+        constants.ND_SPINDLE_COUNT: 1
         }
 
     self.fake_cl = objects.Cluster(hvparams=hvparams, os_hvp=os_hvp,
@@ -161,7 +162,8 @@ class TestClusterObject(unittest.TestCase):
                              ndparams={},
                              group="testgroup")
     group_ndparams = {
-        constants.ND_OOB_PROGRAM: "/bin/group-oob"
+        constants.ND_OOB_PROGRAM: "/bin/group-oob",
+        constants.ND_SPINDLE_COUNT: 10,
         }
     fake_group = objects.NodeGroup(name="testgroup",
                                    ndparams=group_ndparams)
@@ -170,7 +172,8 @@ class TestClusterObject(unittest.TestCase):
 
   def testFillNdParamsNode(self):
     node_ndparams = {
-        constants.ND_OOB_PROGRAM: "/bin/node-oob"
+        constants.ND_OOB_PROGRAM: "/bin/node-oob",
+        constants.ND_SPINDLE_COUNT: 2,
         }
     fake_node = objects.Node(name="test",
                              ndparams=node_ndparams,
@@ -182,19 +185,32 @@ class TestClusterObject(unittest.TestCase):
 
   def testFillNdParamsAll(self):
     node_ndparams = {
-        constants.ND_OOB_PROGRAM: "/bin/node-oob"
+        constants.ND_OOB_PROGRAM: "/bin/node-oob",
+        constants.ND_SPINDLE_COUNT: 5,
         }
     fake_node = objects.Node(name="test",
                              ndparams=node_ndparams,
                              group="testgroup")
     group_ndparams = {
-        constants.ND_OOB_PROGRAM: "/bin/group-oob"
+        constants.ND_OOB_PROGRAM: "/bin/group-oob",
+        constants.ND_SPINDLE_COUNT: 4,
         }
     fake_group = objects.NodeGroup(name="testgroup",
                                    ndparams=group_ndparams)
     self.assertEqual(node_ndparams,
                      self.fake_cl.FillND(fake_node, fake_group))
 
+  def testPrimaryHypervisor(self):
+    assert self.fake_cl.enabled_hypervisors is None
+    self.fake_cl.enabled_hypervisors = [constants.HT_XEN_HVM]
+    self.assertEqual(self.fake_cl.primary_hypervisor, constants.HT_XEN_HVM)
+
+    self.fake_cl.enabled_hypervisors = [constants.HT_XEN_PVM, constants.HT_KVM]
+    self.assertEqual(self.fake_cl.primary_hypervisor, constants.HT_XEN_PVM)
+
+    self.fake_cl.enabled_hypervisors = sorted(constants.HYPER_TYPES)
+    self.assertEqual(self.fake_cl.primary_hypervisor, constants.HT_CHROOT)
+
 
 class TestOS(unittest.TestCase):
   ALL_DATA = [
@@ -283,5 +299,54 @@ class TestInstance(unittest.TestCase):
     self.assertRaises(errors.OpPrereqError, inst.FindDisk, 1)
 
 
+class TestNode(unittest.TestCase):
+  def testEmpty(self):
+    self.assertEqual(objects.Node().ToDict(), {})
+    self.assertTrue(isinstance(objects.Node.FromDict({}), objects.Node))
+
+  def testHvState(self):
+    node = objects.Node(name="node18157.example.com", hv_state={
+      constants.HT_XEN_HVM: objects.NodeHvState(cpu_total=64),
+      constants.HT_KVM: objects.NodeHvState(cpu_node=1),
+      })
+
+    node2 = objects.Node.FromDict(node.ToDict())
+
+    # Make sure nothing can reference it anymore
+    del node
+
+    self.assertEqual(node2.name, "node18157.example.com")
+    self.assertEqual(frozenset(node2.hv_state), frozenset([
+      constants.HT_XEN_HVM,
+      constants.HT_KVM,
+      ]))
+    self.assertEqual(node2.hv_state[constants.HT_KVM].cpu_node, 1)
+    self.assertEqual(node2.hv_state[constants.HT_XEN_HVM].cpu_total, 64)
+
+  def testDiskState(self):
+    node = objects.Node(name="node32087.example.com", disk_state={
+      constants.LD_LV: {
+        "lv32352": objects.NodeDiskState(total=128),
+        "lv2082": objects.NodeDiskState(total=512),
+        },
+      })
+
+    node2 = objects.Node.FromDict(node.ToDict())
+
+    # Make sure nothing can reference it anymore
+    del node
+
+    self.assertEqual(node2.name, "node32087.example.com")
+    self.assertEqual(frozenset(node2.disk_state), frozenset([
+      constants.LD_LV,
+      ]))
+    self.assertEqual(frozenset(node2.disk_state[constants.LD_LV]), frozenset([
+      "lv32352",
+      "lv2082",
+      ]))
+    self.assertEqual(node2.disk_state[constants.LD_LV]["lv2082"].total, 512)
+    self.assertEqual(node2.disk_state[constants.LD_LV]["lv32352"].total, 128)
+
+
 if __name__ == '__main__':
   testutils.GanetiTestProgram()
index e3ba1cc..7e6cb6e 100755 (executable)
@@ -35,6 +35,16 @@ from ganeti import compat
 import testutils
 
 
+#: Unless an opcode is included in the following list it must have a result
+#: check of some sort
+MISSING_RESULT_CHECK = frozenset([
+  opcodes.OpTestAllocator,
+  opcodes.OpTestDelay,
+  opcodes.OpTestDummy,
+  opcodes.OpTestJqueue,
+  ])
+
+
 class TestOpcodes(unittest.TestCase):
   def test(self):
     self.assertRaises(ValueError, opcodes.OpCode.LoadOpCode, None)
@@ -49,7 +59,13 @@ class TestOpcodes(unittest.TestCase):
       self.assertEqual(cls.OP_ID, opcodes._NameToId(cls.__name__))
       self.assertFalse(compat.any(cls.OP_ID.startswith(prefix)
                                   for prefix in opcodes._SUMMARY_PREFIX.keys()))
-      self.assertTrue(cls.OP_RESULT is None or callable(cls.OP_RESULT))
+      if cls in MISSING_RESULT_CHECK:
+        self.assertTrue(cls.OP_RESULT is None,
+                        msg=("%s is listed to not have a result check" %
+                             cls.OP_ID))
+      else:
+        self.assertTrue(callable(cls.OP_RESULT),
+                        msg=("%s should have a result check" % cls.OP_ID))
 
       self.assertRaises(TypeError, cls, unsupported_parameter="some value")
 
@@ -338,5 +354,64 @@ class TestResultChecks(unittest.TestCase):
       }))
 
 
+class TestClusterOsList(unittest.TestCase):
+  def test(self):
+    good = [
+      None,
+      [],
+      [(constants.DDM_ADD, "dos"),
+       (constants.DDM_REMOVE, "linux")],
+      ]
+
+    for i in good:
+      self.assertTrue(opcodes._TestClusterOsList(i))
+
+    wrong = ["", 0, "xy", ["Hello World"], object(),
+      [("foo", "bar")],
+      [("", "")],
+      [[constants.DDM_ADD]],
+      [(constants.DDM_ADD, "")],
+      [(constants.DDM_REMOVE, "")],
+      [(constants.DDM_ADD, None)],
+      [(constants.DDM_REMOVE, None)],
+      ]
+
+    for i in wrong:
+      self.assertFalse(opcodes._TestClusterOsList(i))
+
+
+class TestOpInstanceSetParams(unittest.TestCase):
+  def _GenericTests(self, fn):
+    self.assertTrue(fn([]))
+    self.assertTrue(fn([(constants.DDM_ADD, {})]))
+    self.assertTrue(fn([(constants.DDM_REMOVE, {})]))
+    for i in [0, 1, 2, 3, 9, 10, 1024]:
+      self.assertTrue(fn([(i, {})]))
+
+    self.assertFalse(fn(None))
+    self.assertFalse(fn({}))
+    self.assertFalse(fn(""))
+    self.assertFalse(fn(0))
+    self.assertFalse(fn([(-100, {})]))
+    self.assertFalse(fn([(constants.DDM_ADD, 2, 3)]))
+    self.assertFalse(fn([[constants.DDM_ADD]]))
+
+  def testNicModifications(self):
+    fn = opcodes.OpInstanceSetParams.TestNicModifications
+    self._GenericTests(fn)
+
+    for param in constants.INIC_PARAMS:
+      self.assertTrue(fn([[constants.DDM_ADD, {param: None}]]))
+      self.assertTrue(fn([[constants.DDM_ADD, {param: param}]]))
+
+  def testDiskModifications(self):
+    fn = opcodes.OpInstanceSetParams.TestDiskModifications
+    self._GenericTests(fn)
+
+    for param in constants.IDISK_PARAMS:
+      self.assertTrue(fn([[constants.DDM_ADD, {param: 0}]]))
+      self.assertTrue(fn([[constants.DDM_ADD, {param: param}]]))
+
+
 if __name__ == "__main__":
   testutils.GanetiTestProgram()
diff --git a/test/ganeti.ovf_unittest.py b/test/ganeti.ovf_unittest.py
new file mode 100644 (file)
index 0000000..5be3faf
--- /dev/null
@@ -0,0 +1,818 @@
+#!/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 testing ganeti.ovf.
+
+"""
+
+import optparse
+import os
+import os.path
+import re
+import shutil
+import sys
+import tempfile
+import unittest
+
+try:
+  import xml.etree.ElementTree as ET
+except ImportError:
+  import elementtree.ElementTree as ET
+
+from ganeti import constants
+from ganeti import errors
+from ganeti import ovf
+from ganeti import utils
+
+import testutils
+
+OUTPUT_DIR = "newdir"
+
+GANETI_DISKS = {
+  "disk_count": "1",
+  "disk0_dump": "new_disk.raw",
+  "disk0_size": "0",
+  "disk0_ivname": "disk/0",
+}
+GANETI_NETWORKS = {
+  "nic_count": "1",
+  "nic0_mode": "bridged",
+  "nic0_ip": "none",
+  "nic0_mac": "aa:00:00:d8:2c:1e",
+  "nic0_link": "xen-br0",
+}
+GANETI_HYPERVISOR = {
+  "hypervisor_name": "xen-pvm",
+  "root-path": "/dev/sda",
+  "kernel_args": "ro",
+}
+GANETI_OS = {"os_name": "lenny-image"}
+GANETI_BACKEND = {
+  "vcpus": "1",
+  "memory" : "2048",
+  "auto_balance": "False",
+}
+GANETI_NAME = "ganeti-test-xen"
+GANETI_TEMPLATE = "plain"
+GANETI_TAGS = None
+GANETI_VERSION = "0"
+
+VIRTUALBOX_DISKS = {
+  "disk_count": "2",
+  "disk0_ivname": "disk/0",
+  "disk0_dump": "new_disk.raw",
+  "disk0_size": "0",
+  "disk1_ivname": "disk/1",
+  "disk1_dump": "second_disk.raw",
+  "disk1_size": "0",
+}
+VIRTUALBOX_NETWORKS = {
+  "nic_count": "1",
+  "nic0_mode": "bridged",
+  "nic0_ip": "none",
+  "nic0_link": "auto",
+  "nic0_mac": "auto",
+}
+VIRTUALBOX_HYPERVISOR = {"hypervisor_name": "auto"}
+VIRTUALBOX_OS = {"os_name": None}
+VIRTUALBOX_BACKEND = {
+ "vcpus": "1",
+  "memory" : "2048",
+  "auto_balance": "auto",
+}
+VIRTUALBOX_NAME = None
+VIRTUALBOX_TEMPLATE = None
+VIRTUALBOX_TAGS = None
+VIRTUALBOX_VERSION = None
+
+EMPTY_DISKS = {}
+EMPTY_NETWORKS = {}
+EMPTY_HYPERVISOR = {"hypervisor_name": "auto"}
+EMPTY_OS = {}
+EMPTY_BACKEND = {
+  "vcpus": "auto",
+  "memory" : "auto",
+  "auto_balance": "auto",
+}
+EMPTY_NAME = None
+EMPTY_TEMPLATE = None
+EMPTY_TAGS = None
+EMPTY_VERSION = None
+
+CMDARGS_DISKS = {
+  "disk_count": "1",
+  "disk0_ivname": "disk/0",
+  "disk0_dump": "disk0.raw",
+  "disk0_size": "8",
+}
+CMDARGS_NETWORKS = {
+  "nic0_link": "auto",
+  "nic0_mode": "bridged",
+  "nic0_ip": "none",
+  "nic0_mac": "auto",
+  "nic_count": "1",
+}
+CMDARGS_HYPERVISOR = {
+  "hypervisor_name": "xen-pvm"
+}
+CMDARGS_OS = {"os_name": "lenny-image"}
+CMDARGS_BACKEND = {
+  "auto_balance": False,
+  "vcpus": "1",
+  "memory": "256",
+}
+CMDARGS_NAME = "test-instance"
+CMDARGS_TEMPLATE = "plain"
+CMDARGS_TAGS = "test-tag-1,test-tag-2"
+
+ARGS_EMPTY = {
+  "output_dir": None,
+  "nics": [],
+  "disks": [],
+  "name": "test-instance",
+  "ova_package": False,
+  "ext_usage": False,
+  "disk_format": "cow",
+  "compression": False,
+}
+ARGS_EXPORT_DIR = dict(ARGS_EMPTY, **{
+  "output_dir": OUTPUT_DIR,
+  "name": None,
+  "hypervisor": None,
+  "os": None,
+  "beparams": {},
+  "no_nics": False,
+  "disk_template": None,
+  "tags": None,
+})
+ARGS_VBOX = dict(ARGS_EXPORT_DIR, **{
+  "output_dir": OUTPUT_DIR,
+  "name": "test-instance",
+  "os": "lenny-image",
+  "hypervisor": ("xen-pvm", {}),
+  "osparams":{},
+  "disks": [],
+})
+ARGS_COMPLETE = dict(ARGS_VBOX, **{
+  "beparams": {"vcpus":"1", "memory":"256", "auto_balance": False},
+  "disks": [(0,{"size":"5mb"})],
+  "nics": [("0",{"mode":"bridged"})],
+  "disk_template": "plain",
+  "tags": "test-tag-1,test-tag-2",
+})
+ARGS_BROKEN = dict(ARGS_EXPORT_DIR , **{
+  "no_nics": True,
+  "disk_template": "diskless",
+  "name": "test-instance",
+  "os": "lenny-image",
+  "osparams": {},
+})
+
+EXP_ARGS_COMPRESSED = dict(ARGS_EXPORT_DIR, **{
+  "compression": True,
+})
+
+EXP_DISKS_LIST = [
+  {
+    "format": "vmdk",
+    "compression": "gzip",
+    "virt-size": 90000,
+    "real-size": 203,
+    "path": "new_disk.cow.gz",
+  },
+  {
+    "format": "cow",
+    "virt-size": 15,
+    "real-size": 15,
+    "path": "new_disk.cow",
+  },
+]
+EXP_NETWORKS_LIST = [
+  {"mac": "aa:00:00:d8:2c:1e", "ip":"None", "link":"br0","mode":"routed"},
+]
+EXP_PARTIAL_GANETI_DICT = {
+  "hypervisor": {"name": "xen-kvm"},
+  "os": {"name": "lenny-image"},
+  "auto_balance": "True",
+  "version": "0",
+}
+EXP_GANETI_DICT = {
+  'tags': None,
+  'auto_balance': 'False',
+  'hypervisor': {
+     'root-path': '/dev/sda',
+     'name': 'xen-pvm',
+     'kernel_args': 'ro'
+   },
+  'version': '0',
+  'disk_template': None,
+  'os': {'name': 'lenny-image'}
+}
+EXP_NAME ="xen-dev-i1"
+EXP_VCPUS = 1
+EXP_MEMORY = 512
+
+EXPORT_EMPTY = ("<Envelope xml:lang=\"en-US\" xmlns=\"http://schemas.dmtf.org/"
+                "ovf/envelope/1\" xmlns:gnt=\"http://ganeti\" xmlns:ovf=\""
+                "http://schemas.dmtf.org/ovf/envelope/1\" xmlns:rasd=\""
+                "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_Resource"
+                "AllocationSettingData\" xmlns:vssd=\"http://schemas.dmtf.org"
+                "/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData\""
+                " xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" />")
+EXPORT_DISKS_EMPTY = ("<References /><DiskSection><Info>Virtual disk"
+                      " information</Info></DiskSection>")
+EXPORT_DISKS = ("<References><File ovf:compression=\"gzip\" ovf:href=\"new_disk"
+                ".cow.gz\" ovf:id=\"file0\" ovf:size=\"203\" /><File ovf:href="
+                "\"new_disk.cow\" ovf:id=\"file1\" ovf:size=\"15\" />"
+                "</References><DiskSection><Info>Virtual disk information"
+                "</Info><Disk ovf:capacity=\"90000\" ovf:diskId=\"disk0\" ovf"
+                ":fileRef=\"file0\" ovf:format=\"http://www.vmware.com/"
+                "interfaces/specifications/vmdk.html#monolithicSparse\" /><Disk"
+                " ovf:capacity=\"15\" ovf:diskId=\"disk1\" ovf:fileRef"
+                "=\"file1\" ovf:format=\"http://www.gnome.org/~markmc/qcow"
+                "-image-format.html\" /></DiskSection>")
+EXPORT_NETWORKS_EMPTY = ("<NetworkSection><Info>List of logical networks</Info>"
+                         "</NetworkSection>")
+EXPORT_NETWORKS = ("<NetworkSection><Info>List of logical networks</Info>"
+                   "<Network ovf:name=\"routed0\" /></NetworkSection>")
+EXPORT_GANETI_INCOMPLETE = ("<gnt:GanetiSection><gnt:Version>0</gnt:Version>"
+                            "<gnt:AutoBalance>True</gnt:AutoBalance><gnt:"
+                            "OperatingSystem><gnt:Name>lenny-image</gnt:Name>"
+                            "<gnt:Parameters /></gnt:OperatingSystem><gnt:"
+                            "Hypervisor><gnt:Name>xen-kvm</gnt:Name><gnt:"
+                            "Parameters /></gnt:Hypervisor><gnt:Network><gnt:"
+                            "Nic ovf:name=\"routed0\"><gnt:Mode>routed</gnt:"
+                            "Mode><gnt:MACAddress>aa:00:00:d8:2c:1e</gnt:"
+                            "MACAddress><gnt:IPAddress>None</gnt:IPAddress>"
+                            "<gnt:Link>br0</gnt:Link></gnt:Nic></gnt:Network>"
+                            "</gnt:GanetiSection>")
+EXPORT_GANETI = ("<gnt:GanetiSection><gnt:Version>0</gnt:Version><gnt:"
+                 "AutoBalance>False</gnt:AutoBalance><gnt:OperatingSystem>"
+                 "<gnt:Name>lenny-image</gnt:Name><gnt:Parameters /></gnt:"
+                 "OperatingSystem><gnt:Hypervisor><gnt:Name>xen-pvm</gnt:Name>"
+                 "<gnt:Parameters><gnt:root-path>/dev/sda</gnt:root-path><gnt:"
+                 "kernel_args>ro</gnt:kernel_args></gnt:Parameters></gnt:"
+                 "Hypervisor><gnt:Network><gnt:Nic ovf:name=\"routed0\"><gnt:"
+                 "Mode>routed</gnt:Mode><gnt:MACAddress>aa:00:00:d8:2c:1e</gnt:"
+                 "MACAddress><gnt:IPAddress>None</gnt:IPAddress><gnt:Link>br0"
+                 "</gnt:Link></gnt:Nic></gnt:Network></gnt:GanetiSection>")
+EXPORT_SYSTEM = ("<References><File ovf:compression=\"gzip\" ovf:href=\"new_"
+                 "disk.cow.gz\" ovf:id=\"file0\" ovf:size=\"203\" /><File ovf:"
+                 "href=\"new_disk.cow\" ovf:id=\"file1\" ovf:size=\"15\" />"
+                 "</References><DiskSection><Info>Virtual disk information"
+                 "</Info><Disk ovf:capacity=\"90000\" ovf:diskId=\"disk0\""
+                 " ovf:fileRef=\"file0\" ovf:format=\"http://www.vmware.com"
+                 "/interfaces/specifications/vmdk.html#monolithicSparse\" />"
+                 "<Disk ovf:capacity=\"15\" ovf:diskId=\"disk1\" ovf:fileRef"
+                 "=\"file1\" ovf:format=\"http://www.gnome.org/~markmc/qcow"
+                 "-image-format.html\" /></DiskSection><NetworkSection><Info>"
+                 "List of logical networks</Info><Network ovf:name=\"routed0\""
+                 " /></NetworkSection><VirtualSystem ovf:id=\"xen-dev-i1\">"
+                 "<Info>A virtual machine</Info><Name>xen-dev-i1</Name>"
+                 "<OperatingSystemSection ovf:id=\"0\"><Info>Installed guest"
+                 " operating system</Info></OperatingSystemSection><Virtual"
+                 "HardwareSection><Info>Virtual hardware requirements</Info>"
+                 "<System><vssd:ElementName>Virtual Hardware Family"
+                 "</vssd:ElementName><vssd:InstanceID>0</vssd:InstanceID><vssd:"
+                 "VirtualSystemIdentifier>xen-dev-i1</vssd:VirtualSystem"
+                 "Identifier><vssd:VirtualSystemType>ganeti-ovf</vssd:Virtual"
+                 "SystemType></System><Item><rasd:ElementName>1 virtual CPU(s)"
+                 "</rasd:ElementName><rasd:InstanceID>1</rasd:InstanceID><rasd:"
+                 "ResourceType>3</rasd:ResourceType><rasd:VirtualQuantity>1"
+                 "</rasd:VirtualQuantity></Item><Item><rasd:AllocationUnits>"
+                 "byte * 2^20</rasd:AllocationUnits><rasd:ElementName>512MB of"
+                 " memory</rasd:ElementName><rasd:InstanceID>2</rasd:"
+                 "InstanceID><rasd:ResourceType>4</rasd:ResourceType><rasd:"
+                 "VirtualQuantity>512</rasd:VirtualQuantity></Item><Item>"
+                 "<rasd:Address>0</rasd:Address><rasd:ElementName>scsi"
+                 "_controller0</rasd:ElementName><rasd:InstanceID>3"
+                 "</rasd:InstanceID><rasd:ResourceSubType>lsilogic</rasd"
+                 ":ResourceSubType><rasd:ResourceType>6</rasd:ResourceType>"
+                 "</Item><Item><rasd:ElementName>disk0</rasd:ElementName><rasd"
+                 ":HostResource>ovf:/disk/disk0</rasd:HostResource><rasd"
+                 ":InstanceID>4</rasd:InstanceID><rasd:Parent>3</rasd:Parent>"
+                 "<rasd:ResourceType>17</rasd:ResourceType></Item><Item><rasd:"
+                 "ElementName>disk1</rasd:ElementName><rasd:HostResource>ovf:/"
+                 "disk/disk1</rasd:HostResource><rasd:InstanceID>5</rasd"
+                 ":InstanceID><rasd:Parent>3</rasd:Parent><rasd:ResourceType>17"
+                 "</rasd:ResourceType></Item><Item><rasd:Address>aa:00"
+                 ":00:d8:2c:1e</rasd:Address><rasd:Connection>routed0</rasd"
+                 ":Connection><rasd:ElementName>routed0</rasd:ElementName><rasd"
+                 ":InstanceID>6</rasd:InstanceID><rasd:ResourceType>10</rasd"
+                 ":ResourceType></Item></VirtualHardwareSection>"
+                 "</VirtualSystem>")
+
+
+def _GetArgs(args, with_name=False):
+  options = optparse.Values()
+  needed = args
+  if with_name:
+    needed["name"] = "test-instance"
+  options._update_loose(needed)
+  return options
+
+
+OPTS_EMPTY = _GetArgs(ARGS_EMPTY)
+OPTS_EXPORT_NO_NAME = _GetArgs(ARGS_EXPORT_DIR)
+OPTS_EXPORT = _GetArgs(ARGS_EXPORT_DIR, with_name=True)
+
+EXP_OPTS = OPTS_EXPORT_NO_NAME
+EXP_OPTS_COMPRESSED = _GetArgs(EXP_ARGS_COMPRESSED)
+
+OPTS_VBOX = _GetArgs(ARGS_VBOX)
+OPTS_COMPLETE = _GetArgs(ARGS_COMPLETE)
+OPTS_NONIC_NODISK = _GetArgs(ARGS_BROKEN)
+
+
+def _GetFullFilename(file_name):
+  file_path = "%s/test/data/ovfdata/%s" % (testutils.GetSourceDir(),
+    file_name)
+  file_path = os.path.abspath(file_path)
+  return file_path
+
+
+class BetterUnitTest(unittest.TestCase):
+  def assertRaisesRegexp(self, exception, regexp_val, function, *args):
+    try:
+      function(*args)
+      self.fail("Expected raising %s" % exception)
+    except exception, err:
+      regexp = re.compile(regexp_val)
+      if re.search(regexp, str(err)) == None:
+        self.fail("Expected matching '%s', got '%s'" %
+          (regexp_val, str(err)))
+
+
+class TestOVFImporter(BetterUnitTest):
+  def setUp(self):
+    self.non_existing_file = _GetFullFilename("not_the_file.ovf")
+    self.ganeti_ovf = _GetFullFilename("ganeti.ovf")
+    self.virtualbox_ovf = _GetFullFilename("virtualbox.ovf")
+    self.ova_package = _GetFullFilename("ova.ova")
+    self.empty_ovf = _GetFullFilename("empty.ovf")
+    self.wrong_extension = _GetFullFilename("wrong_extension.ovd")
+    self.wrong_ova_archive = _GetFullFilename("wrong_ova.ova")
+    self.no_ovf_in_ova = _GetFullFilename("no_ovf.ova")
+    self.importer = None
+
+  def tearDown(self):
+    if self.importer:
+      self.importer.Cleanup()
+    del_dir = os.path.abspath(OUTPUT_DIR)
+    try:
+      shutil.rmtree(del_dir)
+    except OSError:
+      pass
+
+  def testFileDoesNotExistError(self):
+    self.assertRaisesRegexp(errors.OpPrereqError, "does not exist",
+      ovf.OVFImporter, self.non_existing_file, None)
+
+  def testWrongInputFileExtensionError(self):
+    self.assertRaisesRegexp(errors.OpPrereqError,
+      "Unknown file extension", ovf.OVFImporter,
+      self.wrong_extension, None)
+
+  def testOVAUnpackingDirectories(self):
+    self.importer = ovf.OVFImporter(self.ova_package, OPTS_EMPTY)
+    self.assertTrue(self.importer.input_dir != None)
+    self.assertEquals(self.importer.output_dir , constants.EXPORT_DIR)
+    self.assertTrue(self.importer.temp_dir != None)
+
+  def testOVFUnpackingDirectories(self):
+    self.importer = ovf.OVFImporter(self.virtualbox_ovf,
+      OPTS_EMPTY)
+    self.assertEquals(self.importer.input_dir , _GetFullFilename(""))
+    self.assertEquals(self.importer.output_dir , constants.EXPORT_DIR)
+    self.assertEquals(self.importer.temp_dir , None)
+
+  def testOVFSetOutputDirDirectories(self):
+    self.importer = ovf.OVFImporter(self.ganeti_ovf, OPTS_EXPORT)
+    self.assertEquals(self.importer.input_dir , _GetFullFilename(""))
+    self.assertTrue(OUTPUT_DIR in self.importer.output_dir)
+    self.assertEquals(self.importer.temp_dir , None)
+
+  def testWrongOVAArchiveError(self):
+    self.assertRaisesRegexp(errors.OpPrereqError, "not a proper tar",
+      ovf.OVFImporter, self.wrong_ova_archive, None)
+
+  def testNoOVFFileInOVAPackageError(self):
+    self.assertRaisesRegexp(errors.OpPrereqError, "No .ovf file",
+      ovf.OVFImporter, self.no_ovf_in_ova, None)
+
+  def testParseGanetiOvf(self):
+    self.importer = ovf.OVFImporter(self.ganeti_ovf, OPTS_EXPORT_NO_NAME)
+    self.importer.Parse()
+    self.assertTrue("%s/ganeti-test-xen" % OUTPUT_DIR in
+      self.importer.output_dir)
+    self.assertEqual(self.importer.results_disk, GANETI_DISKS)
+    self.assertEqual(self.importer.results_network, GANETI_NETWORKS)
+    self.assertEqual(self.importer.results_hypervisor, GANETI_HYPERVISOR)
+    self.assertEqual(self.importer.results_os, GANETI_OS)
+    self.assertEqual(self.importer.results_backend, GANETI_BACKEND)
+    self.assertEqual(self.importer.results_name, GANETI_NAME)
+    self.assertEqual(self.importer.results_template, GANETI_TEMPLATE)
+    self.assertEqual(self.importer.results_tags, GANETI_TAGS)
+    self.assertEqual(self.importer.results_version, GANETI_VERSION)
+
+  def testParseVirtualboxOvf(self):
+    self.importer = ovf.OVFImporter(self.virtualbox_ovf, OPTS_VBOX)
+    self.importer.Parse()
+    self.assertTrue("%s/test-instance" % OUTPUT_DIR in self.importer.output_dir)
+    self.assertEquals(self.importer.results_disk, VIRTUALBOX_DISKS)
+    self.assertEquals(self.importer.results_network, VIRTUALBOX_NETWORKS)
+    self.assertEquals(self.importer.results_hypervisor, CMDARGS_HYPERVISOR)
+    self.assertEquals(self.importer.results_os, CMDARGS_OS)
+    self.assertEquals(self.importer.results_backend, VIRTUALBOX_BACKEND)
+    self.assertEquals(self.importer.results_name, CMDARGS_NAME)
+    self.assertEquals(self.importer.results_template, VIRTUALBOX_TEMPLATE)
+    self.assertEqual(self.importer.results_tags, VIRTUALBOX_TAGS)
+    self.assertEqual(self.importer.results_version, constants.EXPORT_VERSION)
+
+  def testParseEmptyOvf(self):
+    self.importer = ovf.OVFImporter(self.empty_ovf, OPTS_COMPLETE)
+    self.importer.Parse()
+    self.assertTrue("%s/test-instance" % OUTPUT_DIR in self.importer.output_dir)
+    self.assertEquals(self.importer.results_disk, CMDARGS_DISKS)
+    self.assertEquals(self.importer.results_network, CMDARGS_NETWORKS)
+    self.assertEquals(self.importer.results_hypervisor, CMDARGS_HYPERVISOR)
+    self.assertEquals(self.importer.results_os, CMDARGS_OS)
+    self.assertEquals(self.importer.results_backend, CMDARGS_BACKEND)
+    self.assertEquals(self.importer.results_name, CMDARGS_NAME)
+    self.assertEquals(self.importer.results_template, CMDARGS_TEMPLATE)
+    self.assertEqual(self.importer.results_tags, CMDARGS_TAGS)
+    self.assertEqual(self.importer.results_version, constants.EXPORT_VERSION)
+
+  def testParseNameOptions(self):
+    self.importer = ovf.OVFImporter(self.empty_ovf, OPTS_COMPLETE)
+    results = self.importer._ParseNameOptions()
+    self.assertEquals(results, CMDARGS_NAME)
+
+  def testParseHypervisorOptions(self):
+    self.importer = ovf.OVFImporter(self.empty_ovf, OPTS_COMPLETE)
+    results = self.importer._ParseHypervisorOptions()
+    self.assertEquals(results, CMDARGS_HYPERVISOR)
+
+  def testParseOSOptions(self):
+    self.importer = ovf.OVFImporter(self.empty_ovf, OPTS_COMPLETE)
+    results = self.importer._ParseOSOptions()
+    self.assertEquals(results, CMDARGS_OS)
+
+  def testParseBackendOptions(self):
+    self.importer = ovf.OVFImporter(self.empty_ovf, OPTS_COMPLETE)
+    results = self.importer._ParseBackendOptions()
+    self.assertEquals(results, CMDARGS_BACKEND)
+
+  def testParseTags(self):
+    self.importer = ovf.OVFImporter(self.empty_ovf, OPTS_COMPLETE)
+    results = self.importer._ParseTags()
+    self.assertEquals(results, CMDARGS_TAGS)
+
+  def testParseNicOptions(self):
+    self.importer = ovf.OVFImporter(self.empty_ovf, OPTS_COMPLETE)
+    results = self.importer._ParseNicOptions()
+    self.assertEquals(results, CMDARGS_NETWORKS)
+
+  def testParseDiskOptionsFromGanetiOVF(self):
+    self.importer = ovf.OVFImporter(self.ganeti_ovf, OPTS_EXPORT)
+    os.mkdir(OUTPUT_DIR)
+    results = self.importer._GetDiskInfo()
+    self.assertEquals(results, GANETI_DISKS)
+
+  def testParseTemplateOptions(self):
+    self.importer = ovf.OVFImporter(self.empty_ovf, OPTS_COMPLETE)
+    results = self.importer._ParseTemplateOptions()
+    self.assertEquals(results, GANETI_TEMPLATE)
+
+  def testParseDiskOptionsFromCmdLine(self):
+    self.importer = ovf.OVFImporter(self.empty_ovf, OPTS_COMPLETE)
+    os.mkdir(OUTPUT_DIR)
+    results = self.importer._ParseDiskOptions()
+    self.assertEquals(results, CMDARGS_DISKS)
+
+  def testGetDiskFormat(self):
+    self.importer = ovf.OVFImporter(self.ganeti_ovf, OPTS_EXPORT)
+    disks_list = self.importer.ovf_reader.GetDisksNames()
+    results = [self.importer._GetDiskQemuInfo("%s/%s" %
+      (self.importer.input_dir, path), "file format: (\S+)")
+      for (path, _) in disks_list]
+    self.assertEqual(results, ["vmdk"])
+
+  def testNoInstanceNameOVF(self):
+    self.importer = ovf.OVFImporter(self.empty_ovf, OPTS_EXPORT_NO_NAME)
+    self.assertRaisesRegexp(errors.OpPrereqError, "Name of instance",
+      self.importer.Parse)
+
+  def testErrorNoOSNameOVF(self):
+    self.importer = ovf.OVFImporter(self.virtualbox_ovf, OPTS_EXPORT)
+    self.assertRaisesRegexp(errors.OpPrereqError, "OS name",
+      self.importer.Parse)
+
+  def testErrorNoDiskAndNoNetwork(self):
+    self.importer = ovf.OVFImporter(self.empty_ovf, OPTS_NONIC_NODISK)
+    self.assertRaisesRegexp(errors.OpPrereqError,
+      "Either disk specification or network"
+      " description", self.importer.Parse)
+
+
+class TestOVFExporter(BetterUnitTest):
+  def setUp(self):
+    self.exporter = None
+    self.wrong_config_file = _GetFullFilename("wrong_config.ini")
+    self.unsafe_path_to_disk = _GetFullFilename("unsafe_path.ini")
+    self.disk_image_not_exist = _GetFullFilename("no_disk.ini")
+    self.empty_config = _GetFullFilename("empty.ini")
+    self.standard_export = _GetFullFilename("config.ini")
+    self.wrong_network_mode = self.disk_image_not_exist
+    self.no_memory = self.disk_image_not_exist
+    self.no_vcpus = self.disk_image_not_exist
+    self.no_os = _GetFullFilename("no_os.ini")
+    self.no_hypervisor = self.disk_image_not_exist
+
+  def tearDown(self):
+    if self.exporter:
+      self.exporter.Cleanup()
+    del_dir = os.path.abspath(OUTPUT_DIR)
+    try:
+      shutil.rmtree(del_dir)
+    except OSError:
+      pass
+
+  def testErrorWrongConfigFile(self):
+    self.assertRaisesRegexp(errors.OpPrereqError,
+      "Error when trying to read", ovf.OVFExporter,
+      self.wrong_config_file, EXP_OPTS)
+
+  def testErrorPathToTheDiskIncorrect(self):
+    self.exporter = ovf.OVFExporter(self.unsafe_path_to_disk, EXP_OPTS)
+    self.assertRaisesRegexp(errors.OpPrereqError, "contains a directory name",
+      self.exporter._ParseDisks)
+
+  def testErrorDiskImageNotExist(self):
+    self.exporter = ovf.OVFExporter(self.disk_image_not_exist, EXP_OPTS)
+    self.assertRaisesRegexp(errors.OpPrereqError, "Disk image does not exist",
+      self.exporter._ParseDisks)
+
+  def testParseNetworks(self):
+    self.exporter = ovf.OVFExporter(self.standard_export, EXP_OPTS)
+    results = self.exporter._ParseNetworks()
+    self.assertEqual(results, EXP_NETWORKS_LIST)
+
+  def testErrorWrongNetworkMode(self):
+    self.exporter = ovf.OVFExporter(self.wrong_network_mode, EXP_OPTS)
+    self.assertRaisesRegexp(errors.OpPrereqError,
+      "Network mode nic not recognized", self.exporter._ParseNetworks)
+
+  def testParseVCPusMem(self):
+    self.exporter = ovf.OVFExporter(self.standard_export, EXP_OPTS)
+    vcpus = self.exporter._ParseVCPUs()
+    memory = self.exporter._ParseMemory()
+    self.assertEqual(vcpus, EXP_VCPUS)
+    self.assertEqual(memory, EXP_MEMORY)
+
+  def testErrorNoVCPUs(self):
+    self.exporter = ovf.OVFExporter(self.no_vcpus, EXP_OPTS)
+    self.assertRaisesRegexp(errors.OpPrereqError, "No CPU information found",
+      self.exporter._ParseVCPUs)
+
+  def testErrorNoMemory(self):
+    self.exporter = ovf.OVFExporter(self.no_memory, EXP_OPTS)
+    self.assertRaisesRegexp(errors.OpPrereqError, "No memory information found",
+      self.exporter._ParseMemory)
+
+  def testParseGaneti(self):
+    self.exporter = ovf.OVFExporter(self.standard_export, EXP_OPTS)
+    results = self.exporter._ParseGaneti()
+    self.assertEqual(results, EXP_GANETI_DICT)
+
+  def testErrorNoHypervisor(self):
+    self.exporter = ovf.OVFExporter(self.no_hypervisor, EXP_OPTS)
+    self.assertRaisesRegexp(errors.OpPrereqError,
+      "No hypervisor information found", self.exporter._ParseGaneti)
+
+  def testErrorNoOS(self):
+    self.exporter = ovf.OVFExporter(self.no_os, EXP_OPTS)
+    self.assertRaisesRegexp(errors.OpPrereqError,
+      "No operating system information found", self.exporter._ParseGaneti)
+
+  def testErrorParseNoInstanceName(self):
+    self.exporter = ovf.OVFExporter(self.empty_config, EXP_OPTS)
+    self.assertRaisesRegexp(errors.OpPrereqError, "No instance name found",
+      self.exporter.Parse)
+
+
+class TestOVFReader(BetterUnitTest):
+  def setUp(self):
+    self.wrong_xml_file = _GetFullFilename("wrong_xml.ovf")
+    self.ganeti_ovf = _GetFullFilename("ganeti.ovf")
+    self.virtualbox_ovf = _GetFullFilename("virtualbox.ovf")
+    self.corrupted_ovf = _GetFullFilename("corrupted_resources.ovf")
+    self.wrong_manifest_ovf = _GetFullFilename("wrong_manifest.ovf")
+    self.no_disk_in_ref_ovf = _GetFullFilename("no_disk_in_ref.ovf")
+    self.empty_ovf = _GetFullFilename("empty.ovf")
+    self.compressed_disk = _GetFullFilename("gzip_disk.ovf")
+
+  def tearDown(self):
+    pass
+
+  def testXMLParsingError(self):
+    self.assertRaisesRegexp(errors.OpPrereqError,
+      "Error while reading .ovf", ovf.OVFReader, self.wrong_xml_file)
+
+  def testFileInResourcesDoesNotExistError(self):
+    self.assertRaisesRegexp(errors.OpPrereqError, "does not exist",
+      ovf.OVFReader, self.corrupted_ovf)
+
+  def testWrongManifestChecksumError(self):
+    reader = ovf.OVFReader(self.wrong_manifest_ovf)
+    self.assertRaisesRegexp(errors.OpPrereqError,
+      "does not match the value in manifest file", reader.VerifyManifest)
+
+  def testGoodManifestChecksum(self):
+    reader = ovf.OVFReader(self.ganeti_ovf)
+    self.assertEqual(reader.VerifyManifest(), None)
+
+  def testGetDisksNamesOVFCorruptedError(self):
+    reader = ovf.OVFReader(self.no_disk_in_ref_ovf)
+    self.assertRaisesRegexp(errors.OpPrereqError,
+      "not found in references", reader.GetDisksNames)
+
+  def testGetDisksNamesVirtualbox(self):
+    reader = ovf.OVFReader(self.virtualbox_ovf)
+    disk_names = reader.GetDisksNames()
+    expected_names = [
+      ("new_disk.vmdk", None) ,
+      ("second_disk.vmdk", None),
+    ]
+    self.assertEqual(sorted(disk_names), sorted(expected_names))
+
+  def testGetDisksNamesEmpty(self):
+    reader = ovf.OVFReader(self.empty_ovf)
+    disk_names = reader.GetDisksNames()
+    self.assertEqual(disk_names, [])
+
+  def testGetDisksNamesCompressed(self):
+    reader = ovf.OVFReader(self.compressed_disk)
+    disk_names = reader.GetDisksNames()
+    self.assertEqual(disk_names, [("compr_disk.vmdk.gz", "gzip")])
+
+  def testGetNetworkDataGaneti(self):
+    reader = ovf.OVFReader(self.ganeti_ovf)
+    networks = reader.GetNetworkData()
+    self.assertEqual(networks, GANETI_NETWORKS)
+
+  def testGetNetworkDataVirtualbox(self):
+    reader = ovf.OVFReader(self.virtualbox_ovf)
+    networks = reader.GetNetworkData()
+    self.assertEqual(networks, VIRTUALBOX_NETWORKS)
+
+  def testGetNetworkDataEmpty(self):
+    reader = ovf.OVFReader(self.empty_ovf)
+    networks = reader.GetNetworkData()
+    self.assertEqual(networks, EMPTY_NETWORKS)
+
+  def testGetHypervisorDataGaneti(self):
+    reader = ovf.OVFReader(self.ganeti_ovf)
+    hypervisor = reader.GetHypervisorData()
+    self.assertEqual(hypervisor, GANETI_HYPERVISOR)
+
+  def testGetHypervisorDataEmptyOvf(self):
+    reader = ovf.OVFReader(self.empty_ovf)
+    hypervisor = reader.GetHypervisorData()
+    self.assertEqual(hypervisor, EMPTY_HYPERVISOR)
+
+  def testGetOSDataGaneti(self):
+    reader = ovf.OVFReader(self.ganeti_ovf)
+    osys = reader.GetOSData()
+    self.assertEqual(osys, GANETI_OS)
+
+  def testGetOSDataEmptyOvf(self):
+    reader = ovf.OVFReader(self.empty_ovf)
+    osys = reader.GetOSData()
+    self.assertEqual(osys, EMPTY_OS)
+
+  def testGetBackendDataGaneti(self):
+    reader = ovf.OVFReader(self.ganeti_ovf)
+    backend = reader.GetBackendData()
+    self.assertEqual(backend, GANETI_BACKEND)
+
+  def testGetBackendDataVirtualbox(self):
+    reader = ovf.OVFReader(self.virtualbox_ovf)
+    backend = reader.GetBackendData()
+    self.assertEqual(backend, VIRTUALBOX_BACKEND)
+
+  def testGetBackendDataEmptyOvf(self):
+    reader = ovf.OVFReader(self.empty_ovf)
+    backend = reader.GetBackendData()
+    self.assertEqual(backend, EMPTY_BACKEND)
+
+  def testGetInstanceNameGaneti(self):
+    reader = ovf.OVFReader(self.ganeti_ovf)
+    name = reader.GetInstanceName()
+    self.assertEqual(name, GANETI_NAME)
+
+  def testGetInstanceNameDataEmptyOvf(self):
+    reader = ovf.OVFReader(self.empty_ovf)
+    name = reader.GetInstanceName()
+    self.assertEqual(name, EMPTY_NAME)
+
+  def testGetDiskTemplateGaneti(self):
+    reader = ovf.OVFReader(self.ganeti_ovf)
+    name = reader.GetDiskTemplate()
+    self.assertEqual(name, GANETI_TEMPLATE)
+
+  def testGetDiskTemplateEmpty(self):
+    reader = ovf.OVFReader(self.empty_ovf)
+    name = reader.GetDiskTemplate()
+    self.assertEqual(name, EMPTY_TEMPLATE)
+
+  def testGetTagsGaneti(self):
+    reader = ovf.OVFReader(self.ganeti_ovf)
+    tags = reader.GetTagsData()
+    self.assertEqual(tags, GANETI_TAGS)
+
+  def testGetTagsEmpty(self):
+    reader = ovf.OVFReader(self.empty_ovf)
+    tags = reader.GetTagsData()
+    self.assertEqual(tags, EMPTY_TAGS)
+
+  def testGetVersionGaneti(self):
+    reader = ovf.OVFReader(self.ganeti_ovf)
+    version = reader.GetVersionData()
+    self.assertEqual(version, GANETI_VERSION)
+
+  def testGetVersionEmpty(self):
+    reader = ovf.OVFReader(self.empty_ovf)
+    version = reader.GetVersionData()
+    self.assertEqual(version, EMPTY_VERSION)
+
+
+class TestOVFWriter(BetterUnitTest):
+  def setUp(self):
+    self.writer = ovf.OVFWriter(True)
+
+  def tearDown(self):
+    pass
+
+  def testOVFWriterInit(self):
+    result = ET.tostring(self.writer.tree)
+    self.assertTrue(EXPORT_EMPTY in result)
+
+  def testSaveDisksDataEmpty(self):
+    self.writer.SaveDisksData([])
+    result = ET.tostring(self.writer.tree)
+    self.assertTrue(EXPORT_DISKS_EMPTY in result)
+
+  def testSaveDisksData(self):
+    self.writer.SaveDisksData(EXP_DISKS_LIST)
+    result = ET.tostring(self.writer.tree)
+    self.assertTrue(EXPORT_DISKS in result)
+
+  def testSaveNetworkDataEmpty(self):
+    self.writer.SaveNetworksData([])
+    result = ET.tostring(self.writer.tree)
+    self.assertTrue(EXPORT_NETWORKS_EMPTY in result)
+
+  def testSaveNetworksData(self):
+    self.writer.SaveNetworksData(EXP_NETWORKS_LIST)
+    result = ET.tostring(self.writer.tree)
+    self.assertTrue(EXPORT_NETWORKS in result)
+
+  def testSaveGanetiDataIncomplete(self):
+    self.writer.SaveGanetiData(EXP_PARTIAL_GANETI_DICT, EXP_NETWORKS_LIST)
+    result = ET.tostring(self.writer.tree)
+    self.assertTrue(EXPORT_GANETI_INCOMPLETE in result)
+
+  def testSaveGanetiDataComplete(self):
+    self.writer.SaveGanetiData(EXP_GANETI_DICT, EXP_NETWORKS_LIST)
+    result = ET.tostring(self.writer.tree)
+    self.assertTrue(EXPORT_GANETI in result)
+
+  def testSaveVirtualSystem(self):
+    self.writer.SaveDisksData(EXP_DISKS_LIST)
+    self.writer.SaveNetworksData(EXP_NETWORKS_LIST)
+    self.writer.SaveVirtualSystemData(EXP_NAME, EXP_VCPUS, EXP_MEMORY)
+    result = ET.tostring(self.writer.tree)
+    self.assertTrue(EXPORT_SYSTEM in result)
+
+
+if __name__ == "__main__":
+  testutils.GanetiTestProgram()
index c781289..df249ec 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 #
 
-# Copyright (C) 2010 Google Inc.
+# 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
@@ -37,8 +37,8 @@ class TestMakeSimpleFilter(unittest.TestCase):
     if parse_exp is None:
       parse_exp = names
 
-    filter_ = qlang.MakeSimpleFilter(field, names)
-    self.assertEqual(filter_, expected)
+    qfilter = qlang.MakeSimpleFilter(field, names)
+    self.assertEqual(qfilter, expected)
 
   def test(self):
     self._Test("name", None, None, parse_exp=[])
@@ -53,9 +53,9 @@ class TestParseFilter(unittest.TestCase):
   def setUp(self):
     self.parser = qlang.BuildFilterParser()
 
-  def _Test(self, filter_, expected, expect_filter=True):
-    self.assertEqual(qlang.MakeFilter([filter_], not expect_filter), expected)
-    self.assertEqual(qlang.ParseFilter(filter_, parser=self.parser), expected)
+  def _Test(self, qfilter, expected, expect_filter=True):
+    self.assertEqual(qlang.MakeFilter([qfilter], not expect_filter), expected)
+    self.assertEqual(qlang.ParseFilter(qfilter, parser=self.parser), expected)
 
   def test(self):
     self._Test("name==\"foobar\"", [qlang.OP_EQUAL, "name", "foobar"])
@@ -147,6 +147,11 @@ class TestParseFilter(unittest.TestCase):
                [qlang.OP_NOT, [qlang.OP_REGEXP, "field",
                                utils.DnsNameGlobPattern("*.example.*")]])
 
+    self._Test("ctime < 1234", [qlang.OP_LT, "ctime", 1234])
+    self._Test("ctime > 1234", [qlang.OP_GT, "ctime", 1234])
+    self._Test("mtime <= 9999", [qlang.OP_LE, "mtime", 9999])
+    self._Test("mtime >= 9999", [qlang.OP_GE, "mtime", 9999])
+
   def testAllFields(self):
     for name in frozenset(i for d in query.ALL_FIELD_LISTS for i in d.keys()):
       self._Test("%s == \"value\"" % name, [qlang.OP_EQUAL, name, "value"])
@@ -167,13 +172,18 @@ class TestParseFilter(unittest.TestCase):
     # Non-matching regexp delimiters
     tests.append("name =~ /foobarbaz#")
 
-    for filter_ in tests:
+    # Invalid operators
+    tests.append("name <> value")
+    tests.append("name => value")
+    tests.append("name =< value")
+
+    for qfilter in tests:
       try:
-        qlang.ParseFilter(filter_, parser=self.parser)
+        qlang.ParseFilter(qfilter, parser=self.parser)
       except errors.QueryFilterParseError, err:
         self.assertEqual(len(err.GetDetails()), 3)
       else:
-        self.fail("Invalid filter '%s' did not raise exception" % filter_)
+        self.fail("Invalid filter '%s' did not raise exception" % qfilter)
 
 
 class TestMakeFilter(unittest.TestCase):
@@ -186,6 +196,12 @@ class TestMakeFilter(unittest.TestCase):
                      [qlang.OP_OR, [qlang.OP_EQUAL, "name", "web1"],
                                    [qlang.OP_EQUAL, "name", "web2"]])
 
+  def testPlainNamesOtherNamefield(self):
+    self.assertEqual(qlang.MakeFilter(["mailA", "mailB"], False,
+                                      namefield="id"),
+                     [qlang.OP_OR, [qlang.OP_EQUAL, "id", "mailA"],
+                                   [qlang.OP_EQUAL, "id", "mailB"]])
+
   def testForcedFilter(self):
     for i in [None, [], ["1", "2"], ["", "", ""], ["a", "b", "c", "d"]]:
       self.assertRaises(errors.OpPrereqError, qlang.MakeFilter, i, True)
index 5618c7f..ca83d1c 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 #
 
-# Copyright (C) 2010, 2011 Google Inc.
+# Copyright (C) 2010, 2011, 2012 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
@@ -222,6 +222,14 @@ class TestQuery(unittest.TestCase):
         None, 0, lambda *args: None),
         ], [])
 
+    # Duplicate field name
+    self.assertRaises(ValueError, query._PrepareFieldList, [
+      (query._MakeField("name", "Name", constants.QFT_TEXT, "Name"),
+       None, 0, lambda *args: None),
+      (query._MakeField("name", "Other", constants.QFT_OTHER, "Other"),
+       None, 0, lambda *args: None),
+      ], [])
+
   def testUnknown(self):
     fielddef = query._PrepareFieldList([
       (query._MakeField("name", "Name", constants.QFT_TEXT, "Name"),
@@ -314,14 +322,30 @@ class TestNodeQuery(unittest.TestCase):
     return query.Query(query.NODE_FIELDS, selected)
 
   def testSimple(self):
+    cluster = objects.Cluster(cluster_name="testcluster",
+                              ndparams=constants.NDC_DEFAULTS.copy())
+    grp1 = objects.NodeGroup(name="default",
+                             uuid="c0e89160-18e7-11e0-a46e-001d0904baeb",
+                             alloc_policy=constants.ALLOC_POLICY_PREFERRED,
+                             ipolicy=objects.MakeEmptyIPolicy(),
+                             ndparams={},
+                             )
+    grp2 = objects.NodeGroup(name="group2",
+                             uuid="c0e89160-18e7-11e0-a46e-001d0904babe",
+                             alloc_policy=constants.ALLOC_POLICY_PREFERRED,
+                             ipolicy=objects.MakeEmptyIPolicy(),
+                             ndparams={constants.ND_SPINDLE_COUNT: 2},
+                             )
+    groups = {grp1.uuid: grp1, grp2.uuid: grp2}
     nodes = [
-      objects.Node(name="node1", drained=False),
-      objects.Node(name="node2", drained=True),
-      objects.Node(name="node3", drained=False),
+      objects.Node(name="node1", drained=False, group=grp1.uuid, ndparams={}),
+      objects.Node(name="node2", drained=True, group=grp2.uuid, ndparams={}),
+      objects.Node(name="node3", drained=False, group=grp1.uuid,
+                   ndparams={constants.ND_SPINDLE_COUNT: 4}),
       ]
     for live_data in [None, dict.fromkeys([node.name for node in nodes], {})]:
-      nqd = query.NodeQueryData(nodes, live_data, None, None, None, None, None,
-                                None)
+      nqd = query.NodeQueryData(nodes, live_data, None, None, None,
+                                groups, None, cluster)
 
       q = self._Create(["name", "drained"])
       self.assertEqual(q.RequestedData(), set([query.NQ_CONFIG]))
@@ -337,6 +361,16 @@ class TestNodeQuery(unittest.TestCase):
                        [["node1", False],
                         ["node2", True],
                         ["node3", False]])
+      q = self._Create(["ndp/spindle_count"])
+      self.assertEqual(q.RequestedData(), set([query.NQ_GROUP]))
+      self.assertEqual(q.Query(nqd),
+                       [[(constants.RS_NORMAL,
+                          constants.NDC_DEFAULTS[constants.ND_SPINDLE_COUNT])],
+                        [(constants.RS_NORMAL,
+                          grp2.ndparams[constants.ND_SPINDLE_COUNT])],
+                        [(constants.RS_NORMAL,
+                          nodes[2].ndparams[constants.ND_SPINDLE_COUNT])],
+                       ])
 
   def test(self):
     selected = query.NODE_FIELDS.keys()
@@ -554,7 +588,7 @@ class TestInstanceQuery(unittest.TestCase):
     return query.Query(query.INSTANCE_FIELDS, selected)
 
   def testSimple(self):
-    q = self._Create(["name", "be/memory", "ip"])
+    q = self._Create(["name", "be/maxmem", "ip"])
     self.assertEqual(q.RequestedData(), set([query.IQ_CONFIG]))
 
     cluster = objects.Cluster(cluster_name="testcluster",
@@ -574,7 +608,7 @@ class TestInstanceQuery(unittest.TestCase):
       objects.Instance(name="inst2", hvparams={}, nics=[], osparams={},
         os="foomoo",
         beparams={
-          constants.BE_MEMORY: 512,
+          constants.BE_MAXMEM: 512,
         }),
       objects.Instance(name="inst3", hvparams={}, beparams={}, osparams={},
         os="dos", nics=[objects.NIC(ip="192.0.2.99", nicparams={})]),
@@ -636,7 +670,8 @@ class TestInstanceQuery(unittest.TestCase):
       objects.Instance(name="inst1", hvparams={}, beparams={}, nics=[],
         uuid="f90eccb3-e227-4e3c-bf2a-94a21ca8f9cd",
         ctime=1291244000, mtime=1291244400, serial_no=30,
-        admin_up=True, hypervisor=constants.HT_XEN_PVM, os="linux1",
+        admin_state=constants.ADMINST_UP, hypervisor=constants.HT_XEN_PVM,
+        os="linux1",
         primary_node="node1",
         disk_template=constants.DT_PLAIN,
         disks=[],
@@ -644,18 +679,21 @@ class TestInstanceQuery(unittest.TestCase):
       objects.Instance(name="inst2", hvparams={}, nics=[],
         uuid="73a0f8a7-068c-4630-ada2-c3440015ab1a",
         ctime=1291211000, mtime=1291211077, serial_no=1,
-        admin_up=True, hypervisor=constants.HT_XEN_HVM, os="deb99",
+        admin_state=constants.ADMINST_UP, hypervisor=constants.HT_XEN_HVM,
+        os="deb99",
         primary_node="node5",
         disk_template=constants.DT_DISKLESS,
         disks=[],
         beparams={
-          constants.BE_MEMORY: 512,
+          constants.BE_MAXMEM: 512,
+          constants.BE_MINMEM: 256,
         },
         osparams={}),
       objects.Instance(name="inst3", hvparams={}, beparams={},
         uuid="11ec8dff-fb61-4850-bfe0-baa1803ff280",
         ctime=1291011000, mtime=1291013000, serial_no=1923,
-        admin_up=False, hypervisor=constants.HT_KVM, os="busybox",
+        admin_state=constants.ADMINST_DOWN, hypervisor=constants.HT_KVM,
+        os="busybox",
         primary_node="node6",
         disk_template=constants.DT_DRBD8,
         disks=[],
@@ -670,7 +708,8 @@ class TestInstanceQuery(unittest.TestCase):
       objects.Instance(name="inst4", hvparams={}, beparams={},
         uuid="68dab168-3ef5-4c9d-b4d3-801e0672068c",
         ctime=1291244390, mtime=1291244395, serial_no=25,
-        admin_up=False, hypervisor=constants.HT_XEN_PVM, os="linux1",
+        admin_state=constants.ADMINST_DOWN, hypervisor=constants.HT_XEN_PVM,
+        os="linux1",
         primary_node="nodeoff2",
         disk_template=constants.DT_DRBD8,
         disks=[],
@@ -694,23 +733,27 @@ class TestInstanceQuery(unittest.TestCase):
       objects.Instance(name="inst5", hvparams={}, nics=[],
         uuid="0e3dca12-5b42-4e24-98a2-415267545bd0",
         ctime=1231211000, mtime=1261200000, serial_no=3,
-        admin_up=True, hypervisor=constants.HT_XEN_HVM, os="deb99",
+        admin_state=constants.ADMINST_UP, hypervisor=constants.HT_XEN_HVM,
+        os="deb99",
         primary_node="nodebad2",
         disk_template=constants.DT_DISKLESS,
         disks=[],
         beparams={
-          constants.BE_MEMORY: 512,
+          constants.BE_MAXMEM: 512,
+          constants.BE_MINMEM: 512,
         },
         osparams={}),
       objects.Instance(name="inst6", hvparams={}, nics=[],
         uuid="72de6580-c8d5-4661-b902-38b5785bb8b3",
         ctime=7513, mtime=11501, serial_no=13390,
-        admin_up=False, hypervisor=constants.HT_XEN_HVM, os="deb99",
+        admin_state=constants.ADMINST_DOWN, hypervisor=constants.HT_XEN_HVM,
+        os="deb99",
         primary_node="node7",
         disk_template=constants.DT_DISKLESS,
         disks=[],
         beparams={
-          constants.BE_MEMORY: 768,
+          constants.BE_MAXMEM: 768,
+          constants.BE_MINMEM: 256,
         },
         osparams={
           "clean_install": "no",
@@ -718,7 +761,18 @@ class TestInstanceQuery(unittest.TestCase):
       objects.Instance(name="inst7", hvparams={}, nics=[],
         uuid="ceec5dc4-b729-4f42-ae28-69b3cd24920e",
         ctime=None, mtime=None, serial_no=1947,
-        admin_up=False, hypervisor=constants.HT_XEN_HVM, os="deb99",
+        admin_state=constants.ADMINST_DOWN, hypervisor=constants.HT_XEN_HVM,
+        os="deb99",
+        primary_node="node6",
+        disk_template=constants.DT_DISKLESS,
+        disks=[],
+        beparams={},
+        osparams={}),
+      objects.Instance(name="inst8", hvparams={}, nics=[],
+        uuid="ceec5dc4-b729-4f42-ae28-69b3cd24920f",
+        ctime=None, mtime=None, serial_no=19478,
+        admin_state=constants.ADMINST_OFFLINE, hypervisor=constants.HT_XEN_HVM,
+        os="deb99",
         primary_node="node6",
         disk_template=constants.DT_DISKLESS,
         disks=[],
@@ -791,14 +845,16 @@ class TestInstanceQuery(unittest.TestCase):
       elif inst.name in live_data:
         if inst.name in wrongnode_inst:
           exp_status = constants.INSTST_WRONGNODE
-        elif inst.admin_up:
+        elif inst.admin_state == constants.ADMINST_UP:
           exp_status = constants.INSTST_RUNNING
         else:
           exp_status = constants.INSTST_ERRORUP
-      elif inst.admin_up:
+      elif inst.admin_state == constants.ADMINST_UP:
         exp_status = constants.INSTST_ERRORDOWN
-      else:
+      elif inst.admin_state == constants.ADMINST_DOWN:
         exp_status = constants.INSTST_ADMINDOWN
+      else:
+        exp_status = constants.INSTST_ADMINOFFLINE
 
       self.assertEqual(row[fieldidx["status"]],
                        (constants.RS_NORMAL, exp_status))
@@ -806,8 +862,8 @@ class TestInstanceQuery(unittest.TestCase):
       (_, status) = row[fieldidx["status"]]
       tested_status.add(status)
 
-      for (field, livefield) in [("oper_ram", "memory"),
-                                 ("oper_vcpus", "vcpus")]:
+      #FIXME(dynmem): check oper_ram vs min/max mem
+      for (field, livefield) in [("oper_vcpus", "vcpus")]:
         if inst.primary_node in bad_nodes:
           exp = (constants.RS_NODATA, None)
         elif inst.name in live_data:
@@ -899,21 +955,47 @@ class TestInstanceQuery(unittest.TestCase):
 class TestGroupQuery(unittest.TestCase):
 
   def setUp(self):
+    self.custom_diskparams = {
+      constants.DT_DRBD8: {
+        constants.DRBD_DEFAULT_METAVG: "foobar",
+      },
+    }
+
     self.groups = [
       objects.NodeGroup(name="default",
                         uuid="c0e89160-18e7-11e0-a46e-001d0904baeb",
-                        alloc_policy=constants.ALLOC_POLICY_PREFERRED),
+                        alloc_policy=constants.ALLOC_POLICY_PREFERRED,
+                        ipolicy=objects.MakeEmptyIPolicy(),
+                        ndparams={},
+                        diskparams={},
+                        ),
       objects.NodeGroup(name="restricted",
                         uuid="d2a40a74-18e7-11e0-9143-001d0904baeb",
-                        alloc_policy=constants.ALLOC_POLICY_LAST_RESORT),
+                        alloc_policy=constants.ALLOC_POLICY_LAST_RESORT,
+                        ipolicy=objects.MakeEmptyIPolicy(),
+                        ndparams={},
+                        diskparams=self.custom_diskparams,
+                        ),
       ]
+    self.cluster = objects.Cluster(cluster_name="testcluster",
+      hvparams=constants.HVC_DEFAULTS,
+      beparams={
+        constants.PP_DEFAULT: constants.BEC_DEFAULTS,
+        },
+      nicparams={
+        constants.PP_DEFAULT: constants.NICC_DEFAULTS,
+        },
+      ndparams=constants.NDC_DEFAULTS,
+      ipolicy=constants.IPOLICY_DEFAULTS,
+      diskparams=constants.DISK_DT_DEFAULTS,
+      )
 
   def _Create(self, selected):
     return query.Query(query.GROUP_FIELDS, selected)
 
   def testSimple(self):
     q = self._Create(["name", "uuid", "alloc_policy"])
-    gqd = query.GroupQueryData(self.groups, None, None)
+    gqd = query.GroupQueryData(self.cluster, self.groups, None, None, False)
 
     self.assertEqual(q.RequestedData(), set([query.GQ_CONFIG]))
 
@@ -935,7 +1017,8 @@ class TestGroupQuery(unittest.TestCase):
       }
 
     q = self._Create(["name", "node_cnt", "node_list"])
-    gqd = query.GroupQueryData(self.groups, groups_to_nodes, None)
+    gqd = query.GroupQueryData(self.cluster, self.groups, groups_to_nodes, None,
+                               False)
 
     self.assertEqual(q.RequestedData(), set([query.GQ_CONFIG, query.GQ_NODE]))
 
@@ -957,7 +1040,8 @@ class TestGroupQuery(unittest.TestCase):
       }
 
     q = self._Create(["pinst_cnt", "pinst_list"])
-    gqd = query.GroupQueryData(self.groups, None, groups_to_instances)
+    gqd = query.GroupQueryData(self.cluster, self.groups, None,
+      groups_to_instances, False)
 
     self.assertEqual(q.RequestedData(), set([query.GQ_INST]))
 
@@ -970,6 +1054,27 @@ class TestGroupQuery(unittest.TestCase):
                        ],
                       ])
 
+  def testDiskparams(self):
+    q = self._Create(["name", "uuid", "diskparams", "custom_diskparams"])
+    gqd = query.GroupQueryData(self.cluster, self.groups, None, None, True)
+
+    self.assertEqual(q.RequestedData(),
+                     set([query.GQ_CONFIG, query.GQ_DISKPARAMS]))
+
+    self.assertEqual(q.Query(gqd),
+      [[(constants.RS_NORMAL, "default"),
+        (constants.RS_NORMAL, "c0e89160-18e7-11e0-a46e-001d0904baeb"),
+        (constants.RS_NORMAL, constants.DISK_DT_DEFAULTS),
+        (constants.RS_NORMAL, {}),
+        ],
+       [(constants.RS_NORMAL, "restricted"),
+        (constants.RS_NORMAL, "d2a40a74-18e7-11e0-9143-001d0904baeb"),
+        (constants.RS_NORMAL, objects.FillDiskParams(constants.DISK_DT_DEFAULTS,
+                                                     self.custom_diskparams)),
+        (constants.RS_NORMAL, self.custom_diskparams),
+        ],
+       ])
+
 
 class TestOsQuery(unittest.TestCase):
   def _Create(self, selected):
@@ -1054,72 +1159,80 @@ class TestQueryFields(unittest.TestCase):
 
 class TestQueryFilter(unittest.TestCase):
   def testRequestedNames(self):
-    innerfilter = [["=", "name", "x%s" % i] for i in range(4)]
+    for (what, fielddefs) in query.ALL_FIELDS.items():
+      if what == constants.QR_JOB:
+        namefield = "id"
+      elif what == constants.QR_EXPORT:
+        namefield = "export"
+      else:
+        namefield = "name"
 
-    for fielddefs in query.ALL_FIELD_LISTS:
-      assert "name" in fielddefs
+      assert namefield in fielddefs
+
+      innerfilter = [["=", namefield, "x%s" % i] for i in range(4)]
 
       # No name field
-      q = query.Query(fielddefs, ["name"], filter_=["=", "name", "abc"],
+      q = query.Query(fielddefs, [namefield], qfilter=["=", namefield, "abc"],
                       namefield=None)
       self.assertEqual(q.RequestedNames(), None)
 
       # No filter
-      q = query.Query(fielddefs, ["name"], filter_=None, namefield="name")
+      q = query.Query(fielddefs, [namefield], qfilter=None, namefield=namefield)
       self.assertEqual(q.RequestedNames(), None)
 
       # Check empty query
-      q = query.Query(fielddefs, ["name"], filter_=["|"], namefield="name")
+      q = query.Query(fielddefs, [namefield], qfilter=["|"],
+                      namefield=namefield)
       self.assertEqual(q.RequestedNames(), None)
 
       # Check order
-      q = query.Query(fielddefs, ["name"], filter_=["|"] + innerfilter,
-                      namefield="name")
+      q = query.Query(fielddefs, [namefield], qfilter=["|"] + innerfilter,
+                      namefield=namefield)
       self.assertEqual(q.RequestedNames(), ["x0", "x1", "x2", "x3"])
 
       # Check reverse order
-      q = query.Query(fielddefs, ["name"],
-                      filter_=["|"] + list(reversed(innerfilter)),
-                      namefield="name")
+      q = query.Query(fielddefs, [namefield],
+                      qfilter=["|"] + list(reversed(innerfilter)),
+                      namefield=namefield)
       self.assertEqual(q.RequestedNames(), ["x3", "x2", "x1", "x0"])
 
       # Duplicates
-      q = query.Query(fielddefs, ["name"],
-                      filter_=["|"] + innerfilter + list(reversed(innerfilter)),
-                      namefield="name")
+      q = query.Query(fielddefs, [namefield],
+                      qfilter=["|"] + innerfilter + list(reversed(innerfilter)),
+                      namefield=namefield)
       self.assertEqual(q.RequestedNames(), ["x0", "x1", "x2", "x3"])
 
       # Unknown name field
-      self.assertRaises(AssertionError, query.Query, fielddefs, ["name"],
+      self.assertRaises(AssertionError, query.Query, fielddefs, [namefield],
                         namefield="_unknown_field_")
 
       # Filter with AND
-      q = query.Query(fielddefs, ["name"],
-                      filter_=["|", ["=", "name", "foo"],
-                                    ["&", ["=", "name", ""]]],
-                      namefield="name")
+      q = query.Query(fielddefs, [namefield],
+                      qfilter=["|", ["=", namefield, "foo"],
+                                    ["&", ["=", namefield, ""]]],
+                      namefield=namefield)
       self.assertTrue(q.RequestedNames() is None)
 
       # Filter with NOT
-      q = query.Query(fielddefs, ["name"],
-                      filter_=["|", ["=", "name", "foo"],
-                                    ["!", ["=", "name", ""]]],
-                      namefield="name")
+      q = query.Query(fielddefs, [namefield],
+                      qfilter=["|", ["=", namefield, "foo"],
+                                    ["!", ["=", namefield, ""]]],
+                      namefield=namefield)
       self.assertTrue(q.RequestedNames() is None)
 
       # Filter with only OR (names must be in correct order)
-      q = query.Query(fielddefs, ["name"],
-                      filter_=["|", ["=", "name", "x17361"],
-                                    ["|", ["=", "name", "x22015"]],
-                                    ["|", ["|", ["=", "name", "x13193"]]],
-                                    ["=", "name", "x15215"]],
-                      namefield="name")
+      q = query.Query(fielddefs, [namefield],
+                      qfilter=["|", ["=", namefield, "x17361"],
+                                    ["|", ["=", namefield, "x22015"]],
+                                    ["|", ["|", ["=", namefield, "x13193"]]],
+                                    ["=", namefield, "x15215"]],
+                      namefield=namefield)
       self.assertEqual(q.RequestedNames(),
                        ["x17361", "x22015", "x13193", "x15215"])
 
   @staticmethod
-  def _GenNestedFilter(op, depth):
-    nested = ["=", "name", "value"]
+  def _GenNestedFilter(namefield, op, depth):
+    nested = ["=", namefield, "value"]
     for i in range(depth):
       nested = [op, nested]
     return nested
@@ -1127,23 +1240,30 @@ class TestQueryFilter(unittest.TestCase):
   def testCompileFilter(self):
     levels_max = query._FilterCompilerHelper._LEVELS_MAX
 
-    checks = [
-      [], ["="], ["=", "foo"], ["unknownop"], ["!"],
-      ["=", "_unknown_field", "value"],
-      self._GenNestedFilter("|", levels_max),
-      self._GenNestedFilter("|", levels_max * 3),
-      self._GenNestedFilter("!", levels_max),
-      ]
+    for (what, fielddefs) in query.ALL_FIELDS.items():
+      if what == constants.QR_JOB:
+        namefield = "id"
+      elif what == constants.QR_EXPORT:
+        namefield = "export"
+      else:
+        namefield = "name"
 
-    for fielddefs in query.ALL_FIELD_LISTS:
-      for filter_ in checks:
+      checks = [
+        [], ["="], ["=", "foo"], ["unknownop"], ["!"],
+        ["=", "_unknown_field", "value"],
+        self._GenNestedFilter(namefield, "|", levels_max),
+        self._GenNestedFilter(namefield, "|", levels_max * 3),
+        self._GenNestedFilter(namefield, "!", levels_max),
+        ]
+
+      for qfilter in checks:
         self.assertRaises(errors.ParameterError, query._CompileFilter,
-                          fielddefs, None, filter_)
+                          fielddefs, None, qfilter)
 
       for op in ["|", "!"]:
-        filter_ = self._GenNestedFilter(op, levels_max - 1)
+        qfilter = self._GenNestedFilter(namefield, op, levels_max - 1)
         self.assertTrue(callable(query._CompileFilter(fielddefs, None,
-                                                      filter_)))
+                                                      qfilter)))
 
   def testQueryInputOrder(self):
     fielddefs = query._PrepareFieldList([
@@ -1160,10 +1280,10 @@ class TestQueryFilter(unittest.TestCase):
       { "pnode": "node20", "snode": "node1", },
       ]
 
-    filter_ = ["|", ["=", "pnode", "node1"], ["=", "snode", "node1"]]
+    qfilter = ["|", ["=", "pnode", "node1"], ["=", "snode", "node1"]]
 
     q = query.Query(fielddefs, ["pnode", "snode"], namefield="pnode",
-                    filter_=filter_)
+                    qfilter=qfilter)
     self.assertTrue(q.RequestedNames() is None)
     self.assertFalse(q.RequestedData())
     self.assertEqual(q.Query(data),
@@ -1179,7 +1299,7 @@ class TestQueryFilter(unittest.TestCase):
 
     # No name field, result must be in incoming order
     q = query.Query(fielddefs, ["pnode", "snode"], namefield=None,
-                    filter_=filter_)
+                    qfilter=qfilter)
     self.assertFalse(q.RequestedData())
     self.assertEqual(q.Query(data),
       [[(constants.RS_NORMAL, "node1"), (constants.RS_NORMAL, "node44")],
@@ -1242,7 +1362,7 @@ class TestQueryFilter(unittest.TestCase):
       ]
 
     q = query.Query(fielddefs, ["pnode", "num"], namefield="pnode",
-                    filter_=["|", ["=", "pnode", "node1"],
+                    qfilter=["|", ["=", "pnode", "node1"],
                                   ["=", "pnode", "node2"],
                                   ["=", "pnode", "node1"]])
     self.assertEqual(q.RequestedNames(), ["node1", "node2"],
@@ -1268,7 +1388,7 @@ class TestQueryFilter(unittest.TestCase):
       ]
 
     q = query.Query(fielddefs, ["pnode", "num"], namefield="pnode",
-                    filter_=["|", ["=", "pnode", "nodeX"],
+                    qfilter=["|", ["=", "pnode", "nodeX"],
                                   ["=", "pnode", "nodeY"],
                                   ["=", "pnode", "nodeY"],
                                   ["=", "pnode", "nodeY"],
@@ -1311,20 +1431,20 @@ class TestQueryFilter(unittest.TestCase):
 
     # Empty filter
     q = query.Query(fielddefs, ["name", "other"], namefield="name",
-                    filter_=["|"])
+                    qfilter=["|"])
     self.assertTrue(q.RequestedNames() is None)
     self.assertEqual(q.RequestedData(), set([DK_A, DK_B]))
     self.assertEqual(q.Query(data), [])
 
     # Normal filter
     q = query.Query(fielddefs, ["name", "other"], namefield="name",
-                    filter_=["=", "name", "node1"])
+                    qfilter=["=", "name", "node1"])
     self.assertEqual(q.RequestedNames(), ["node1"])
     self.assertEqual(q.Query(data),
       [[(constants.RS_NORMAL, "node1"), (constants.RS_NORMAL, "foo")]])
 
     q = query.Query(fielddefs, ["name", "other"], namefield="name",
-                    filter_=(["|", ["=", "name", "node1"],
+                    qfilter=(["|", ["=", "name", "node1"],
                                    ["=", "name", "node3"]]))
     self.assertEqual(q.RequestedNames(), ["node1", "node3"])
     self.assertEqual(q.Query(data),
@@ -1333,7 +1453,7 @@ class TestQueryFilter(unittest.TestCase):
 
     # Complex filter
     q = query.Query(fielddefs, ["name", "other"], namefield="name",
-                    filter_=(["|", ["=", "name", "node1"],
+                    qfilter=(["|", ["=", "name", "node1"],
                                    ["|", ["=", "name", "node3"],
                                          ["=", "name", "node2"]],
                                    ["=", "name", "node3"]]))
@@ -1348,11 +1468,11 @@ class TestQueryFilter(unittest.TestCase):
     for i in [-1, 0, 1, 123, [], None, True, False]:
       self.assertRaises(errors.ParameterError, query.Query,
                         fielddefs, ["name", "other"], namefield="name",
-                        filter_=["=", "name", i])
+                        qfilter=["=", "name", i])
 
     # Negative filter
     q = query.Query(fielddefs, ["name", "other"], namefield="name",
-                    filter_=["!", ["|", ["=", "name", "node1"],
+                    qfilter=["!", ["|", ["=", "name", "node1"],
                                         ["=", "name", "node3"]]])
     self.assertTrue(q.RequestedNames() is None)
     self.assertEqual(q.Query(data),
@@ -1360,7 +1480,7 @@ class TestQueryFilter(unittest.TestCase):
 
     # Not equal
     q = query.Query(fielddefs, ["name", "other"], namefield="name",
-                    filter_=["!=", "name", "node3"])
+                    qfilter=["!=", "name", "node3"])
     self.assertTrue(q.RequestedNames() is None)
     self.assertEqual(q.Query(data),
       [[(constants.RS_NORMAL, "node1"), (constants.RS_NORMAL, "foo")],
@@ -1368,7 +1488,7 @@ class TestQueryFilter(unittest.TestCase):
 
     # Data type
     q = query.Query(fielddefs, [], namefield="name",
-                    filter_=["|", ["=", "other", "bar"],
+                    qfilter=["|", ["=", "other", "bar"],
                                   ["=", "name", "foo"]])
     self.assertTrue(q.RequestedNames() is None)
     self.assertEqual(q.RequestedData(), set([DK_A, DK_B]))
@@ -1376,13 +1496,13 @@ class TestQueryFilter(unittest.TestCase):
 
     # Only one data type
     q = query.Query(fielddefs, ["other"], namefield="name",
-                    filter_=["=", "other", "bar"])
+                    qfilter=["=", "other", "bar"])
     self.assertTrue(q.RequestedNames() is None)
     self.assertEqual(q.RequestedData(), set([DK_B]))
     self.assertEqual(q.Query(data), [[(constants.RS_NORMAL, "bar")]])
 
     q = query.Query(fielddefs, [], namefield="name",
-                    filter_=["=", "other", "bar"])
+                    qfilter=["=", "other", "bar"])
     self.assertTrue(q.RequestedNames() is None)
     self.assertEqual(q.RequestedData(), set([DK_B]))
     self.assertEqual(q.Query(data), [[]])
@@ -1403,7 +1523,7 @@ class TestQueryFilter(unittest.TestCase):
       ]
 
     q = query.Query(fielddefs, ["name", "other"], namefield="name",
-                    filter_=["=[]", "other", "bar"])
+                    qfilter=["=[]", "other", "bar"])
     self.assertTrue(q.RequestedNames() is None)
     self.assertEqual(q.Query(data), [
       [(constants.RS_NORMAL, "node2"),
@@ -1411,7 +1531,7 @@ class TestQueryFilter(unittest.TestCase):
       ])
 
     q = query.Query(fielddefs, ["name", "other"], namefield="name",
-                    filter_=["|", ["=[]", "other", "bar"],
+                    qfilter=["|", ["=[]", "other", "bar"],
                                   ["=[]", "other", "a"],
                                   ["=[]", "other", "b"]])
     self.assertTrue(q.RequestedNames() is None)
@@ -1428,7 +1548,7 @@ class TestQueryFilter(unittest.TestCase):
 
     # Boolean test
     q = query.Query(fielddefs, ["name", "other"], namefield="name",
-                    filter_=["?", "other"])
+                    qfilter=["?", "other"])
     self.assertEqual(q.OldStyleQuery(data), [
       ["node1", ["a", "b", "foo"]],
       ["node2", ["x", "y", "bar"]],
@@ -1436,7 +1556,7 @@ class TestQueryFilter(unittest.TestCase):
       ])
 
     q = query.Query(fielddefs, ["name", "other"], namefield="name",
-                    filter_=["!", ["?", "other"]])
+                    qfilter=["!", ["?", "other"]])
     self.assertEqual(q.OldStyleQuery(data), [
       ["empty", []],
       ])
@@ -1454,7 +1574,7 @@ class TestQueryFilter(unittest.TestCase):
       ]
 
     q = query.Query(fielddefs, ["name"], namefield="name",
-                    filter_=["=", "name", "node2"])
+                    qfilter=["=", "name", "node2"])
     self.assertEqual(q.RequestedNames(), ["node2"])
     self.assertEqual(q.Query(data), [
       [(constants.RS_NORMAL, "node2.example.com")],
@@ -1462,19 +1582,19 @@ class TestQueryFilter(unittest.TestCase):
       ])
 
     q = query.Query(fielddefs, ["name"], namefield="name",
-                    filter_=["=", "name", "node1"])
+                    qfilter=["=", "name", "node1"])
     self.assertEqual(q.RequestedNames(), ["node1"])
     self.assertEqual(q.Query(data), [
       [(constants.RS_NORMAL, "node1.example.com")],
       ])
 
     q = query.Query(fielddefs, ["name"], namefield="name",
-                    filter_=["=", "name", "othername"])
+                    qfilter=["=", "name", "othername"])
     self.assertEqual(q.RequestedNames(), ["othername"])
     self.assertEqual(q.Query(data), [])
 
     q = query.Query(fielddefs, ["name"], namefield="name",
-                    filter_=["|", ["=", "name", "node1.example.com"],
+                    qfilter=["|", ["=", "name", "node1.example.com"],
                                   ["=", "name", "node2"]])
     self.assertEqual(q.RequestedNames(), ["node1.example.com", "node2"])
     self.assertEqual(q.Query(data), [
@@ -1489,7 +1609,7 @@ class TestQueryFilter(unittest.TestCase):
       ])
 
     q = query.Query(fielddefs, ["name"], namefield="name",
-                    filter_=["!=", "name", "node1"])
+                    qfilter=["!=", "name", "node1"])
     self.assertTrue(q.RequestedNames() is None)
     self.assertEqual(q.Query(data), [
       [(constants.RS_NORMAL, "node2.example.com")],
@@ -1515,7 +1635,7 @@ class TestQueryFilter(unittest.TestCase):
       ]
 
     q = query.Query(fielddefs, ["name", "value"],
-                    filter_=["|", ["=", "value", False],
+                    qfilter=["|", ["=", "value", False],
                                   ["=", "value", True]])
     self.assertTrue(q.RequestedNames() is None)
     self.assertEqual(q.Query(data), [
@@ -1525,7 +1645,7 @@ class TestQueryFilter(unittest.TestCase):
       ])
 
     q = query.Query(fielddefs, ["name", "value"],
-                    filter_=["|", ["=", "value", False],
+                    qfilter=["|", ["=", "value", False],
                                   ["!", ["=", "value", False]]])
     self.assertTrue(q.RequestedNames() is None)
     self.assertEqual(q.Query(data), [
@@ -1538,10 +1658,10 @@ class TestQueryFilter(unittest.TestCase):
     for i in ["False", "True", "0", "1", "no", "yes", "N", "Y"]:
       self.assertRaises(errors.ParameterError, query.Query,
                         fielddefs, ["name", "value"],
-                        filter_=["=", "value", i])
+                        qfilter=["=", "value", i])
 
     # Truth filter
-    q = query.Query(fielddefs, ["name", "value"], filter_=["?", "value"])
+    q = query.Query(fielddefs, ["name", "value"], qfilter=["?", "value"])
     self.assertTrue(q.RequestedNames() is None)
     self.assertEqual(q.Query(data), [
       [(constants.RS_NORMAL, "node2"), (constants.RS_NORMAL, True)],
@@ -1549,7 +1669,7 @@ class TestQueryFilter(unittest.TestCase):
       ])
 
     # Negative bool filter
-    q = query.Query(fielddefs, ["name", "value"], filter_=["!", ["?", "value"]])
+    q = query.Query(fielddefs, ["name", "value"], qfilter=["!", ["?", "value"]])
     self.assertTrue(q.RequestedNames() is None)
     self.assertEqual(q.Query(data), [
       [(constants.RS_NORMAL, "node1"), (constants.RS_NORMAL, False)],
@@ -1557,7 +1677,7 @@ class TestQueryFilter(unittest.TestCase):
 
     # Complex truth filter
     q = query.Query(fielddefs, ["name", "value"],
-                    filter_=["|", ["&", ["=", "name", "node1"],
+                    qfilter=["|", ["&", ["=", "name", "node1"],
                                         ["!", ["?", "value"]]],
                                   ["?", "value"]])
     self.assertTrue(q.RequestedNames() is None)
@@ -1583,14 +1703,14 @@ class TestQueryFilter(unittest.TestCase):
       ]
 
     q = query.Query(fielddefs, ["name"], namefield="name",
-                    filter_=["=~", "name", "site"])
+                    qfilter=["=~", "name", "site"])
     self.assertTrue(q.RequestedNames() is None)
     self.assertEqual(q.Query(data), [
       [(constants.RS_NORMAL, "node2.site.example.com")],
       ])
 
     q = query.Query(fielddefs, ["name"], namefield="name",
-                    filter_=["=~", "name", "^node2"])
+                    qfilter=["=~", "name", "^node2"])
     self.assertTrue(q.RequestedNames() is None)
     self.assertEqual(q.Query(data), [
       [(constants.RS_NORMAL, "node2.example.net")],
@@ -1598,7 +1718,7 @@ class TestQueryFilter(unittest.TestCase):
       ])
 
     q = query.Query(fielddefs, ["name"], namefield="name",
-                    filter_=["=~", "name", r"(?i)\.COM$"])
+                    qfilter=["=~", "name", r"(?i)\.COM$"])
     self.assertTrue(q.RequestedNames() is None)
     self.assertEqual(q.Query(data), [
       [(constants.RS_NORMAL, "node1.example.com")],
@@ -1606,7 +1726,7 @@ class TestQueryFilter(unittest.TestCase):
       ])
 
     q = query.Query(fielddefs, ["name"], namefield="name",
-                    filter_=["=~", "name", r"."])
+                    qfilter=["=~", "name", r"."])
     self.assertTrue(q.RequestedNames() is None)
     self.assertEqual(q.Query(data), [
       [(constants.RS_NORMAL, "node1.example.com")],
@@ -1615,7 +1735,7 @@ class TestQueryFilter(unittest.TestCase):
       ])
 
     q = query.Query(fielddefs, ["name"], namefield="name",
-                    filter_=["=~", "name", r"^$"])
+                    qfilter=["=~", "name", r"^$"])
     self.assertTrue(q.RequestedNames() is None)
     self.assertEqual(q.Query(data), [
       [(constants.RS_NORMAL, "")],
@@ -1623,7 +1743,39 @@ class TestQueryFilter(unittest.TestCase):
 
     # Invalid regular expression
     self.assertRaises(errors.ParameterError, query.Query, fielddefs, ["name"],
-                      filter_=["=~", "name", r"["])
+                      qfilter=["=~", "name", r"["])
+
+  def testFilterLessGreater(self):
+    fielddefs = query._PrepareFieldList([
+      (query._MakeField("value", "Value", constants.QFT_NUMBER, "Value"),
+       None, 0, lambda ctx, item: item),
+      ], [])
+
+    data = range(100)
+
+    q = query.Query(fielddefs, ["value"],
+                    qfilter=["<", "value", 20])
+    self.assertTrue(q.RequestedNames() is None)
+    self.assertEqual(q.Query(data),
+                     [[(constants.RS_NORMAL, i)] for i in range(20)])
+
+    q = query.Query(fielddefs, ["value"],
+                    qfilter=["<=", "value", 30])
+    self.assertTrue(q.RequestedNames() is None)
+    self.assertEqual(q.Query(data),
+                     [[(constants.RS_NORMAL, i)] for i in range(31)])
+
+    q = query.Query(fielddefs, ["value"],
+                    qfilter=[">", "value", 40])
+    self.assertTrue(q.RequestedNames() is None)
+    self.assertEqual(q.Query(data),
+                     [[(constants.RS_NORMAL, i)] for i in range(41, 100)])
+
+    q = query.Query(fielddefs, ["value"],
+                    qfilter=[">=", "value", 50])
+    self.assertTrue(q.RequestedNames() is None)
+    self.assertEqual(q.Query(data),
+                     [[(constants.RS_NORMAL, i)] for i in range(50, 100)])
 
 
 if __name__ == "__main__":
index fc29cce..0582cb7 100755 (executable)
 """Script for testing ganeti.rapi.baserlib"""
 
 import unittest
+import itertools
 
 from ganeti import errors
 from ganeti import opcodes
 from ganeti import ht
 from ganeti import http
+from ganeti import compat
 from ganeti.rapi import baserlib
 
 import testutils
@@ -97,5 +99,63 @@ class TestFillOpcode(unittest.TestCase):
                       rename={ "data": "test", })
 
 
+class TestOpcodeResource(unittest.TestCase):
+  @staticmethod
+  def _MakeClass(method, attrs):
+    return type("Test%s" % method, (baserlib.OpcodeResource, ), attrs)
+
+  @staticmethod
+  def _GetMethodAttributes(method):
+    attrs = ["%s_OPCODE" % method, "%s_RENAME" % method,
+             "Get%sOpInput" % method.capitalize()]
+    assert attrs == dict((opattrs[0], list(opattrs[1:]))
+                         for opattrs in baserlib._OPCODE_ATTRS)[method]
+    return attrs
+
+  def test(self):
+    for method in baserlib._SUPPORTED_METHODS:
+      # Empty handler
+      obj = self._MakeClass(method, {})(None, None, None)
+      for attr in itertools.chain(*baserlib._OPCODE_ATTRS):
+        self.assertFalse(hasattr(obj, attr))
+
+      # Direct handler function
+      obj = self._MakeClass(method, {
+        method: lambda _: None,
+        })(None, None, None)
+      self.assertFalse(compat.all(hasattr(obj, attr)
+                                  for i in baserlib._SUPPORTED_METHODS
+                                  for attr in self._GetMethodAttributes(i)))
+
+      # Let metaclass define handler function
+      for opcls in [None, object()]:
+        obj = self._MakeClass(method, {
+          "%s_OPCODE" % method: opcls,
+          })(None, None, None)
+        self.assertTrue(callable(getattr(obj, method)))
+        self.assertEqual(getattr(obj, "%s_OPCODE" % method), opcls)
+        self.assertFalse(hasattr(obj, "%s_RENAME" % method))
+        self.assertFalse(compat.any(hasattr(obj, attr)
+                                    for i in baserlib._SUPPORTED_METHODS
+                                      if i != method
+                                    for attr in self._GetMethodAttributes(i)))
+
+  def testIllegalRename(self):
+    class _TClass(baserlib.OpcodeResource):
+      PUT_RENAME = None
+      def PUT(self): pass
+
+    self.assertRaises(AssertionError, _TClass, None, None, None)
+
+  def testEmpty(self):
+    class _Empty(baserlib.OpcodeResource):
+      pass
+
+    obj = _Empty(None, None, None)
+
+    for attr in itertools.chain(*baserlib._OPCODE_ATTRS):
+      self.assertFalse(hasattr(obj, attr))
+
+
 if __name__ == "__main__":
   testutils.GanetiTestProgram()
index eb22d75..a09d4a2 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 #
 
-# Copyright (C) 2010 Google Inc.
+# 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
@@ -22,7 +22,6 @@
 """Script for unittesting the RAPI client module"""
 
 
-import re
 import unittest
 import warnings
 import pycurl
@@ -33,7 +32,9 @@ from ganeti import serializer
 from ganeti import utils
 from ganeti import query
 from ganeti import objects
+from ganeti import rapi
 
+import ganeti.rapi.testutils
 from ganeti.rapi import connector
 from ganeti.rapi import rlib2
 from ganeti.rapi import client
@@ -41,61 +42,16 @@ from ganeti.rapi import client
 import testutils
 
 
-_URI_RE = re.compile(r"https://(?P<host>.*):(?P<port>\d+)(?P<path>/.*)")
-
 # List of resource handlers which aren't used by the RAPI client
 _KNOWN_UNUSED = set([
-  connector.R_root,
-  connector.R_2,
+  rlib2.R_root,
+  rlib2.R_2,
   ])
 
 # Global variable for collecting used handlers
 _used_handlers = None
 
 
-def _GetPathFromUri(uri):
-  """Gets the path and query from a URI.
-
-  """
-  match = _URI_RE.match(uri)
-  if match:
-    return match.groupdict()["path"]
-  else:
-    return None
-
-
-class FakeCurl:
-  def __init__(self, rapi):
-    self._rapi = rapi
-    self._opts = {}
-    self._info = {}
-
-  def setopt(self, opt, value):
-    self._opts[opt] = value
-
-  def getopt(self, opt):
-    return self._opts.get(opt)
-
-  def unsetopt(self, opt):
-    self._opts.pop(opt, None)
-
-  def getinfo(self, info):
-    return self._info[info]
-
-  def perform(self):
-    method = self._opts[pycurl.CUSTOMREQUEST]
-    url = self._opts[pycurl.URL]
-    request_body = self._opts[pycurl.POSTFIELDS]
-    writefn = self._opts[pycurl.WRITEFUNCTION]
-
-    path = _GetPathFromUri(url)
-    (code, resp_body) = self._rapi.FetchResponse(path, method, request_body)
-
-    self._info[pycurl.RESPONSE_CODE] = code
-    if resp_body is not None:
-      writefn(resp_body)
-
-
 class RapiMock(object):
   def __init__(self):
     self._mapper = connector.Mapper()
@@ -118,7 +74,7 @@ class RapiMock(object):
   def GetLastRequestData(self):
     return self._last_req_data
 
-  def FetchResponse(self, path, method, request_body):
+  def FetchResponse(self, path, method, headers, request_body):
     self._last_req_data = request_body
 
     try:
@@ -149,11 +105,6 @@ class TestConstants(unittest.TestCase):
     self.assertEqual(client.GANETI_RAPI_VERSION, constants.RAPI_VERSION)
     self.assertEqual(client.HTTP_APP_JSON, http.HTTP_APP_JSON)
     self.assertEqual(client._REQ_DATA_VERSION_FIELD, rlib2._REQ_DATA_VERSION)
-    self.assertEqual(client._INST_CREATE_REQV1, rlib2._INST_CREATE_REQV1)
-    self.assertEqual(client._INST_REINSTALL_REQV1, rlib2._INST_REINSTALL_REQV1)
-    self.assertEqual(client._NODE_MIGRATE_REQV1, rlib2._NODE_MIGRATE_REQV1)
-    self.assertEqual(client._NODE_EVAC_RES1, rlib2._NODE_EVAC_RES1)
-    self.assertEqual(client._INST_NIC_PARAMS, constants.INIC_PARAMS)
     self.assertEqual(client.JOB_STATUS_QUEUED, constants.JOB_STATUS_QUEUED)
     self.assertEqual(client.JOB_STATUS_WAITING, constants.JOB_STATUS_WAITING)
     self.assertEqual(client.JOB_STATUS_CANCELING,
@@ -166,23 +117,33 @@ class TestConstants(unittest.TestCase):
     self.assertEqual(client.JOB_STATUS_ALL, constants.JOB_STATUS_ALL)
 
     # Node evacuation
-    self.assertEqual(client.NODE_EVAC_PRI, constants.IALLOCATOR_NEVAC_PRI)
-    self.assertEqual(client.NODE_EVAC_SEC, constants.IALLOCATOR_NEVAC_SEC)
-    self.assertEqual(client.NODE_EVAC_ALL, constants.IALLOCATOR_NEVAC_ALL)
+    self.assertEqual(client.NODE_EVAC_PRI, constants.NODE_EVAC_PRI)
+    self.assertEqual(client.NODE_EVAC_SEC, constants.NODE_EVAC_SEC)
+    self.assertEqual(client.NODE_EVAC_ALL, constants.NODE_EVAC_ALL)
 
     # Legacy name
     self.assertEqual(client.JOB_STATUS_WAITLOCK, constants.JOB_STATUS_WAITING)
 
+    # RAPI feature strings
+    self.assertEqual(client._INST_CREATE_REQV1, rlib2._INST_CREATE_REQV1)
+    self.assertEqual(client.INST_CREATE_REQV1, rlib2._INST_CREATE_REQV1)
+    self.assertEqual(client._INST_REINSTALL_REQV1, rlib2._INST_REINSTALL_REQV1)
+    self.assertEqual(client.INST_REINSTALL_REQV1, rlib2._INST_REINSTALL_REQV1)
+    self.assertEqual(client._NODE_MIGRATE_REQV1, rlib2._NODE_MIGRATE_REQV1)
+    self.assertEqual(client.NODE_MIGRATE_REQV1, rlib2._NODE_MIGRATE_REQV1)
+    self.assertEqual(client._NODE_EVAC_RES1, rlib2._NODE_EVAC_RES1)
+    self.assertEqual(client.NODE_EVAC_RES1, rlib2._NODE_EVAC_RES1)
+
 
 class RapiMockTest(unittest.TestCase):
   def test(self):
     rapi = RapiMock()
     path = "/version"
-    self.assertEqual((404, None), rapi.FetchResponse("/foo", "GET", None))
+    self.assertEqual((404, None), rapi.FetchResponse("/foo", "GET", None, None))
     self.assertEqual((501, "Method not implemented"),
-                     rapi.FetchResponse("/version", "POST", None))
+                     rapi.FetchResponse("/version", "POST", None, None))
     rapi.AddResponse("2")
-    code, response = rapi.FetchResponse("/version", "GET", None)
+    code, response = rapi.FetchResponse("/version", "GET", None, None)
     self.assertEqual(200, code)
     self.assertEqual("2", response)
     self.failUnless(isinstance(rapi.GetLastHandler(), rlib2.R_version))
@@ -211,8 +172,8 @@ def _FakeGnuTlsPycurlVersion():
 class TestExtendedConfig(unittest.TestCase):
   def testAuth(self):
     cl = client.GanetiRapiClient("master.example.com",
-                                 username="user", password="pw",
-                                 curl_factory=lambda: FakeCurl(RapiMock()))
+      username="user", password="pw",
+      curl_factory=lambda: rapi.testutils.FakeCurl(RapiMock()))
 
     curl = cl._CreateCurl()
     self.assertEqual(curl.getopt(pycurl.HTTPAUTH), pycurl.HTTPAUTH_BASIC)
@@ -249,7 +210,7 @@ class TestExtendedConfig(unittest.TestCase):
                                              verify_hostname=verify_hostname,
                                              _pycurl_version_fn=pcverfn)
 
-            curl_factory = lambda: FakeCurl(RapiMock())
+            curl_factory = lambda: rapi.testutils.FakeCurl(RapiMock())
             cl = client.GanetiRapiClient("master.example.com",
                                          curl_config_fn=cfgfn,
                                          curl_factory=curl_factory)
@@ -266,7 +227,7 @@ class TestExtendedConfig(unittest.TestCase):
   def testNoCertVerify(self):
     cfgfn = client.GenericCurlConfig()
 
-    curl_factory = lambda: FakeCurl(RapiMock())
+    curl_factory = lambda: rapi.testutils.FakeCurl(RapiMock())
     cl = client.GanetiRapiClient("master.example.com", curl_config_fn=cfgfn,
                                  curl_factory=curl_factory)
 
@@ -278,7 +239,7 @@ class TestExtendedConfig(unittest.TestCase):
   def testCertVerifyCurlBundle(self):
     cfgfn = client.GenericCurlConfig(use_curl_cabundle=True)
 
-    curl_factory = lambda: FakeCurl(RapiMock())
+    curl_factory = lambda: rapi.testutils.FakeCurl(RapiMock())
     cl = client.GanetiRapiClient("master.example.com", curl_config_fn=cfgfn,
                                  curl_factory=curl_factory)
 
@@ -291,7 +252,7 @@ class TestExtendedConfig(unittest.TestCase):
     mycert = "/tmp/some/UNUSED/cert/file.pem"
     cfgfn = client.GenericCurlConfig(cafile=mycert)
 
-    curl_factory = lambda: FakeCurl(RapiMock())
+    curl_factory = lambda: rapi.testutils.FakeCurl(RapiMock())
     cl = client.GanetiRapiClient("master.example.com", curl_config_fn=cfgfn,
                                  curl_factory=curl_factory)
 
@@ -306,7 +267,7 @@ class TestExtendedConfig(unittest.TestCase):
     cfgfn = client.GenericCurlConfig(capath=certdir,
                                      _pycurl_version_fn=pcverfn)
 
-    curl_factory = lambda: FakeCurl(RapiMock())
+    curl_factory = lambda: rapi.testutils.FakeCurl(RapiMock())
     cl = client.GanetiRapiClient("master.example.com", curl_config_fn=cfgfn,
                                  curl_factory=curl_factory)
 
@@ -321,7 +282,7 @@ class TestExtendedConfig(unittest.TestCase):
     cfgfn = client.GenericCurlConfig(capath=certdir,
                                      _pycurl_version_fn=pcverfn)
 
-    curl_factory = lambda: FakeCurl(RapiMock())
+    curl_factory = lambda: rapi.testutils.FakeCurl(RapiMock())
     cl = client.GanetiRapiClient("master.example.com", curl_config_fn=cfgfn,
                                  curl_factory=curl_factory)
 
@@ -333,7 +294,7 @@ class TestExtendedConfig(unittest.TestCase):
     cfgfn = client.GenericCurlConfig(capath=certdir,
                                      _pycurl_version_fn=pcverfn)
 
-    curl_factory = lambda: FakeCurl(RapiMock())
+    curl_factory = lambda: rapi.testutils.FakeCurl(RapiMock())
     cl = client.GanetiRapiClient("master.example.com", curl_config_fn=cfgfn,
                                  curl_factory=curl_factory)
 
@@ -345,7 +306,7 @@ class TestExtendedConfig(unittest.TestCase):
     cfgfn = client.GenericCurlConfig(capath=certdir,
                                      _pycurl_version_fn=pcverfn)
 
-    curl_factory = lambda: FakeCurl(RapiMock())
+    curl_factory = lambda: rapi.testutils.FakeCurl(RapiMock())
     cl = client.GanetiRapiClient("master.example.com", curl_config_fn=cfgfn,
                                  curl_factory=curl_factory)
 
@@ -357,7 +318,7 @@ class TestExtendedConfig(unittest.TestCase):
         cfgfn = client.GenericCurlConfig(connect_timeout=connect_timeout,
                                          timeout=timeout)
 
-        curl_factory = lambda: FakeCurl(RapiMock())
+        curl_factory = lambda: rapi.testutils.FakeCurl(RapiMock())
         cl = client.GanetiRapiClient("master.example.com", curl_config_fn=cfgfn,
                                      curl_factory=curl_factory)
 
@@ -371,7 +332,7 @@ class GanetiRapiClientTests(testutils.GanetiTestCase):
     testutils.GanetiTestCase.setUp(self)
 
     self.rapi = RapiMock()
-    self.curl = FakeCurl(self.rapi)
+    self.curl = rapi.testutils.FakeCurl(self.rapi)
     self.client = client.GanetiRapiClient("master.example.com",
                                           curl_factory=lambda: self.curl)
 
@@ -866,7 +827,7 @@ class GanetiRapiClientTests(testutils.GanetiTestCase):
     self.rapi.AddResponse(serializer.DumpJson([rlib2._NODE_EVAC_RES1]))
     self.rapi.AddResponse("8888")
     job_id = self.client.EvacuateNode("node-3", iallocator="hail", dry_run=True,
-                                      mode=constants.IALLOCATOR_NEVAC_ALL,
+                                      mode=constants.NODE_EVAC_ALL,
                                       early_release=True)
     self.assertEqual(8888, job_id)
     self.assertItems(["node-3"])
@@ -972,6 +933,24 @@ class GanetiRapiClientTests(testutils.GanetiTestCase):
     self.assertQuery("force", ["1"])
     self.assertEqual("\"master-candidate\"", self.rapi.GetLastRequestData())
 
+  def testPowercycleNode(self):
+    self.rapi.AddResponse("23051")
+    self.assertEqual(23051,
+        self.client.PowercycleNode("node5468", force=True))
+    self.assertHandler(rlib2.R_2_nodes_name_powercycle)
+    self.assertItems(["node5468"])
+    self.assertQuery("force", ["1"])
+    self.assertFalse(self.rapi.GetLastRequestData())
+    self.assertEqual(self.rapi.CountPending(), 0)
+
+  def testModifyNode(self):
+    self.rapi.AddResponse("3783")
+    job_id = self.client.ModifyNode("node16979.example.com", drained=True)
+    self.assertEqual(job_id, 3783)
+    self.assertHandler(rlib2.R_2_nodes_name_modify)
+    self.assertItems(["node16979.example.com"])
+    self.assertEqual(self.rapi.CountPending(), 0)
+
   def testGetNodeStorageUnits(self):
     self.rapi.AddResponse("42")
     self.assertEqual(42,
@@ -1165,6 +1144,14 @@ class GanetiRapiClientTests(testutils.GanetiTestCase):
     self.assertHandler(rlib2.R_2_instances_name_deactivate_disks)
     self.assertFalse(self.rapi.GetLastHandler().queryargs)
 
+  def testRecreateInstanceDisks(self):
+    self.rapi.AddResponse("13553")
+    job_id = self.client.RecreateInstanceDisks("inst23153")
+    self.assertEqual(job_id, 13553)
+    self.assertItems(["inst23153"])
+    self.assertHandler(rlib2.R_2_instances_name_recreate_disks)
+    self.assertFalse(self.rapi.GetLastHandler().queryargs)
+
   def testGetInstanceConsole(self):
     self.rapi.AddResponse("26876")
     job_id = self.client.GetInstanceConsole("inst21491")
@@ -1220,22 +1207,22 @@ class GanetiRapiClientTests(testutils.GanetiTestCase):
 
   def testQuery(self):
     for idx, what in enumerate(constants.QR_VIA_RAPI):
-      for idx2, filter_ in enumerate([None, ["?", "name"]]):
+      for idx2, qfilter in enumerate([None, ["?", "name"]]):
         job_id = 11010 + (idx << 4) + (idx2 << 16)
         fields = sorted(query.ALL_FIELDS[what].keys())[:10]
 
         self.rapi.AddResponse(str(job_id))
-        self.assertEqual(self.client.Query(what, fields, filter_=filter_),
+        self.assertEqual(self.client.Query(what, fields, qfilter=qfilter),
                          job_id)
         self.assertItems([what])
         self.assertHandler(rlib2.R_2_query)
         self.assertFalse(self.rapi.GetLastHandler().queryargs)
         data = serializer.LoadJson(self.rapi.GetLastRequestData())
         self.assertEqual(data["fields"], fields)
-        if filter_ is None:
-          self.assertTrue("filter" not in data)
+        if qfilter is None:
+          self.assertTrue("qfilter" not in data)
         else:
-          self.assertEqual(data["filter"], filter_)
+          self.assertEqual(data["qfilter"], qfilter)
         self.assertEqual(self.rapi.CountPending(), 0)
 
   def testQueryFields(self):
index 70f68b5..5d53118 100755 (executable)
@@ -69,19 +69,5 @@ class MapperTests(unittest.TestCase):
     self._TestFailingUri("/instances/does/not/exist")
 
 
-class R_RootTests(unittest.TestCase):
-  """Testing for R_root class."""
-
-  def setUp(self):
-    self.root = connector.R_root(None, None, None)
-
-  def testGet(self):
-    expected = [
-      {'name': '2', 'uri': '/2'},
-      {'name': 'version', 'uri': '/version'},
-      ]
-    self.assertEquals(self.root.GET(), expected)
-
-
 if __name__ == '__main__':
   testutils.GanetiTestProgram()
index de82745..cde68ce 100755 (executable)
 
 
 import unittest
-import tempfile
+import itertools
+import random
 
 from ganeti import constants
 from ganeti import opcodes
 from ganeti import compat
 from ganeti import http
 from ganeti import query
+from ganeti import luxi
+from ganeti import errors
 
 from ganeti.rapi import rlib2
 
 import testutils
 
 
+class _FakeRequestPrivateData:
+  def __init__(self, body_data):
+    self.body_data = body_data
+
+
+class _FakeRequest:
+  def __init__(self, body_data):
+    self.private = _FakeRequestPrivateData(body_data)
+
+
+def _CreateHandler(cls, items, queryargs, body_data, client_cls):
+  return cls(items, queryargs, _FakeRequest(body_data),
+             _client_cls=client_cls)
+
+
+class _FakeClient:
+  def __init__(self):
+    self._jobs = []
+
+  def GetNextSubmittedJob(self):
+    return self._jobs.pop(0)
+
+  def SubmitJob(self, ops):
+    job_id = str(1 + int(random.random() * 1000000))
+    self._jobs.append((job_id, ops))
+    return job_id
+
+
+class _FakeClientFactory:
+  def __init__(self, cls):
+    self._client_cls = cls
+    self._clients = []
+
+  def GetNextClient(self):
+    return self._clients.pop(0)
+
+  def __call__(self):
+    cl = self._client_cls()
+    self._clients.append(cl)
+    return cl
+
+
 class TestConstants(unittest.TestCase):
   def testConsole(self):
     # Exporting the console field without authentication might expose
@@ -56,13 +101,692 @@ class TestConstants(unittest.TestCase):
       self.assertFalse(set(fields) - set(query.ALL_FIELDS[qr].keys()))
 
 
-class TestParseInstanceCreateRequestVersion1(testutils.GanetiTestCase):
-  def setUp(self):
-    testutils.GanetiTestCase.setUp(self)
+class TestClientConnectError(unittest.TestCase):
+  @staticmethod
+  def _FailingClient():
+    raise luxi.NoMasterError("test")
+
+  def test(self):
+    resources = [
+      rlib2.R_2_groups,
+      rlib2.R_2_instances,
+      rlib2.R_2_nodes,
+      ]
+    for cls in resources:
+      handler = _CreateHandler(cls, ["name"], [], None, self._FailingClient)
+      self.assertRaises(http.HttpBadGateway, handler.GET)
+
+
+class TestJobSubmitError(unittest.TestCase):
+  class _SubmitErrorClient:
+    @staticmethod
+    def SubmitJob(ops):
+      raise errors.JobQueueFull("test")
+
+  def test(self):
+    handler = _CreateHandler(rlib2.R_2_redist_config, [], [], None,
+                             self._SubmitErrorClient)
+    self.assertRaises(http.HttpServiceUnavailable, handler.PUT)
+
+
+class TestClusterModify(unittest.TestCase):
+  def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+    handler = _CreateHandler(rlib2.R_2_cluster_modify, [], [], {
+      "vg_name": "testvg",
+      "candidate_pool_size": 100,
+      }, clfactory)
+    job_id = handler.PUT()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpClusterSetParams))
+    self.assertEqual(op.vg_name, "testvg")
+    self.assertEqual(op.candidate_pool_size, 100)
+
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
+
+  def testInvalidValue(self):
+    for attr in ["vg_name", "candidate_pool_size", "beparams", "_-Unknown#"]:
+      clfactory = _FakeClientFactory(_FakeClient)
+      handler = _CreateHandler(rlib2.R_2_cluster_modify, [], [], {
+        attr: True,
+        }, clfactory)
+      self.assertRaises(http.HttpBadRequest, handler.PUT)
+      self.assertRaises(IndexError, clfactory.GetNextClient)
+
+
+class TestRedistConfig(unittest.TestCase):
+  def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+    handler = _CreateHandler(rlib2.R_2_redist_config, [], [], None, clfactory)
+    job_id = handler.PUT()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpClusterRedistConf))
+
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
+
+
+class TestNodeMigrate(unittest.TestCase):
+  def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+    handler = _CreateHandler(rlib2.R_2_nodes_name_migrate, ["node1"], {}, {
+      "iallocator": "fooalloc",
+      }, clfactory)
+    job_id = handler.POST()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpNodeMigrate))
+    self.assertEqual(op.node_name, "node1")
+    self.assertEqual(op.iallocator, "fooalloc")
+
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
+
+  def testQueryArgsConflict(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+    handler = _CreateHandler(rlib2.R_2_nodes_name_migrate, ["node2"], {
+      "live": True,
+      "mode": constants.HT_MIGRATION_NONLIVE,
+      }, None, clfactory)
+    self.assertRaises(http.HttpBadRequest, handler.POST)
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+  def testQueryArgsMode(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+    queryargs = {
+      "mode": [constants.HT_MIGRATION_LIVE],
+      }
+    handler = _CreateHandler(rlib2.R_2_nodes_name_migrate, ["node17292"],
+                             queryargs, None, clfactory)
+    job_id = handler.POST()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpNodeMigrate))
+    self.assertEqual(op.node_name, "node17292")
+    self.assertEqual(op.mode, constants.HT_MIGRATION_LIVE)
+
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
+
+  def testQueryArgsLive(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+
+    for live in [False, True]:
+      queryargs = {
+        "live": [str(int(live))],
+        }
+      handler = _CreateHandler(rlib2.R_2_nodes_name_migrate, ["node6940"],
+                               queryargs, None, clfactory)
+      job_id = handler.POST()
+
+      cl = clfactory.GetNextClient()
+      self.assertRaises(IndexError, clfactory.GetNextClient)
+
+      (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+      self.assertEqual(job_id, exp_job_id)
+      self.assertTrue(isinstance(op, opcodes.OpNodeMigrate))
+      self.assertEqual(op.node_name, "node6940")
+      if live:
+        self.assertEqual(op.mode, constants.HT_MIGRATION_LIVE)
+      else:
+        self.assertEqual(op.mode, constants.HT_MIGRATION_NONLIVE)
+
+      self.assertRaises(IndexError, cl.GetNextSubmittedJob)
+
+
+class TestNodeEvacuate(unittest.TestCase):
+  def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+    handler = _CreateHandler(rlib2.R_2_nodes_name_evacuate, ["node92"], {
+      "dry-run": ["1"],
+      }, {
+      "mode": constants.IALLOCATOR_NEVAC_SEC,
+      }, clfactory)
+    job_id = handler.POST()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpNodeEvacuate))
+    self.assertEqual(op.node_name, "node92")
+    self.assertEqual(op.mode, constants.IALLOCATOR_NEVAC_SEC)
+    self.assertTrue(op.dry_run)
+
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
+
+
+class TestNodePowercycle(unittest.TestCase):
+  def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+    handler = _CreateHandler(rlib2.R_2_nodes_name_powercycle, ["node20744"], {
+      "force": ["1"],
+      }, None, clfactory)
+    job_id = handler.POST()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpNodePowercycle))
+    self.assertEqual(op.node_name, "node20744")
+    self.assertTrue(op.force)
+
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
+
+
+class TestGroupAssignNodes(unittest.TestCase):
+  def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+    handler = _CreateHandler(rlib2.R_2_groups_name_assign_nodes, ["grp-a"], {
+      "dry-run": ["1"],
+      "force": ["1"],
+      }, {
+      "nodes": ["n2", "n3"],
+      }, clfactory)
+    job_id = handler.PUT()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpGroupAssignNodes))
+    self.assertEqual(op.group_name, "grp-a")
+    self.assertEqual(op.nodes, ["n2", "n3"])
+    self.assertTrue(op.dry_run)
+    self.assertTrue(op.force)
+
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
+
+
+class TestInstanceDelete(unittest.TestCase):
+  def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+    handler = _CreateHandler(rlib2.R_2_instances_name, ["inst30965"], {
+      "dry-run": ["1"],
+      }, {}, clfactory)
+    job_id = handler.DELETE()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpInstanceRemove))
+    self.assertEqual(op.instance_name, "inst30965")
+    self.assertTrue(op.dry_run)
+    self.assertFalse(op.ignore_failures)
+
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
+
+
+class TestInstanceInfo(unittest.TestCase):
+  def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+    handler = _CreateHandler(rlib2.R_2_instances_name_info, ["inst31217"], {
+      "static": ["1"],
+      }, {}, clfactory)
+    job_id = handler.GET()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpInstanceQueryData))
+    self.assertEqual(op.instances, ["inst31217"])
+    self.assertTrue(op.static)
+
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
+
+
+class TestInstanceReboot(unittest.TestCase):
+  def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+    handler = _CreateHandler(rlib2.R_2_instances_name_reboot, ["inst847"], {
+      "dry-run": ["1"],
+      "ignore_secondaries": ["1"],
+      }, {}, clfactory)
+    job_id = handler.POST()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpInstanceReboot))
+    self.assertEqual(op.instance_name, "inst847")
+    self.assertEqual(op.reboot_type, constants.INSTANCE_REBOOT_HARD)
+    self.assertTrue(op.ignore_secondaries)
+    self.assertTrue(op.dry_run)
+
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
+
+
+class TestInstanceStartup(unittest.TestCase):
+  def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+    handler = _CreateHandler(rlib2.R_2_instances_name_startup, ["inst31083"], {
+      "force": ["1"],
+      "no_remember": ["1"],
+      }, {}, clfactory)
+    job_id = handler.PUT()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpInstanceStartup))
+    self.assertEqual(op.instance_name, "inst31083")
+    self.assertTrue(op.no_remember)
+    self.assertTrue(op.force)
+    self.assertFalse(op.dry_run)
+
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
+
+
+class TestInstanceShutdown(unittest.TestCase):
+  def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+    handler = _CreateHandler(rlib2.R_2_instances_name_shutdown, ["inst26791"], {
+      "no_remember": ["0"],
+      }, {}, clfactory)
+    job_id = handler.PUT()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpInstanceShutdown))
+    self.assertEqual(op.instance_name, "inst26791")
+    self.assertFalse(op.no_remember)
+    self.assertFalse(op.dry_run)
+
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
+
+
+class TestInstanceActivateDisks(unittest.TestCase):
+  def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+    handler = _CreateHandler(rlib2.R_2_instances_name_activate_disks, ["xyz"], {
+      "ignore_size": ["1"],
+      }, {}, clfactory)
+    job_id = handler.PUT()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpInstanceActivateDisks))
+    self.assertEqual(op.instance_name, "xyz")
+    self.assertTrue(op.ignore_size)
+    self.assertFalse(hasattr(op, "dry_run"))
+
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
+
+
+class TestInstanceDeactivateDisks(unittest.TestCase):
+  def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+    handler = _CreateHandler(rlib2.R_2_instances_name_deactivate_disks,
+                             ["inst22357"], {}, {}, clfactory)
+    job_id = handler.PUT()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpInstanceDeactivateDisks))
+    self.assertEqual(op.instance_name, "inst22357")
+    self.assertFalse(hasattr(op, "dry_run"))
+    self.assertFalse(hasattr(op, "force"))
+
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
+
+
+class TestInstanceRecreateDisks(unittest.TestCase):
+  def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+    handler = _CreateHandler(rlib2.R_2_instances_name_recreate_disks,
+                             ["inst22357"], {}, {}, clfactory)
+    job_id = handler.POST()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpInstanceRecreateDisks))
+    self.assertEqual(op.instance_name, "inst22357")
+    self.assertFalse(hasattr(op, "dry_run"))
+    self.assertFalse(hasattr(op, "force"))
+
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
+
+
+class TestInstanceFailover(unittest.TestCase):
+  def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+    handler = _CreateHandler(rlib2.R_2_instances_name_failover,
+                             ["inst12794"], {}, {}, clfactory)
+    job_id = handler.PUT()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpInstanceFailover))
+    self.assertEqual(op.instance_name, "inst12794")
+    self.assertFalse(hasattr(op, "dry_run"))
+    self.assertFalse(hasattr(op, "force"))
+
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
+
+
+class TestInstanceDiskGrow(unittest.TestCase):
+  def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+    data = {
+      "amount": 1024,
+      }
+    handler = _CreateHandler(rlib2.R_2_instances_name_disk_grow,
+                             ["inst10742", "3"], {}, data, clfactory)
+    job_id = handler.POST()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpInstanceGrowDisk))
+    self.assertEqual(op.instance_name, "inst10742")
+    self.assertEqual(op.disk, 3)
+    self.assertEqual(op.amount, 1024)
+    self.assertFalse(hasattr(op, "dry_run"))
+    self.assertFalse(hasattr(op, "force"))
+
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
+
+
+class TestBackupPrepare(unittest.TestCase):
+  def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+    queryargs = {
+      "mode": constants.EXPORT_MODE_REMOTE,
+      }
+    handler = _CreateHandler(rlib2.R_2_instances_name_prepare_export,
+                             ["inst17925"], queryargs, {}, clfactory)
+    job_id = handler.PUT()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpBackupPrepare))
+    self.assertEqual(op.instance_name, "inst17925")
+    self.assertEqual(op.mode, constants.EXPORT_MODE_REMOTE)
+    self.assertFalse(hasattr(op, "dry_run"))
+    self.assertFalse(hasattr(op, "force"))
+
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
+
+
+class TestGroupRemove(unittest.TestCase):
+  def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+    handler = _CreateHandler(rlib2.R_2_groups_name,
+                             ["grp28575"], {}, {}, clfactory)
+    job_id = handler.DELETE()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpGroupRemove))
+    self.assertEqual(op.group_name, "grp28575")
+    self.assertFalse(op.dry_run)
+    self.assertFalse(hasattr(op, "force"))
+
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
+
+
+class TestStorageQuery(unittest.TestCase):
+  def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+    queryargs = {
+      "storage_type": constants.ST_LVM_PV,
+      "output_fields": "name,other",
+      }
+    handler = _CreateHandler(rlib2.R_2_nodes_name_storage,
+                             ["node21075"], queryargs, {}, clfactory)
+    job_id = handler.GET()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpNodeQueryStorage))
+    self.assertEqual(op.nodes, ["node21075"])
+    self.assertEqual(op.storage_type, constants.ST_LVM_PV)
+    self.assertEqual(op.output_fields, ["name", "other"])
+    self.assertFalse(hasattr(op, "dry_run"))
+    self.assertFalse(hasattr(op, "force"))
+
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
+
+  def testErrors(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+
+    queryargs = {
+      "output_fields": "name,other",
+      }
+    handler = _CreateHandler(rlib2.R_2_nodes_name_storage,
+                             ["node10538"], queryargs, {}, clfactory)
+    self.assertRaises(http.HttpBadRequest, handler.GET)
+
+    queryargs = {
+      "storage_type": constants.ST_LVM_VG,
+      }
+    handler = _CreateHandler(rlib2.R_2_nodes_name_storage,
+                             ["node21273"], queryargs, {}, clfactory)
+    self.assertRaises(http.HttpBadRequest, handler.GET)
+
+    queryargs = {
+      "storage_type": "##unknown_storage##",
+      "output_fields": "name,other",
+      }
+    handler = _CreateHandler(rlib2.R_2_nodes_name_storage,
+                             ["node10315"], queryargs, {}, clfactory)
+    self.assertRaises(http.HttpBadRequest, handler.GET)
+
+
+class TestStorageModify(unittest.TestCase):
+  def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+
+    for allocatable in [None, "1", "0"]:
+      queryargs = {
+        "storage_type": constants.ST_LVM_VG,
+        "name": "pv-a",
+        }
+
+      if allocatable is not None:
+        queryargs["allocatable"] = allocatable
+
+      handler = _CreateHandler(rlib2.R_2_nodes_name_storage_modify,
+                               ["node9292"], queryargs, {}, clfactory)
+      job_id = handler.PUT()
+
+      cl = clfactory.GetNextClient()
+      self.assertRaises(IndexError, clfactory.GetNextClient)
+
+      (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+      self.assertEqual(job_id, exp_job_id)
+      self.assertTrue(isinstance(op, opcodes.OpNodeModifyStorage))
+      self.assertEqual(op.node_name, "node9292")
+      self.assertEqual(op.storage_type, constants.ST_LVM_VG)
+      self.assertEqual(op.name, "pv-a")
+      if allocatable is None:
+        self.assertFalse(op.changes)
+      else:
+        assert allocatable in ("0", "1")
+        self.assertEqual(op.changes, {
+          constants.SF_ALLOCATABLE: (allocatable == "1"),
+          })
+      self.assertFalse(hasattr(op, "dry_run"))
+      self.assertFalse(hasattr(op, "force"))
+
+      self.assertRaises(IndexError, cl.GetNextSubmittedJob)
+
+  def testErrors(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+
+    # No storage type
+    queryargs = {
+      "name": "xyz",
+      }
+    handler = _CreateHandler(rlib2.R_2_nodes_name_storage_modify,
+                             ["node26016"], queryargs, {}, clfactory)
+    self.assertRaises(http.HttpBadRequest, handler.PUT)
+
+    # No name
+    queryargs = {
+      "storage_type": constants.ST_LVM_VG,
+      }
+    handler = _CreateHandler(rlib2.R_2_nodes_name_storage_modify,
+                             ["node21218"], queryargs, {}, clfactory)
+    self.assertRaises(http.HttpBadRequest, handler.PUT)
+
+    # Invalid value
+    queryargs = {
+      "storage_type": constants.ST_LVM_VG,
+      "name": "pv-b",
+      "allocatable": "noint",
+      }
+    handler = _CreateHandler(rlib2.R_2_nodes_name_storage_modify,
+                             ["node30685"], queryargs, {}, clfactory)
+    self.assertRaises(http.HttpBadRequest, handler.PUT)
+
+
+class TestStorageRepair(unittest.TestCase):
+  def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+    queryargs = {
+      "storage_type": constants.ST_LVM_PV,
+      "name": "pv16611",
+      }
+    handler = _CreateHandler(rlib2.R_2_nodes_name_storage_repair,
+                             ["node19265"], queryargs, {}, clfactory)
+    job_id = handler.PUT()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpRepairNodeStorage))
+    self.assertEqual(op.node_name, "node19265")
+    self.assertEqual(op.storage_type, constants.ST_LVM_PV)
+    self.assertEqual(op.name, "pv16611")
+    self.assertFalse(hasattr(op, "dry_run"))
+    self.assertFalse(hasattr(op, "force"))
+
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
+
+  def testErrors(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+
+    # No storage type
+    queryargs = {
+      "name": "xyz",
+      }
+    handler = _CreateHandler(rlib2.R_2_nodes_name_storage_repair,
+                             ["node11275"], queryargs, {}, clfactory)
+    self.assertRaises(http.HttpBadRequest, handler.PUT)
+
+    # No name
+    queryargs = {
+      "storage_type": constants.ST_LVM_VG,
+      }
+    handler = _CreateHandler(rlib2.R_2_nodes_name_storage_repair,
+                             ["node21218"], queryargs, {}, clfactory)
+    self.assertRaises(http.HttpBadRequest, handler.PUT)
+
+
+class TestTags(unittest.TestCase):
+  TAG_HANDLERS = [
+    rlib2.R_2_instances_name_tags,
+    rlib2.R_2_nodes_name_tags,
+    rlib2.R_2_groups_name_tags,
+    rlib2.R_2_tags,
+    ]
+
+  def testSetAndDelete(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+
+    for method, opcls in [("PUT", opcodes.OpTagsSet),
+                          ("DELETE", opcodes.OpTagsDel)]:
+      for idx, handler in enumerate(self.TAG_HANDLERS):
+        dry_run = bool(idx % 2)
+        name = "test%s" % idx
+        queryargs = {
+          "tag": ["foo", "bar", "baz"],
+          "dry-run": str(int(dry_run)),
+          }
+
+        handler = _CreateHandler(handler, [name], queryargs, {}, clfactory)
+        job_id = getattr(handler, method)()
+
+        cl = clfactory.GetNextClient()
+        self.assertRaises(IndexError, clfactory.GetNextClient)
+
+        (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+        self.assertEqual(job_id, exp_job_id)
+        self.assertTrue(isinstance(op, opcls))
+        self.assertEqual(op.kind, handler.TAG_LEVEL)
+        if handler.TAG_LEVEL == constants.TAG_CLUSTER:
+          self.assertTrue(op.name is None)
+        else:
+          self.assertEqual(op.name, name)
+        self.assertEqual(op.tags, ["foo", "bar", "baz"])
+        self.assertEqual(op.dry_run, dry_run)
+        self.assertFalse(hasattr(op, "force"))
+
+        self.assertRaises(IndexError, cl.GetNextSubmittedJob)
 
-    self.Parse = rlib2._ParseInstanceCreateRequestVersion1
 
+class TestInstanceCreation(testutils.GanetiTestCase):
   def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+
+    name = "inst863.example.com"
+
     disk_variants = [
       # No disks
       [],
@@ -94,10 +818,13 @@ class TestParseInstanceCreateRequestVersion1(testutils.GanetiTestCase):
       None,
       {},
       { constants.BE_VCPUS: 2, },
-      { constants.BE_MEMORY: 123, },
+      { constants.BE_MAXMEM: 200, },
+      { constants.BE_MEMORY: 256, },
       { constants.BE_VCPUS: 2,
-        constants.BE_MEMORY: 1024,
-        constants.BE_AUTO_BALANCE: True, }
+        constants.BE_MAXMEM: 1024,
+        constants.BE_MINMEM: 1024,
+        constants.BE_AUTO_BALANCE: True,
+        constants.BE_ALWAYS_FAILOVER: True, }
       ]
 
     hvparam_variants = [
@@ -113,25 +840,41 @@ class TestParseInstanceCreateRequestVersion1(testutils.GanetiTestCase):
           for disks in disk_variants:
             for beparams in beparam_variants:
               for hvparams in hvparam_variants:
-                data = {
-                  "name": "inst1.example.com",
-                  "hypervisor": constants.HT_FAKE,
-                  "disks": disks,
-                  "nics": nics,
-                  "mode": mode,
-                  "disk_template": disk_template,
-                  "os": "debootstrap",
-                  }
-
-                if beparams is not None:
-                  data["beparams"] = beparams
-
-                if hvparams is not None:
-                  data["hvparams"] = hvparams
-
                 for dry_run in [False, True]:
-                  op = self.Parse(data, dry_run)
-                  self.assert_(isinstance(op, opcodes.OpInstanceCreate))
+                  queryargs = {
+                    "dry-run": str(int(dry_run)),
+                    }
+
+                  data = {
+                    rlib2._REQ_DATA_VERSION: 1,
+                    "name": name,
+                    "hypervisor": constants.HT_FAKE,
+                    "disks": disks,
+                    "nics": nics,
+                    "mode": mode,
+                    "disk_template": disk_template,
+                    "os": "debootstrap",
+                    }
+
+                  if beparams is not None:
+                    data["beparams"] = beparams
+
+                  if hvparams is not None:
+                    data["hvparams"] = hvparams
+
+                  handler = _CreateHandler(rlib2.R_2_instances, [],
+                                           queryargs, data, clfactory)
+                  job_id = handler.POST()
+
+                  cl = clfactory.GetNextClient()
+                  self.assertRaises(IndexError, clfactory.GetNextClient)
+
+                  (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+                  self.assertEqual(job_id, exp_job_id)
+                  self.assertRaises(IndexError, cl.GetNextSubmittedJob)
+
+                  self.assertTrue(isinstance(op, opcodes.OpInstanceCreate))
+                  self.assertEqual(op.instance_name, name)
                   self.assertEqual(op.mode, mode)
                   self.assertEqual(op.disk_template, disk_template)
                   self.assertEqual(op.dry_run, dry_run)
@@ -160,34 +903,47 @@ class TestParseInstanceCreateRequestVersion1(testutils.GanetiTestCase):
                     self.assertEqualValues(op.hvparams, hvparams)
 
   def testLegacyName(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+
     name = "inst29128.example.com"
     data = {
+      rlib2._REQ_DATA_VERSION: 1,
       "name": name,
       "disks": [],
       "nics": [],
       "mode": constants.INSTANCE_CREATE,
       "disk_template": constants.DT_PLAIN,
       }
-    op = self.Parse(data, False)
-    self.assert_(isinstance(op, opcodes.OpInstanceCreate))
+
+    handler = _CreateHandler(rlib2.R_2_instances, [], {}, data, clfactory)
+    job_id = handler.POST()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpInstanceCreate))
     self.assertEqual(op.instance_name, name)
     self.assertFalse(hasattr(op, "name"))
+    self.assertFalse(op.dry_run)
+
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
 
     # Define both
-    data = {
-      "name": name,
-      "instance_name": "other.example.com",
-      "disks": [],
-      "nics": [],
-      "mode": constants.INSTANCE_CREATE,
-      "disk_template": constants.DT_PLAIN,
-      }
-    self.assertRaises(http.HttpBadRequest, self.Parse, data, False)
+    data["instance_name"] = "other.example.com"
+    assert "name" in data and "instance_name" in data
+    handler = _CreateHandler(rlib2.R_2_instances, [], {}, data, clfactory)
+    self.assertRaises(http.HttpBadRequest, handler.POST)
+    self.assertRaises(IndexError, clfactory.GetNextClient)
 
   def testLegacyOs(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+
     name = "inst4673.example.com"
     os = "linux29206"
     data = {
+      rlib2._REQ_DATA_VERSION: 1,
       "name": name,
       "os_type": os,
       "disks": [],
@@ -195,27 +951,35 @@ class TestParseInstanceCreateRequestVersion1(testutils.GanetiTestCase):
       "mode": constants.INSTANCE_CREATE,
       "disk_template": constants.DT_PLAIN,
       }
-    op = self.Parse(data, False)
-    self.assert_(isinstance(op, opcodes.OpInstanceCreate))
+
+    handler = _CreateHandler(rlib2.R_2_instances, [], {}, data, clfactory)
+    job_id = handler.POST()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpInstanceCreate))
     self.assertEqual(op.instance_name, name)
     self.assertEqual(op.os_type, os)
     self.assertFalse(hasattr(op, "os"))
+    self.assertFalse(op.dry_run)
+
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
 
     # Define both
-    data = {
-      "instance_name": name,
-      "os": os,
-      "os_type": "linux9584",
-      "disks": [],
-      "nics": [],
-      "mode": constants.INSTANCE_CREATE,
-      "disk_template": constants.DT_PLAIN,
-      }
-    self.assertRaises(http.HttpBadRequest, self.Parse, data, False)
+    data["os"] = "linux9584"
+    assert "os" in data and "os_type" in data
+    handler = _CreateHandler(rlib2.R_2_instances, [], {}, data, clfactory)
+    self.assertRaises(http.HttpBadRequest, handler.POST)
 
   def testErrors(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+
     # Test all required fields
     reqfields = {
+      rlib2._REQ_DATA_VERSION: 1,
       "name": "inst1.example.com",
       "disks": [],
       "nics": [],
@@ -224,9 +988,11 @@ class TestParseInstanceCreateRequestVersion1(testutils.GanetiTestCase):
       }
 
     for name in reqfields.keys():
-      self.assertRaises(http.HttpBadRequest, self.Parse,
-                        dict(i for i in reqfields.iteritems() if i[0] != name),
-                        False)
+      data = dict(i for i in reqfields.iteritems() if i[0] != name)
+
+      handler = _CreateHandler(rlib2.R_2_instances, [], {}, data, clfactory)
+      self.assertRaises(http.HttpBadRequest, handler.POST)
+      self.assertRaises(IndexError, clfactory.GetNextClient)
 
     # Invalid disks and nics
     for field in ["disks", "nics"]:
@@ -236,16 +1002,52 @@ class TestParseInstanceCreateRequestVersion1(testutils.GanetiTestCase):
       for invvalue in invalid_values:
         data = reqfields.copy()
         data[field] = invvalue
-        self.assertRaises(http.HttpBadRequest, self.Parse, data, False)
+        handler = _CreateHandler(rlib2.R_2_instances, [], {}, data, clfactory)
+        self.assertRaises(http.HttpBadRequest, handler.POST)
+        self.assertRaises(IndexError, clfactory.GetNextClient)
 
+  def testVersion(self):
+    clfactory = _FakeClientFactory(_FakeClient)
 
-class TestParseExportInstanceRequest(testutils.GanetiTestCase):
-  def setUp(self):
-    testutils.GanetiTestCase.setUp(self)
+    # No version field
+    data = {
+      "name": "inst1.example.com",
+      "disks": [],
+      "nics": [],
+      "mode": constants.INSTANCE_CREATE,
+      "disk_template": constants.DT_PLAIN,
+      }
+
+    handler = _CreateHandler(rlib2.R_2_instances, [], {}, data, clfactory)
+    self.assertRaises(http.HttpBadRequest, handler.POST)
+
+    # Old and incorrect versions
+    for version in [0, -1, 10483, "Hello World"]:
+      data[rlib2._REQ_DATA_VERSION] = version
+
+      handler = _CreateHandler(rlib2.R_2_instances, [], {}, data, clfactory)
+      self.assertRaises(http.HttpBadRequest, handler.POST)
 
-    self.Parse = rlib2._ParseExportInstanceRequest
+      self.assertRaises(IndexError, clfactory.GetNextClient)
 
+    # Correct version
+    data[rlib2._REQ_DATA_VERSION] = 1
+    handler = _CreateHandler(rlib2.R_2_instances, [], {}, data, clfactory)
+    job_id = handler.POST()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpInstanceCreate))
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
+
+
+class TestBackupExport(unittest.TestCase):
   def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+
     name = "instmoo"
     data = {
       "mode": constants.EXPORT_MODE_REMOTE,
@@ -255,43 +1057,72 @@ class TestParseExportInstanceRequest(testutils.GanetiTestCase):
       "x509_key_name": ["name", "hash"],
       "destination_x509_ca": "---cert---"
       }
-    op = self.Parse(name, data)
-    self.assert_(isinstance(op, opcodes.OpBackupExport))
+
+    handler = _CreateHandler(rlib2.R_2_instances_name_export, [name], {},
+                             data, clfactory)
+    job_id = handler.PUT()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpBackupExport))
     self.assertEqual(op.instance_name, name)
     self.assertEqual(op.mode, constants.EXPORT_MODE_REMOTE)
+    self.assertEqual(op.target_node, [(1, 2, 3), (99, 99, 99)])
     self.assertEqual(op.shutdown, True)
     self.assertEqual(op.remove_instance, True)
-    self.assertEqualValues(op.x509_key_name, ("name", "hash"))
+    self.assertEqual(op.x509_key_name, ["name", "hash"])
     self.assertEqual(op.destination_x509_ca, "---cert---")
+    self.assertFalse(hasattr(op, "dry_run"))
+    self.assertFalse(hasattr(op, "force"))
+
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
 
   def testDefaults(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+
     name = "inst1"
     data = {
       "destination": "node2",
       "shutdown": False,
       }
-    op = self.Parse(name, data)
-    self.assert_(isinstance(op, opcodes.OpBackupExport))
+
+    handler = _CreateHandler(rlib2.R_2_instances_name_export, [name], {},
+                             data, clfactory)
+    job_id = handler.PUT()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpBackupExport))
     self.assertEqual(op.instance_name, name)
     self.assertEqual(op.target_node, "node2")
     self.assertFalse(hasattr(op, "mode"))
     self.assertFalse(hasattr(op, "remove_instance"))
     self.assertFalse(hasattr(op, "destination"))
+    self.assertFalse(hasattr(op, "dry_run"))
+    self.assertFalse(hasattr(op, "force"))
 
-  def testErrors(self):
-    self.assertRaises(http.HttpBadRequest, self.Parse, "err1",
-                      { "remove_instance": "True", })
-    self.assertRaises(http.HttpBadRequest, self.Parse, "err1",
-                      { "remove_instance": "False", })
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
 
+  def testErrors(self):
+    clfactory = _FakeClientFactory(_FakeClient)
 
-class TestParseMigrateInstanceRequest(testutils.GanetiTestCase):
-  def setUp(self):
-    testutils.GanetiTestCase.setUp(self)
+    for value in ["True", "False"]:
+      handler = _CreateHandler(rlib2.R_2_instances_name_export, ["err1"], {}, {
+        "remove_instance": value,
+        }, clfactory)
+      self.assertRaises(http.HttpBadRequest, handler.PUT)
 
-    self.Parse = rlib2._ParseMigrateInstanceRequest
 
+class TestInstanceMigrate(testutils.GanetiTestCase):
   def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+
     name = "instYooho6ek"
 
     for cleanup in [False, True]:
@@ -300,29 +1131,53 @@ class TestParseMigrateInstanceRequest(testutils.GanetiTestCase):
           "cleanup": cleanup,
           "mode": mode,
           }
-        op = self.Parse(name, data)
-        self.assert_(isinstance(op, opcodes.OpInstanceMigrate))
+
+        handler = _CreateHandler(rlib2.R_2_instances_name_migrate, [name], {},
+                                 data, clfactory)
+        job_id = handler.PUT()
+
+        cl = clfactory.GetNextClient()
+        self.assertRaises(IndexError, clfactory.GetNextClient)
+
+        (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+        self.assertEqual(job_id, exp_job_id)
+        self.assertTrue(isinstance(op, opcodes.OpInstanceMigrate))
         self.assertEqual(op.instance_name, name)
         self.assertEqual(op.mode, mode)
         self.assertEqual(op.cleanup, cleanup)
+        self.assertFalse(hasattr(op, "dry_run"))
+        self.assertFalse(hasattr(op, "force"))
+
+        self.assertRaises(IndexError, cl.GetNextSubmittedJob)
 
   def testDefaults(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+
     name = "instnohZeex0"
 
-    op = self.Parse(name, {})
-    self.assert_(isinstance(op, opcodes.OpInstanceMigrate))
+    handler = _CreateHandler(rlib2.R_2_instances_name_migrate, [name], {}, {},
+                             clfactory)
+    job_id = handler.PUT()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpInstanceMigrate))
     self.assertEqual(op.instance_name, name)
     self.assertFalse(hasattr(op, "mode"))
     self.assertFalse(hasattr(op, "cleanup"))
+    self.assertFalse(hasattr(op, "dry_run"))
+    self.assertFalse(hasattr(op, "force"))
 
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
 
-class TestParseRenameInstanceRequest(testutils.GanetiTestCase):
-  def setUp(self):
-    testutils.GanetiTestCase.setUp(self)
-
-    self.Parse = rlib2._ParseRenameInstanceRequest
 
+class TestParseRenameInstanceRequest(testutils.GanetiTestCase):
   def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+
     name = "instij0eeph7"
 
     for new_name in ["ua0aiyoo", "fai3ongi"]:
@@ -334,14 +1189,28 @@ class TestParseRenameInstanceRequest(testutils.GanetiTestCase):
             "name_check": name_check,
             }
 
-          op = self.Parse(name, data)
-          self.assert_(isinstance(op, opcodes.OpInstanceRename))
+          handler = _CreateHandler(rlib2.R_2_instances_name_rename, [name],
+                                   {}, data, clfactory)
+          job_id = handler.PUT()
+
+          cl = clfactory.GetNextClient()
+          self.assertRaises(IndexError, clfactory.GetNextClient)
+
+          (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+          self.assertEqual(job_id, exp_job_id)
+          self.assertTrue(isinstance(op, opcodes.OpInstanceRename))
           self.assertEqual(op.instance_name, name)
           self.assertEqual(op.new_name, new_name)
           self.assertEqual(op.ip_check, ip_check)
           self.assertEqual(op.name_check, name_check)
+          self.assertFalse(hasattr(op, "dry_run"))
+          self.assertFalse(hasattr(op, "force"))
+
+          self.assertRaises(IndexError, cl.GetNextSubmittedJob)
 
   def testDefaults(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+
     name = "instahchie3t"
 
     for new_name in ["thag9mek", "quees7oh"]:
@@ -349,21 +1218,30 @@ class TestParseRenameInstanceRequest(testutils.GanetiTestCase):
         "new_name": new_name,
         }
 
-      op = self.Parse(name, data)
-      self.assert_(isinstance(op, opcodes.OpInstanceRename))
+      handler = _CreateHandler(rlib2.R_2_instances_name_rename, [name],
+                               {}, data, clfactory)
+      job_id = handler.PUT()
+
+      cl = clfactory.GetNextClient()
+      self.assertRaises(IndexError, clfactory.GetNextClient)
+
+      (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+      self.assertEqual(job_id, exp_job_id)
+      self.assertTrue(isinstance(op, opcodes.OpInstanceRename))
       self.assertEqual(op.instance_name, name)
       self.assertEqual(op.new_name, new_name)
       self.assertFalse(hasattr(op, "ip_check"))
       self.assertFalse(hasattr(op, "name_check"))
+      self.assertFalse(hasattr(op, "dry_run"))
+      self.assertFalse(hasattr(op, "force"))
 
+      self.assertRaises(IndexError, cl.GetNextSubmittedJob)
 
-class TestParseModifyInstanceRequest(testutils.GanetiTestCase):
-  def setUp(self):
-    testutils.GanetiTestCase.setUp(self)
-
-    self.Parse = rlib2._ParseModifyInstanceRequest
 
+class TestParseModifyInstanceRequest(unittest.TestCase):
   def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+
     name = "instush8gah"
 
     test_disks = [
@@ -373,7 +1251,7 @@ class TestParseModifyInstanceRequest(testutils.GanetiTestCase):
 
     for osparams in [{}, { "some": "value", "other": "Hello World", }]:
       for hvparams in [{}, { constants.HV_KERNEL_PATH: "/some/kernel", }]:
-        for beparams in [{}, { constants.BE_MEMORY: 128, }]:
+        for beparams in [{}, { constants.BE_MAXMEM: 128, }]:
           for force in [False, True]:
             for nics in [[], [(0, { constants.INIC_IP: "192.0.2.1", })]]:
               for disks in test_disks:
@@ -388,8 +1266,16 @@ class TestParseModifyInstanceRequest(testutils.GanetiTestCase):
                     "disk_template": disk_template,
                     }
 
-                  op = self.Parse(name, data)
-                  self.assert_(isinstance(op, opcodes.OpInstanceSetParams))
+                  handler = _CreateHandler(rlib2.R_2_instances_name_modify,
+                                           [name], {}, data, clfactory)
+                  job_id = handler.PUT()
+
+                  cl = clfactory.GetNextClient()
+                  self.assertRaises(IndexError, clfactory.GetNextClient)
+
+                  (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+                  self.assertEqual(job_id, exp_job_id)
+                  self.assertTrue(isinstance(op, opcodes.OpInstanceSetParams))
                   self.assertEqual(op.instance_name, name)
                   self.assertEqual(op.hvparams, hvparams)
                   self.assertEqual(op.beparams, beparams)
@@ -401,13 +1287,27 @@ class TestParseModifyInstanceRequest(testutils.GanetiTestCase):
                   self.assertFalse(hasattr(op, "remote_node"))
                   self.assertFalse(hasattr(op, "os_name"))
                   self.assertFalse(hasattr(op, "force_variant"))
+                  self.assertFalse(hasattr(op, "dry_run"))
+
+                  self.assertRaises(IndexError, cl.GetNextSubmittedJob)
 
   def testDefaults(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+
     name = "instir8aish31"
 
-    op = self.Parse(name, {})
-    self.assert_(isinstance(op, opcodes.OpInstanceSetParams))
+    handler = _CreateHandler(rlib2.R_2_instances_name_modify,
+                             [name], {}, {}, clfactory)
+    job_id = handler.PUT()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+    self.assertTrue(isinstance(op, opcodes.OpInstanceSetParams))
     self.assertEqual(op.instance_name, name)
+
     for i in ["hvparams", "beparams", "osparams", "force", "nics", "disks",
               "disk_template", "remote_node", "os_name", "force_variant"]:
       self.assertFalse(hasattr(op, i))
@@ -463,45 +1363,66 @@ class TestParseInstanceReinstallRequest(testutils.GanetiTestCase):
     self.assertEqual(ops[1].os_type, "linux1")
     self.assertFalse(ops[1].osparams)
 
+  def testErrors(self):
+    self.assertRaises(http.HttpBadRequest, self.Parse,
+                      "foo", "not a dictionary")
 
-class TestParseRenameGroupRequest(testutils.GanetiTestCase):
-  def setUp(self):
-    testutils.GanetiTestCase.setUp(self)
-
-    self.Parse = rlib2._ParseRenameGroupRequest
 
+class TestGroupRename(unittest.TestCase):
   def test(self):
-    name = "instij0eeph7"
+    clfactory = _FakeClientFactory(_FakeClient)
+
+    name = "group608242564"
     data = {
-      "new_name": "ua0aiyoo",
+      "new_name": "ua0aiyoo15112",
       }
 
-    op = self.Parse(name, data, False)
+    handler = _CreateHandler(rlib2.R_2_groups_name_rename, [name], {}, data,
+                             clfactory)
+    job_id = handler.PUT()
 
-    self.assert_(isinstance(op, opcodes.OpGroupRename))
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+
+    self.assertTrue(isinstance(op, opcodes.OpGroupRename))
     self.assertEqual(op.group_name, name)
-    self.assertEqual(op.new_name, "ua0aiyoo")
+    self.assertEqual(op.new_name, "ua0aiyoo15112")
     self.assertFalse(op.dry_run)
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
 
   def testDryRun(self):
-    name = "instij0eeph7"
+    clfactory = _FakeClientFactory(_FakeClient)
+
+    name = "group28548"
     data = {
       "new_name": "ua0aiyoo",
       }
 
-    op = self.Parse(name, data, True)
+    handler = _CreateHandler(rlib2.R_2_groups_name_rename, [name], {
+      "dry-run": ["1"],
+      }, data, clfactory)
+    job_id = handler.PUT()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
 
-    self.assert_(isinstance(op, opcodes.OpGroupRename))
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+
+    self.assertTrue(isinstance(op, opcodes.OpGroupRename))
     self.assertEqual(op.group_name, name)
     self.assertEqual(op.new_name, "ua0aiyoo")
-    self.assert_(op.dry_run)
+    self.assertTrue(op.dry_run)
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
 
 
-class TestParseInstanceReplaceDisksRequest(unittest.TestCase):
-  def setUp(self):
-    self.Parse = rlib2._ParseInstanceReplaceDisksRequest
-
+class TestInstanceReplaceDisks(unittest.TestCase):
   def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+
     name = "inst22568"
 
     for disks in [range(1, 4), "1,2,3", "1, 2, 3"]:
@@ -511,44 +1432,79 @@ class TestParseInstanceReplaceDisksRequest(unittest.TestCase):
         "iallocator": "myalloc",
         }
 
-      op = self.Parse(name, data)
-      self.assert_(isinstance(op, opcodes.OpInstanceReplaceDisks))
+      handler = _CreateHandler(rlib2.R_2_instances_name_replace_disks,
+                               [name], {}, data, clfactory)
+      job_id = handler.POST()
+
+      cl = clfactory.GetNextClient()
+      self.assertRaises(IndexError, clfactory.GetNextClient)
+
+      (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+      self.assertEqual(job_id, exp_job_id)
+
+      self.assertTrue(isinstance(op, opcodes.OpInstanceReplaceDisks))
+      self.assertEqual(op.instance_name, name)
       self.assertEqual(op.mode, constants.REPLACE_DISK_SEC)
       self.assertEqual(op.disks, [1, 2, 3])
       self.assertEqual(op.iallocator, "myalloc")
+      self.assertRaises(IndexError, cl.GetNextSubmittedJob)
 
   def testDefaults(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+
     name = "inst11413"
     data = {
       "mode": constants.REPLACE_DISK_AUTO,
       }
 
-    op = self.Parse(name, data)
-    self.assert_(isinstance(op, opcodes.OpInstanceReplaceDisks))
+    handler = _CreateHandler(rlib2.R_2_instances_name_replace_disks,
+                             [name], {}, data, clfactory)
+    job_id = handler.POST()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+
+    self.assertTrue(isinstance(op, opcodes.OpInstanceReplaceDisks))
+    self.assertEqual(op.instance_name, name)
     self.assertEqual(op.mode, constants.REPLACE_DISK_AUTO)
     self.assertFalse(hasattr(op, "iallocator"))
     self.assertFalse(hasattr(op, "disks"))
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
 
   def testNoDisks(self):
-    self.assertRaises(http.HttpBadRequest, self.Parse, "inst20661", {})
+    clfactory = _FakeClientFactory(_FakeClient)
+
+    handler = _CreateHandler(rlib2.R_2_instances_name_replace_disks,
+                             ["inst20661"], {}, {}, clfactory)
+    self.assertRaises(http.HttpBadRequest, handler.POST)
 
     for disks in [None, "", {}]:
-      self.assertRaises(http.HttpBadRequest, self.Parse, "inst20661", {
+      handler = _CreateHandler(rlib2.R_2_instances_name_replace_disks,
+                               ["inst20661"], {}, {
         "disks": disks,
-        })
+        }, clfactory)
+      self.assertRaises(http.HttpBadRequest, handler.POST)
 
   def testWrong(self):
-    self.assertRaises(http.HttpBadRequest, self.Parse, "inst",
-                      { "mode": constants.REPLACE_DISK_AUTO,
-                        "disks": "hello world",
-                      })
+    clfactory = _FakeClientFactory(_FakeClient)
+
+    data = {
+      "mode": constants.REPLACE_DISK_AUTO,
+      "disks": "hello world",
+      }
 
+    handler = _CreateHandler(rlib2.R_2_instances_name_replace_disks,
+                             ["foo"], {}, data, clfactory)
+    self.assertRaises(http.HttpBadRequest, handler.POST)
 
-class TestParseModifyGroupRequest(unittest.TestCase):
-  def setUp(self):
-    self.Parse = rlib2._ParseModifyGroupRequest
 
+class TestGroupModify(unittest.TestCase):
   def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+
     name = "group6002"
 
     for policy in constants.VALID_ALLOC_POLICIES:
@@ -556,34 +1512,60 @@ class TestParseModifyGroupRequest(unittest.TestCase):
         "alloc_policy": policy,
         }
 
-      op = self.Parse(name, data)
-      self.assert_(isinstance(op, opcodes.OpGroupSetParams))
+      handler = _CreateHandler(rlib2.R_2_groups_name_modify, [name], {}, data,
+                               clfactory)
+      job_id = handler.PUT()
+
+      cl = clfactory.GetNextClient()
+      self.assertRaises(IndexError, clfactory.GetNextClient)
+
+      (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+      self.assertEqual(job_id, exp_job_id)
+
+      self.assertTrue(isinstance(op, opcodes.OpGroupSetParams))
       self.assertEqual(op.group_name, name)
       self.assertEqual(op.alloc_policy, policy)
+      self.assertFalse(hasattr(op, "dry_run"))
+      self.assertRaises(IndexError, cl.GetNextSubmittedJob)
 
   def testUnknownPolicy(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+
     data = {
       "alloc_policy": "_unknown_policy_",
       }
 
-    self.assertRaises(http.HttpBadRequest, self.Parse, "name", data)
+    handler = _CreateHandler(rlib2.R_2_groups_name_modify, ["xyz"], {}, data,
+                             clfactory)
+    self.assertRaises(http.HttpBadRequest, handler.PUT)
+    self.assertRaises(IndexError, clfactory.GetNextClient)
 
   def testDefaults(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+
     name = "group6679"
-    data = {}
 
-    op = self.Parse(name, data)
-    self.assert_(isinstance(op, opcodes.OpGroupSetParams))
+    handler = _CreateHandler(rlib2.R_2_groups_name_modify, [name], {}, {},
+                             clfactory)
+    job_id = handler.PUT()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+
+    self.assertTrue(isinstance(op, opcodes.OpGroupSetParams))
     self.assertEqual(op.group_name, name)
     self.assertFalse(hasattr(op, "alloc_policy"))
+    self.assertFalse(hasattr(op, "dry_run"))
+    self.assertRaises(IndexError, cl.GetNextSubmittedJob)
 
 
-class TestParseCreateGroupRequest(unittest.TestCase):
-  def setUp(self):
-    self.Parse = rlib2._ParseCreateGroupRequest
-
+class TestGroupAdd(unittest.TestCase):
   def test(self):
     name = "group3618"
+    clfactory = _FakeClientFactory(_FakeClient)
 
     for policy in constants.VALID_ALLOC_POLICIES:
       data = {
@@ -591,40 +1573,162 @@ class TestParseCreateGroupRequest(unittest.TestCase):
         "alloc_policy": policy,
         }
 
-      op = self.Parse(data, False)
-      self.assert_(isinstance(op, opcodes.OpGroupAdd))
+      handler = _CreateHandler(rlib2.R_2_groups, [], {}, data,
+                               clfactory)
+      job_id = handler.POST()
+
+      cl = clfactory.GetNextClient()
+      self.assertRaises(IndexError, clfactory.GetNextClient)
+
+      (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+      self.assertEqual(job_id, exp_job_id)
+
+      self.assertTrue(isinstance(op, opcodes.OpGroupAdd))
       self.assertEqual(op.group_name, name)
       self.assertEqual(op.alloc_policy, policy)
       self.assertFalse(op.dry_run)
+      self.assertRaises(IndexError, cl.GetNextSubmittedJob)
 
   def testUnknownPolicy(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+
     data = {
       "alloc_policy": "_unknown_policy_",
       }
 
-    self.assertRaises(http.HttpBadRequest, self.Parse, "name", data)
+    handler = _CreateHandler(rlib2.R_2_groups, [], {}, data, clfactory)
+    self.assertRaises(http.HttpBadRequest, handler.POST)
+    self.assertRaises(IndexError, clfactory.GetNextClient)
 
   def testDefaults(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+
     name = "group15395"
     data = {
       "group_name": name,
       }
 
-    op = self.Parse(data, True)
-    self.assert_(isinstance(op, opcodes.OpGroupAdd))
+    handler = _CreateHandler(rlib2.R_2_groups, [], {}, data, clfactory)
+    job_id = handler.POST()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+
+    self.assertTrue(isinstance(op, opcodes.OpGroupAdd))
     self.assertEqual(op.group_name, name)
     self.assertFalse(hasattr(op, "alloc_policy"))
-    self.assertTrue(op.dry_run)
+    self.assertFalse(op.dry_run)
 
   def testLegacyName(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+
     name = "group29852"
     data = {
       "name": name,
       }
 
-    op = self.Parse(data, True)
-    self.assert_(isinstance(op, opcodes.OpGroupAdd))
+    handler = _CreateHandler(rlib2.R_2_groups, [], {
+      "dry-run": ["1"],
+      }, data, clfactory)
+    job_id = handler.POST()
+
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+
+    (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+    self.assertEqual(job_id, exp_job_id)
+
+    self.assertTrue(isinstance(op, opcodes.OpGroupAdd))
     self.assertEqual(op.group_name, name)
+    self.assertFalse(hasattr(op, "alloc_policy"))
+    self.assertTrue(op.dry_run)
+
+
+class TestNodeRole(unittest.TestCase):
+  def test(self):
+    clfactory = _FakeClientFactory(_FakeClient)
+
+    for role in rlib2._NR_MAP.values():
+      handler = _CreateHandler(rlib2.R_2_nodes_name_role,
+                               ["node-z"], {}, role, clfactory)
+      if role == rlib2._NR_MASTER:
+        self.assertRaises(http.HttpBadRequest, handler.PUT)
+      else:
+        job_id = handler.PUT()
+
+        cl = clfactory.GetNextClient()
+        self.assertRaises(IndexError, clfactory.GetNextClient)
+
+        (exp_job_id, (op, )) = cl.GetNextSubmittedJob()
+        self.assertEqual(job_id, exp_job_id)
+        self.assertTrue(isinstance(op, opcodes.OpNodeSetParams))
+        self.assertEqual(op.node_name, "node-z")
+        self.assertFalse(op.force)
+        self.assertFalse(hasattr(op, "dry_run"))
+
+        if role == rlib2._NR_REGULAR:
+          self.assertFalse(op.drained)
+          self.assertFalse(op.offline)
+          self.assertFalse(op.master_candidate)
+        elif role == rlib2._NR_MASTER_CANDIDATE:
+          self.assertFalse(op.drained)
+          self.assertFalse(op.offline)
+          self.assertTrue(op.master_candidate)
+        elif role == rlib2._NR_DRAINED:
+          self.assertTrue(op.drained)
+          self.assertFalse(op.offline)
+          self.assertFalse(op.master_candidate)
+        elif role == rlib2._NR_OFFLINE:
+          self.assertFalse(op.drained)
+          self.assertTrue(op.offline)
+          self.assertFalse(op.master_candidate)
+        else:
+          self.fail("Unknown role '%s'" % role)
+
+      self.assertRaises(IndexError, cl.GetNextSubmittedJob)
+
+
+class TestSimpleResources(unittest.TestCase):
+  def setUp(self):
+    self.clfactory = _FakeClientFactory(_FakeClient)
+
+  def tearDown(self):
+    self.assertRaises(IndexError, self.clfactory.GetNextClient)
+
+  def testFeatures(self):
+    handler = _CreateHandler(rlib2.R_2_features, [], {}, None, self.clfactory)
+    self.assertEqual(set(handler.GET()), rlib2.ALL_FEATURES)
+
+  def testEmpty(self):
+    for cls in [rlib2.R_root, rlib2.R_2]:
+      handler = _CreateHandler(cls, [], {}, None, self.clfactory)
+      self.assertTrue(handler.GET() is None)
+
+  def testVersion(self):
+    handler = _CreateHandler(rlib2.R_version, [], {}, None, self.clfactory)
+    self.assertEqual(handler.GET(), constants.RAPI_VERSION)
+
+
+class TestClusterInfo(unittest.TestCase):
+  class _ClusterInfoClient:
+    def __init__(self):
+      self.cluster_info = None
+
+    def QueryClusterInfo(self):
+      assert self.cluster_info is None
+      self.cluster_info = object()
+      return self.cluster_info
+
+  def test(self):
+    clfactory = _FakeClientFactory(self._ClusterInfoClient)
+    handler = _CreateHandler(rlib2.R_2_info, [], {}, None, clfactory)
+    result = handler.GET()
+    cl = clfactory.GetNextClient()
+    self.assertRaises(IndexError, clfactory.GetNextClient)
+    self.assertEqual(result, cl.cluster_info)
 
 
 if __name__ == '__main__':
diff --git a/test/ganeti.rapi.testutils_unittest.py b/test/ganeti.rapi.testutils_unittest.py
new file mode 100755 (executable)
index 0000000..53dcaa7
--- /dev/null
@@ -0,0 +1,193 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2012 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 testing ganeti.rapi.testutils"""
+
+import unittest
+
+from ganeti import compat
+from ganeti import constants
+from ganeti import errors
+from ganeti import opcodes
+from ganeti import luxi
+from ganeti import rapi
+from ganeti import utils
+
+import ganeti.rapi.testutils
+import ganeti.rapi.client
+
+import testutils
+
+
+KNOWN_UNUSED_LUXI = frozenset([
+  luxi.REQ_SUBMIT_MANY_JOBS,
+  luxi.REQ_ARCHIVE_JOB,
+  luxi.REQ_AUTO_ARCHIVE_JOBS,
+  luxi.REQ_QUERY_EXPORTS,
+  luxi.REQ_QUERY_CONFIG_VALUES,
+  luxi.REQ_QUERY_TAGS,
+  luxi.REQ_SET_DRAIN_FLAG,
+  luxi.REQ_SET_WATCHER_PAUSE,
+  ])
+
+
+# Global variable for storing used LUXI calls
+_used_luxi_calls = None
+
+
+class TestHideInternalErrors(unittest.TestCase):
+  def test(self):
+    def inner():
+      raise errors.GenericError("error")
+
+    fn = rapi.testutils._HideInternalErrors(inner)
+
+    self.assertRaises(rapi.testutils.VerificationError, fn)
+
+
+class TestVerifyOpInput(unittest.TestCase):
+  def testUnknownOpId(self):
+    voi = rapi.testutils.VerifyOpInput
+
+    self.assertRaises(rapi.testutils.VerificationError, voi, "UNK_OP_ID", None)
+
+  def testUnknownParameter(self):
+    voi = rapi.testutils.VerifyOpInput
+
+    self.assertRaises(rapi.testutils.VerificationError, voi,
+      opcodes.OpClusterRename.OP_ID, {
+      "unk": "unk",
+      })
+
+  def testWrongParameterValue(self):
+    voi = rapi.testutils.VerifyOpInput
+    self.assertRaises(rapi.testutils.VerificationError, voi,
+      opcodes.OpClusterRename.OP_ID, {
+      "name": object(),
+      })
+
+  def testSuccess(self):
+    voi = rapi.testutils.VerifyOpInput
+    voi(opcodes.OpClusterRename.OP_ID, {
+      "name": "new-name.example.com",
+      })
+
+
+class TestVerifyOpResult(unittest.TestCase):
+  def testSuccess(self):
+    vor = rapi.testutils.VerifyOpResult
+
+    vor(opcodes.OpClusterVerify.OP_ID, {
+      constants.JOB_IDS_KEY: [
+        (False, "error message"),
+        ],
+      })
+
+  def testWrongResult(self):
+    vor = rapi.testutils.VerifyOpResult
+
+    self.assertRaises(rapi.testutils.VerificationError, vor,
+      opcodes.OpClusterVerify.OP_ID, [])
+
+  def testNoResultCheck(self):
+    vor = rapi.testutils.VerifyOpResult
+
+    assert opcodes.OpTestDummy.OP_RESULT is None
+
+    vor(opcodes.OpTestDummy.OP_ID, None)
+
+
+class TestInputTestClient(unittest.TestCase):
+  def setUp(self):
+    self.cl = rapi.testutils.InputTestClient()
+
+  def tearDown(self):
+    _used_luxi_calls.update(self.cl._GetLuxiCalls())
+
+  def testGetInfo(self):
+    self.assertTrue(self.cl.GetInfo() is NotImplemented)
+
+  def testPrepareExport(self):
+    result = self.cl.PrepareExport("inst1.example.com",
+                                   constants.EXPORT_MODE_LOCAL)
+    self.assertTrue(result is NotImplemented)
+    self.assertRaises(rapi.testutils.VerificationError, self.cl.PrepareExport,
+                      "inst1.example.com", "###invalid###")
+
+  def testGetJobs(self):
+    self.assertTrue(self.cl.GetJobs() is NotImplemented)
+
+  def testQuery(self):
+    result = self.cl.Query(constants.QR_NODE, ["name"])
+    self.assertTrue(result is NotImplemented)
+
+  def testQueryFields(self):
+    result = self.cl.QueryFields(constants.QR_INSTANCE)
+    self.assertTrue(result is NotImplemented)
+
+  def testCancelJob(self):
+    self.assertTrue(self.cl.CancelJob("1") is NotImplemented)
+
+  def testGetNodes(self):
+    self.assertTrue(self.cl.GetNodes() is NotImplemented)
+
+  def testGetInstances(self):
+    self.assertTrue(self.cl.GetInstances() is NotImplemented)
+
+  def testGetGroups(self):
+    self.assertTrue(self.cl.GetGroups() is NotImplemented)
+
+  def testWaitForJobChange(self):
+    result = self.cl.WaitForJobChange("1", ["id"], None, None)
+    self.assertTrue(result is NotImplemented)
+
+
+class CustomTestRunner(unittest.TextTestRunner):
+  def run(self, *args):
+    global _used_luxi_calls
+    assert _used_luxi_calls is None
+
+    diff = (KNOWN_UNUSED_LUXI - luxi.REQ_ALL)
+    assert not diff, "Non-existing LUXI calls listed as unused: %s" % diff
+
+    _used_luxi_calls = set()
+    try:
+      # Run actual tests
+      result = unittest.TextTestRunner.run(self, *args)
+
+      diff = _used_luxi_calls & KNOWN_UNUSED_LUXI
+      if diff:
+        raise AssertionError("LUXI methods marked as unused were called: %s" %
+                             utils.CommaJoin(diff))
+
+      diff = (luxi.REQ_ALL - KNOWN_UNUSED_LUXI - _used_luxi_calls)
+      if diff:
+        raise AssertionError("The following LUXI methods were not used: %s" %
+                             utils.CommaJoin(diff))
+    finally:
+      # Reset global variable
+      _used_luxi_calls = None
+
+    return result
+
+
+if __name__ == "__main__":
+  testutils.GanetiTestProgram(testRunner=CustomTestRunner)
index ce09d8b..16857b5 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 #
 
-# Copyright (C) 2010 Google Inc.
+# Copyright (C) 2010, 2011, 2012 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
 import os
 import sys
 import unittest
+import random
+import tempfile
 
 from ganeti import constants
 from ganeti import compat
 from ganeti import rpc
+from ganeti import rpc_defs
 from ganeti import http
 from ganeti import errors
 from ganeti import serializer
+from ganeti import objects
+from ganeti import backend
 
 import testutils
+import mocks
 
 
-class TestTimeouts(unittest.TestCase):
-  def test(self):
-    names = [name[len("call_"):] for name in dir(rpc.RpcRunner)
-             if name.startswith("call_")]
-    self.assertEqual(len(names), len(rpc._TIMEOUTS))
-    self.assertFalse([name for name in names
-                      if not (rpc._TIMEOUTS[name] is None or
-                              rpc._TIMEOUTS[name] > 0)])
-
-
-class FakeHttpPool:
+class _FakeRequestProcessor:
   def __init__(self, response_fn):
     self._response_fn = response_fn
     self.reqcount = 0
 
-  def ProcessRequests(self, reqs):
+  def __call__(self, reqs, lock_monitor_cb=None):
+    assert lock_monitor_cb is None or callable(lock_monitor_cb)
     for req in reqs:
       self.reqcount += 1
       self._response_fn(req)
@@ -64,24 +61,32 @@ def GetFakeSimpleStoreClass(fn):
   return FakeSimpleStore
 
 
-class TestClient(unittest.TestCase):
+def _RaiseNotImplemented():
+  """Simple wrapper to raise NotImplementedError.
+
+  """
+  raise NotImplementedError
+
+
+class TestRpcProcessor(unittest.TestCase):
   def _FakeAddressLookup(self, map):
     return lambda node_list: [map.get(node) for node in node_list]
 
   def _GetVersionResponse(self, req):
-    self.assertEqual(req.host, "localhost")
+    self.assertEqual(req.host, "127.0.0.1")
     self.assertEqual(req.port, 24094)
     self.assertEqual(req.path, "/version")
+    self.assertEqual(req.read_timeout, rpc._TMO_URGENT)
     req.success = True
     req.resp_status_code = http.HTTP_OK
     req.resp_body = serializer.DumpJson((True, 123))
 
   def testVersionSuccess(self):
-    fn = self._FakeAddressLookup({"localhost": "localhost"})
-    client = rpc.Client("version", None, 24094, address_lookup_fn=fn)
-    client.ConnectNode("localhost")
-    pool = FakeHttpPool(self._GetVersionResponse)
-    result = client.GetResults(http_pool=pool)
+    resolver = rpc._StaticResolver(["127.0.0.1"])
+    http_proc = _FakeRequestProcessor(self._GetVersionResponse)
+    proc = rpc._RpcProcessor(resolver, 24094)
+    result = proc(["localhost"], "version", {"localhost": ""}, 60,
+                  NotImplemented, _req_process_fn=http_proc)
     self.assertEqual(result.keys(), ["localhost"])
     lhresp = result["localhost"]
     self.assertFalse(lhresp.offline)
@@ -90,7 +95,58 @@ class TestClient(unittest.TestCase):
     self.assertEqual(lhresp.payload, 123)
     self.assertEqual(lhresp.call, "version")
     lhresp.Raise("should not raise")
-    self.assertEqual(pool.reqcount, 1)
+    self.assertEqual(http_proc.reqcount, 1)
+
+  def _ReadTimeoutResponse(self, req):
+    self.assertEqual(req.host, "192.0.2.13")
+    self.assertEqual(req.port, 19176)
+    self.assertEqual(req.path, "/version")
+    self.assertEqual(req.read_timeout, 12356)
+    req.success = True
+    req.resp_status_code = http.HTTP_OK
+    req.resp_body = serializer.DumpJson((True, -1))
+
+  def testReadTimeout(self):
+    resolver = rpc._StaticResolver(["192.0.2.13"])
+    http_proc = _FakeRequestProcessor(self._ReadTimeoutResponse)
+    proc = rpc._RpcProcessor(resolver, 19176)
+    host = "node31856"
+    body = {host: ""}
+    result = proc([host], "version", body, 12356, NotImplemented,
+                  _req_process_fn=http_proc)
+    self.assertEqual(result.keys(), [host])
+    lhresp = result[host]
+    self.assertFalse(lhresp.offline)
+    self.assertEqual(lhresp.node, host)
+    self.assertFalse(lhresp.fail_msg)
+    self.assertEqual(lhresp.payload, -1)
+    self.assertEqual(lhresp.call, "version")
+    lhresp.Raise("should not raise")
+    self.assertEqual(http_proc.reqcount, 1)
+
+  def testOfflineNode(self):
+    resolver = rpc._StaticResolver([rpc._OFFLINE])
+    http_proc = _FakeRequestProcessor(NotImplemented)
+    proc = rpc._RpcProcessor(resolver, 30668)
+    host = "n17296"
+    body = {host: ""}
+    result = proc([host], "version", body, 60, NotImplemented,
+                  _req_process_fn=http_proc)
+    self.assertEqual(result.keys(), [host])
+    lhresp = result[host]
+    self.assertTrue(lhresp.offline)
+    self.assertEqual(lhresp.node, host)
+    self.assertTrue(lhresp.fail_msg)
+    self.assertFalse(lhresp.payload)
+    self.assertEqual(lhresp.call, "version")
+
+    # With a message
+    self.assertRaises(errors.OpExecError, lhresp.Raise, "should raise")
+
+    # No message
+    self.assertRaises(errors.OpExecError, lhresp.Raise, None)
+
+    self.assertEqual(http_proc.reqcount, 0)
 
   def _GetMultiVersionResponse(self, req):
     self.assert_(req.host.startswith("node"))
@@ -102,12 +158,12 @@ class TestClient(unittest.TestCase):
 
   def testMultiVersionSuccess(self):
     nodes = ["node%s" % i for i in range(50)]
-    fn = self._FakeAddressLookup(dict(zip(nodes, nodes)))
-    client = rpc.Client("version", None, 23245, address_lookup_fn=fn)
-    client.ConnectList(nodes)
-
-    pool = FakeHttpPool(self._GetMultiVersionResponse)
-    result = client.GetResults(http_pool=pool)
+    body = dict((n, "") for n in nodes)
+    resolver = rpc._StaticResolver(nodes)
+    http_proc = _FakeRequestProcessor(self._GetMultiVersionResponse)
+    proc = rpc._RpcProcessor(resolver, 23245)
+    result = proc(nodes, "version", body, 60, NotImplemented,
+                  _req_process_fn=http_proc)
     self.assertEqual(sorted(result.keys()), sorted(nodes))
 
     for name in nodes:
@@ -119,30 +175,34 @@ class TestClient(unittest.TestCase):
       self.assertEqual(lhresp.call, "version")
       lhresp.Raise("should not raise")
 
-    self.assertEqual(pool.reqcount, len(nodes))
+    self.assertEqual(http_proc.reqcount, len(nodes))
 
-  def _GetVersionResponseFail(self, req):
+  def _GetVersionResponseFail(self, errinfo, req):
     self.assertEqual(req.path, "/version")
     req.success = True
     req.resp_status_code = http.HTTP_OK
-    req.resp_body = serializer.DumpJson((False, "Unknown error"))
+    req.resp_body = serializer.DumpJson((False, errinfo))
 
   def testVersionFailure(self):
-    lookup_map = {"aef9ur4i.example.com": "aef9ur4i.example.com"}
-    fn = self._FakeAddressLookup(lookup_map)
-    client = rpc.Client("version", None, 5903, address_lookup_fn=fn)
-    client.ConnectNode("aef9ur4i.example.com")
-    pool = FakeHttpPool(self._GetVersionResponseFail)
-    result = client.GetResults(http_pool=pool)
-    self.assertEqual(result.keys(), ["aef9ur4i.example.com"])
-    lhresp = result["aef9ur4i.example.com"]
-    self.assertFalse(lhresp.offline)
-    self.assertEqual(lhresp.node, "aef9ur4i.example.com")
-    self.assert_(lhresp.fail_msg)
-    self.assertFalse(lhresp.payload)
-    self.assertEqual(lhresp.call, "version")
-    self.assertRaises(errors.OpExecError, lhresp.Raise, "failed")
-    self.assertEqual(pool.reqcount, 1)
+    resolver = rpc._StaticResolver(["aef9ur4i.example.com"])
+    proc = rpc._RpcProcessor(resolver, 5903)
+    for errinfo in [None, "Unknown error"]:
+      http_proc = \
+        _FakeRequestProcessor(compat.partial(self._GetVersionResponseFail,
+                                             errinfo))
+      host = "aef9ur4i.example.com"
+      body = {host: ""}
+      result = proc(body.keys(), "version", body, 60, NotImplemented,
+                    _req_process_fn=http_proc)
+      self.assertEqual(result.keys(), [host])
+      lhresp = result[host]
+      self.assertFalse(lhresp.offline)
+      self.assertEqual(lhresp.node, host)
+      self.assert_(lhresp.fail_msg)
+      self.assertFalse(lhresp.payload)
+      self.assertEqual(lhresp.call, "version")
+      self.assertRaises(errors.OpExecError, lhresp.Raise, "failed")
+      self.assertEqual(http_proc.reqcount, 1)
 
   def _GetHttpErrorResponse(self, httperrnodes, failnodes, req):
     self.assertEqual(req.path, "/vg_list")
@@ -167,7 +227,8 @@ class TestClient(unittest.TestCase):
 
   def testHttpError(self):
     nodes = ["uaf6pbbv%s" % i for i in range(50)]
-    fn = self._FakeAddressLookup(dict(zip(nodes, nodes)))
+    body = dict((n, "") for n in nodes)
+    resolver = rpc._StaticResolver(nodes)
 
     httperrnodes = set(nodes[1::7])
     self.assertEqual(len(httperrnodes), 7)
@@ -177,12 +238,12 @@ class TestClient(unittest.TestCase):
 
     self.assertEqual(len(set(nodes) - failnodes - httperrnodes), 29)
 
-    client = rpc.Client("vg_list", None, 15165, address_lookup_fn=fn)
-    client.ConnectList(nodes)
-
-    pool = FakeHttpPool(compat.partial(self._GetHttpErrorResponse,
-                                       httperrnodes, failnodes))
-    result = client.GetResults(http_pool=pool)
+    proc = rpc._RpcProcessor(resolver, 15165)
+    http_proc = \
+      _FakeRequestProcessor(compat.partial(self._GetHttpErrorResponse,
+                                           httperrnodes, failnodes))
+    result = proc(nodes, "vg_list", body, rpc._TMO_URGENT, NotImplemented,
+                  _req_process_fn=http_proc)
     self.assertEqual(sorted(result.keys()), sorted(nodes))
 
     for name in nodes:
@@ -203,7 +264,7 @@ class TestClient(unittest.TestCase):
         self.assertEqual(lhresp.payload, hash(name))
         lhresp.Raise("should not raise")
 
-    self.assertEqual(pool.reqcount, len(nodes))
+    self.assertEqual(http_proc.reqcount, len(nodes))
 
   def _GetInvalidResponseA(self, req):
     self.assertEqual(req.path, "/version")
@@ -219,58 +280,592 @@ class TestClient(unittest.TestCase):
     req.resp_body = serializer.DumpJson("invalid response")
 
   def testInvalidResponse(self):
-    lookup_map = {"oqo7lanhly.example.com": "oqo7lanhly.example.com"}
-    fn = self._FakeAddressLookup(lookup_map)
-    client = rpc.Client("version", None, 19978, address_lookup_fn=fn)
+    resolver = rpc._StaticResolver(["oqo7lanhly.example.com"])
+    proc = rpc._RpcProcessor(resolver, 19978)
+
     for fn in [self._GetInvalidResponseA, self._GetInvalidResponseB]:
-      client.ConnectNode("oqo7lanhly.example.com")
-      pool = FakeHttpPool(fn)
-      result = client.GetResults(http_pool=pool)
-      self.assertEqual(result.keys(), ["oqo7lanhly.example.com"])
-      lhresp = result["oqo7lanhly.example.com"]
+      http_proc = _FakeRequestProcessor(fn)
+      host = "oqo7lanhly.example.com"
+      body = {host: ""}
+      result = proc([host], "version", body, 60, NotImplemented,
+                    _req_process_fn=http_proc)
+      self.assertEqual(result.keys(), [host])
+      lhresp = result[host]
       self.assertFalse(lhresp.offline)
-      self.assertEqual(lhresp.node, "oqo7lanhly.example.com")
+      self.assertEqual(lhresp.node, host)
       self.assert_(lhresp.fail_msg)
       self.assertFalse(lhresp.payload)
       self.assertEqual(lhresp.call, "version")
       self.assertRaises(errors.OpExecError, lhresp.Raise, "failed")
-      self.assertEqual(pool.reqcount, 1)
+      self.assertEqual(http_proc.reqcount, 1)
+
+  def _GetBodyTestResponse(self, test_data, req):
+    self.assertEqual(req.host, "192.0.2.84")
+    self.assertEqual(req.port, 18700)
+    self.assertEqual(req.path, "/upload_file")
+    self.assertEqual(serializer.LoadJson(req.post_data), test_data)
+    req.success = True
+    req.resp_status_code = http.HTTP_OK
+    req.resp_body = serializer.DumpJson((True, None))
+
+  def testResponseBody(self):
+    test_data = {
+      "Hello": "World",
+      "xyz": range(10),
+      }
+    resolver = rpc._StaticResolver(["192.0.2.84"])
+    http_proc = _FakeRequestProcessor(compat.partial(self._GetBodyTestResponse,
+                                                     test_data))
+    proc = rpc._RpcProcessor(resolver, 18700)
+    host = "node19759"
+    body = {host: serializer.DumpJson(test_data)}
+    result = proc([host], "upload_file", body, 30, NotImplemented,
+                  _req_process_fn=http_proc)
+    self.assertEqual(result.keys(), [host])
+    lhresp = result[host]
+    self.assertFalse(lhresp.offline)
+    self.assertEqual(lhresp.node, host)
+    self.assertFalse(lhresp.fail_msg)
+    self.assertEqual(lhresp.payload, None)
+    self.assertEqual(lhresp.call, "upload_file")
+    lhresp.Raise("should not raise")
+    self.assertEqual(http_proc.reqcount, 1)
+
 
-  def testAddressLookupSimpleStore(self):
+class TestSsconfResolver(unittest.TestCase):
+  def testSsconfLookup(self):
     addr_list = ["192.0.2.%d" % n for n in range(0, 255, 13)]
     node_list = ["node%d.example.com" % n for n in range(0, 255, 13)]
-    node_addr_list = [ " ".join(t) for t in zip(node_list, addr_list)]
+    node_addr_list = [" ".join(t) for t in zip(node_list, addr_list)]
     ssc = GetFakeSimpleStoreClass(lambda _: node_addr_list)
-    result = rpc._AddressLookup(node_list, ssc=ssc)
-    self.assertEqual(result, addr_list)
+    result = rpc._SsconfResolver(True, node_list, NotImplemented,
+                                 ssc=ssc, nslookup_fn=NotImplemented)
+    self.assertEqual(result, zip(node_list, addr_list))
 
-  def testAddressLookupNSLookup(self):
+  def testNsLookup(self):
     addr_list = ["192.0.2.%d" % n for n in range(0, 255, 13)]
     node_list = ["node%d.example.com" % n for n in range(0, 255, 13)]
     ssc = GetFakeSimpleStoreClass(lambda _: [])
     node_addr_map = dict(zip(node_list, addr_list))
     nslookup_fn = lambda name, family=None: node_addr_map.get(name)
-    result = rpc._AddressLookup(node_list, ssc=ssc, nslookup_fn=nslookup_fn)
-    self.assertEqual(result, addr_list)
+    result = rpc._SsconfResolver(True, node_list, NotImplemented,
+                                 ssc=ssc, nslookup_fn=nslookup_fn)
+    self.assertEqual(result, zip(node_list, addr_list))
+
+  def testDisabledSsconfIp(self):
+    addr_list = ["192.0.2.%d" % n for n in range(0, 255, 13)]
+    node_list = ["node%d.example.com" % n for n in range(0, 255, 13)]
+    ssc = GetFakeSimpleStoreClass(_RaiseNotImplemented)
+    node_addr_map = dict(zip(node_list, addr_list))
+    nslookup_fn = lambda name, family=None: node_addr_map.get(name)
+    result = rpc._SsconfResolver(False, node_list, NotImplemented,
+                                 ssc=ssc, nslookup_fn=nslookup_fn)
+    self.assertEqual(result, zip(node_list, addr_list))
 
-  def testAddressLookupBoth(self):
+  def testBothLookups(self):
     addr_list = ["192.0.2.%d" % n for n in range(0, 255, 13)]
     node_list = ["node%d.example.com" % n for n in range(0, 255, 13)]
     n = len(addr_list) / 2
-    node_addr_list = [ " ".join(t) for t in zip(node_list[n:], addr_list[n:])]
+    node_addr_list = [" ".join(t) for t in zip(node_list[n:], addr_list[n:])]
     ssc = GetFakeSimpleStoreClass(lambda _: node_addr_list)
     node_addr_map = dict(zip(node_list[:n], addr_list[:n]))
     nslookup_fn = lambda name, family=None: node_addr_map.get(name)
-    result = rpc._AddressLookup(node_list, ssc=ssc, nslookup_fn=nslookup_fn)
-    self.assertEqual(result, addr_list)
+    result = rpc._SsconfResolver(True, node_list, NotImplemented,
+                                 ssc=ssc, nslookup_fn=nslookup_fn)
+    self.assertEqual(result, zip(node_list, addr_list))
 
   def testAddressLookupIPv6(self):
-    addr_list = ["2001:db8::%d" % n for n in range(0, 255, 13)]
-    node_list = ["node%d.example.com" % n for n in range(0, 255, 13)]
-    node_addr_list = [ " ".join(t) for t in zip(node_list, addr_list)]
+    addr_list = ["2001:db8::%d" % n for n in range(0, 255, 11)]
+    node_list = ["node%d.example.com" % n for n in range(0, 255, 11)]
+    node_addr_list = [" ".join(t) for t in zip(node_list, addr_list)]
     ssc = GetFakeSimpleStoreClass(lambda _: node_addr_list)
-    result = rpc._AddressLookup(node_list, ssc=ssc)
-    self.assertEqual(result, addr_list)
+    result = rpc._SsconfResolver(True, node_list, NotImplemented,
+                                 ssc=ssc, nslookup_fn=NotImplemented)
+    self.assertEqual(result, zip(node_list, addr_list))
+
+
+class TestStaticResolver(unittest.TestCase):
+  def test(self):
+    addresses = ["192.0.2.%d" % n for n in range(0, 123, 7)]
+    nodes = ["node%s.example.com" % n for n in range(0, 123, 7)]
+    res = rpc._StaticResolver(addresses)
+    self.assertEqual(res(nodes, NotImplemented), zip(nodes, addresses))
+
+  def testWrongLength(self):
+    res = rpc._StaticResolver([])
+    self.assertRaises(AssertionError, res, ["abc"], NotImplemented)
+
+
+class TestNodeConfigResolver(unittest.TestCase):
+  @staticmethod
+  def _GetSingleOnlineNode(name):
+    assert name == "node90.example.com"
+    return objects.Node(name=name, offline=False, primary_ip="192.0.2.90")
+
+  @staticmethod
+  def _GetSingleOfflineNode(name):
+    assert name == "node100.example.com"
+    return objects.Node(name=name, offline=True, primary_ip="192.0.2.100")
+
+  def testSingleOnline(self):
+    self.assertEqual(rpc._NodeConfigResolver(self._GetSingleOnlineNode,
+                                             NotImplemented,
+                                             ["node90.example.com"], None),
+                     [("node90.example.com", "192.0.2.90")])
+
+  def testSingleOffline(self):
+    self.assertEqual(rpc._NodeConfigResolver(self._GetSingleOfflineNode,
+                                             NotImplemented,
+                                             ["node100.example.com"], None),
+                     [("node100.example.com", rpc._OFFLINE)])
+
+  def testSingleOfflineWithAcceptOffline(self):
+    fn = self._GetSingleOfflineNode
+    assert fn("node100.example.com").offline
+    self.assertEqual(rpc._NodeConfigResolver(fn, NotImplemented,
+                                             ["node100.example.com"],
+                                             rpc_defs.ACCEPT_OFFLINE_NODE),
+                     [("node100.example.com", "192.0.2.100")])
+    for i in [False, True, "", "Hello", 0, 1]:
+      self.assertRaises(AssertionError, rpc._NodeConfigResolver,
+                        fn, NotImplemented, ["node100.example.com"], i)
+
+  def testUnknownSingleNode(self):
+    self.assertEqual(rpc._NodeConfigResolver(lambda _: None, NotImplemented,
+                                             ["node110.example.com"], None),
+                     [("node110.example.com", "node110.example.com")])
+
+  def testMultiEmpty(self):
+    self.assertEqual(rpc._NodeConfigResolver(NotImplemented,
+                                             lambda: {},
+                                             [], None),
+                     [])
+
+  def testMultiSomeOffline(self):
+    nodes = dict(("node%s.example.com" % i,
+                  objects.Node(name="node%s.example.com" % i,
+                               offline=((i % 3) == 0),
+                               primary_ip="192.0.2.%s" % i))
+                  for i in range(1, 255))
+
+    # Resolve no names
+    self.assertEqual(rpc._NodeConfigResolver(NotImplemented,
+                                             lambda: nodes,
+                                             [], None),
+                     [])
+
+    # Offline, online and unknown hosts
+    self.assertEqual(rpc._NodeConfigResolver(NotImplemented,
+                                             lambda: nodes,
+                                             ["node3.example.com",
+                                              "node92.example.com",
+                                              "node54.example.com",
+                                              "unknown.example.com",],
+                                             None), [
+      ("node3.example.com", rpc._OFFLINE),
+      ("node92.example.com", "192.0.2.92"),
+      ("node54.example.com", rpc._OFFLINE),
+      ("unknown.example.com", "unknown.example.com"),
+      ])
+
+
+class TestCompress(unittest.TestCase):
+  def test(self):
+    for data in ["", "Hello", "Hello World!\nnew\nlines"]:
+      self.assertEqual(rpc._Compress(data),
+                       (constants.RPC_ENCODING_NONE, data))
+
+    for data in [512 * " ", 5242 * "Hello World!\n"]:
+      compressed = rpc._Compress(data)
+      self.assertEqual(len(compressed), 2)
+      self.assertEqual(backend._Decompress(compressed), data)
+
+  def testDecompression(self):
+    self.assertRaises(AssertionError, backend._Decompress, "")
+    self.assertRaises(AssertionError, backend._Decompress, [""])
+    self.assertRaises(AssertionError, backend._Decompress,
+                      ("unknown compression", "data"))
+    self.assertRaises(Exception, backend._Decompress,
+                      (constants.RPC_ENCODING_ZLIB_BASE64, "invalid zlib data"))
+
+
+class TestRpcClientBase(unittest.TestCase):
+  def testNoHosts(self):
+    cdef = ("test_call", NotImplemented, None, rpc_defs.TMO_SLOW, [],
+            None, None, NotImplemented)
+    http_proc = _FakeRequestProcessor(NotImplemented)
+    client = rpc._RpcClientBase(rpc._StaticResolver([]), NotImplemented,
+                                _req_process_fn=http_proc)
+    self.assertEqual(client._Call(cdef, [], []), {})
+
+    # Test wrong number of arguments
+    self.assertRaises(errors.ProgrammerError, client._Call,
+                      cdef, [], [0, 1, 2])
+
+  def testTimeout(self):
+    def _CalcTimeout((arg1, arg2)):
+      return arg1 + arg2
+
+    def _VerifyRequest(exp_timeout, req):
+      self.assertEqual(req.read_timeout, exp_timeout)
+
+      req.success = True
+      req.resp_status_code = http.HTTP_OK
+      req.resp_body = serializer.DumpJson((True, hex(req.read_timeout)))
+
+    resolver = rpc._StaticResolver([
+      "192.0.2.1",
+      "192.0.2.2",
+      ])
+
+    nodes = [
+      "node1.example.com",
+      "node2.example.com",
+      ]
+
+    tests = [(100, None, 100), (30, None, 30)]
+    tests.extend((_CalcTimeout, i, i + 300)
+                 for i in [0, 5, 16485, 30516])
+
+    for timeout, arg1, exp_timeout in tests:
+      cdef = ("test_call", NotImplemented, None, timeout, [
+        ("arg1", None, NotImplemented),
+        ("arg2", None, NotImplemented),
+        ], None, None, NotImplemented)
+
+      http_proc = _FakeRequestProcessor(compat.partial(_VerifyRequest,
+                                                       exp_timeout))
+      client = rpc._RpcClientBase(resolver, NotImplemented,
+                                  _req_process_fn=http_proc)
+      result = client._Call(cdef, nodes, [arg1, 300])
+      self.assertEqual(len(result), len(nodes))
+      self.assertTrue(compat.all(not res.fail_msg and
+                                 res.payload == hex(exp_timeout)
+                                 for res in result.values()))
+
+  def testArgumentEncoder(self):
+    (AT1, AT2) = range(1, 3)
+
+    resolver = rpc._StaticResolver([
+      "192.0.2.5",
+      "192.0.2.6",
+      ])
+
+    nodes = [
+      "node5.example.com",
+      "node6.example.com",
+      ]
+
+    encoders = {
+      AT1: hex,
+      AT2: hash,
+      }
+
+    cdef = ("test_call", NotImplemented, None, rpc_defs.TMO_NORMAL, [
+      ("arg0", None, NotImplemented),
+      ("arg1", AT1, NotImplemented),
+      ("arg1", AT2, NotImplemented),
+      ], None, None, NotImplemented)
+
+    def _VerifyRequest(req):
+      req.success = True
+      req.resp_status_code = http.HTTP_OK
+      req.resp_body = serializer.DumpJson((True, req.post_data))
+
+    http_proc = _FakeRequestProcessor(_VerifyRequest)
+
+    for num in [0, 3796, 9032119]:
+      client = rpc._RpcClientBase(resolver, encoders.get,
+                                  _req_process_fn=http_proc)
+      result = client._Call(cdef, nodes, ["foo", num, "Hello%s" % num])
+      self.assertEqual(len(result), len(nodes))
+      for res in result.values():
+        self.assertFalse(res.fail_msg)
+        self.assertEqual(serializer.LoadJson(res.payload),
+                         ["foo", hex(num), hash("Hello%s" % num)])
+
+  def testPostProc(self):
+    def _VerifyRequest(nums, req):
+      req.success = True
+      req.resp_status_code = http.HTTP_OK
+      req.resp_body = serializer.DumpJson((True, nums))
+
+    resolver = rpc._StaticResolver([
+      "192.0.2.90",
+      "192.0.2.95",
+      ])
+
+    nodes = [
+      "node90.example.com",
+      "node95.example.com",
+      ]
+
+    def _PostProc(res):
+      self.assertFalse(res.fail_msg)
+      res.payload = sum(res.payload)
+      return res
+
+    cdef = ("test_call", NotImplemented, None, rpc_defs.TMO_NORMAL, [],
+            None, _PostProc, NotImplemented)
+
+    # Seeded random generator
+    rnd = random.Random(20299)
+
+    for i in [0, 4, 74, 1391]:
+      nums = [rnd.randint(0, 1000) for _ in range(i)]
+      http_proc = _FakeRequestProcessor(compat.partial(_VerifyRequest, nums))
+      client = rpc._RpcClientBase(resolver, NotImplemented,
+                                  _req_process_fn=http_proc)
+      result = client._Call(cdef, nodes, [])
+      self.assertEqual(len(result), len(nodes))
+      for res in result.values():
+        self.assertFalse(res.fail_msg)
+        self.assertEqual(res.payload, sum(nums))
+
+  def testPreProc(self):
+    def _VerifyRequest(req):
+      req.success = True
+      req.resp_status_code = http.HTTP_OK
+      req.resp_body = serializer.DumpJson((True, req.post_data))
+
+    resolver = rpc._StaticResolver([
+      "192.0.2.30",
+      "192.0.2.35",
+      ])
+
+    nodes = [
+      "node30.example.com",
+      "node35.example.com",
+      ]
+
+    def _PreProc(node, data):
+      self.assertEqual(len(data), 1)
+      return data[0] + node
+
+    cdef = ("test_call", NotImplemented, None, rpc_defs.TMO_NORMAL, [
+      ("arg0", None, NotImplemented),
+      ], _PreProc, None, NotImplemented)
+
+    http_proc = _FakeRequestProcessor(_VerifyRequest)
+    client = rpc._RpcClientBase(resolver, NotImplemented,
+                                _req_process_fn=http_proc)
+
+    for prefix in ["foo", "bar", "baz"]:
+      result = client._Call(cdef, nodes, [prefix])
+      self.assertEqual(len(result), len(nodes))
+      for (idx, (node, res)) in enumerate(result.items()):
+        self.assertFalse(res.fail_msg)
+        self.assertEqual(serializer.LoadJson(res.payload), prefix + node)
+
+  def testResolverOptions(self):
+    def _VerifyRequest(req):
+      req.success = True
+      req.resp_status_code = http.HTTP_OK
+      req.resp_body = serializer.DumpJson((True, req.post_data))
+
+    nodes = [
+      "node30.example.com",
+      "node35.example.com",
+      ]
+
+    def _Resolver(expected, hosts, options):
+      self.assertEqual(hosts, nodes)
+      self.assertEqual(options, expected)
+      return zip(hosts, nodes)
+
+    def _DynamicResolverOptions((arg0, )):
+      return sum(arg0)
+
+    tests = [
+      (None, None, None),
+      (rpc_defs.ACCEPT_OFFLINE_NODE, None, rpc_defs.ACCEPT_OFFLINE_NODE),
+      (False, None, False),
+      (True, None, True),
+      (0, None, 0),
+      (_DynamicResolverOptions, [1, 2, 3], 6),
+      (_DynamicResolverOptions, range(4, 19), 165),
+      ]
+
+    for (resolver_opts, arg0, expected) in tests:
+      cdef = ("test_call", NotImplemented, resolver_opts, rpc_defs.TMO_NORMAL, [
+        ("arg0", None, NotImplemented),
+        ], None, None, NotImplemented)
+
+      http_proc = _FakeRequestProcessor(_VerifyRequest)
+
+      client = rpc._RpcClientBase(compat.partial(_Resolver, expected),
+                                  NotImplemented, _req_process_fn=http_proc)
+      result = client._Call(cdef, nodes, [arg0])
+      self.assertEqual(len(result), len(nodes))
+      for (idx, (node, res)) in enumerate(result.items()):
+        self.assertFalse(res.fail_msg)
+
+
+class _FakeConfigForRpcRunner:
+  GetAllNodesInfo = NotImplemented
+
+  def __init__(self, cluster=NotImplemented):
+    self._cluster = cluster
+
+  def GetNodeInfo(self, name):
+    return objects.Node(name=name)
+
+  def GetClusterInfo(self):
+    return self._cluster
+
+  def GetInstanceDiskParams(self, _):
+    return constants.DISK_DT_DEFAULTS
+
+
+class TestRpcRunner(unittest.TestCase):
+  def testUploadFile(self):
+    data = 1779 * "Hello World\n"
+
+    tmpfile = tempfile.NamedTemporaryFile()
+    tmpfile.write(data)
+    tmpfile.flush()
+    st = os.stat(tmpfile.name)
+
+    def _VerifyRequest(req):
+      (uldata, ) = serializer.LoadJson(req.post_data)
+      self.assertEqual(len(uldata), 7)
+      self.assertEqual(uldata[0], tmpfile.name)
+      self.assertEqual(list(uldata[1]), list(rpc._Compress(data)))
+      self.assertEqual(uldata[2], st.st_mode)
+      self.assertEqual(uldata[3], "user%s" % os.getuid())
+      self.assertEqual(uldata[4], "group%s" % os.getgid())
+      self.assertTrue(uldata[5] is not None)
+      self.assertEqual(uldata[6], st.st_mtime)
+
+      req.success = True
+      req.resp_status_code = http.HTTP_OK
+      req.resp_body = serializer.DumpJson((True, None))
+
+    http_proc = _FakeRequestProcessor(_VerifyRequest)
+
+    std_runner = rpc.RpcRunner(_FakeConfigForRpcRunner(), None,
+                               _req_process_fn=http_proc,
+                               _getents=mocks.FakeGetentResolver)
+
+    cfg_runner = rpc.ConfigRunner(None, ["192.0.2.13"],
+                                  _req_process_fn=http_proc,
+                                  _getents=mocks.FakeGetentResolver)
+
+    nodes = [
+      "node1.example.com",
+      ]
+
+    for runner in [std_runner, cfg_runner]:
+      result = runner.call_upload_file(nodes, tmpfile.name)
+      self.assertEqual(len(result), len(nodes))
+      for (idx, (node, res)) in enumerate(result.items()):
+        self.assertFalse(res.fail_msg)
+
+  def testEncodeInstance(self):
+    cluster = objects.Cluster(hvparams={
+      constants.HT_KVM: {
+        constants.HV_BLOCKDEV_PREFIX: "foo",
+        },
+      },
+      beparams={
+        constants.PP_DEFAULT: {
+          constants.BE_MAXMEM: 8192,
+          },
+        },
+      os_hvp={},
+      osparams={
+        "linux": {
+          "role": "unknown",
+          },
+        })
+    cluster.UpgradeConfig()
+
+    inst = objects.Instance(name="inst1.example.com",
+      hypervisor=constants.HT_FAKE,
+      os="linux",
+      hvparams={
+        constants.HT_KVM: {
+          constants.HV_BLOCKDEV_PREFIX: "bar",
+          constants.HV_ROOT_PATH: "/tmp",
+          },
+        },
+      beparams={
+        constants.BE_MINMEM: 128,
+        constants.BE_MAXMEM: 256,
+        },
+      nics=[
+        objects.NIC(nicparams={
+          constants.NIC_MODE: "mymode",
+          }),
+        ],
+      disk_template=constants.DT_DISKLESS,
+      disks=[])
+    inst.UpgradeConfig()
+
+    cfg = _FakeConfigForRpcRunner(cluster=cluster)
+    runner = rpc.RpcRunner(cfg, None,
+                           _req_process_fn=NotImplemented,
+                           _getents=mocks.FakeGetentResolver)
+
+    def _CheckBasics(result):
+      self.assertEqual(result["name"], "inst1.example.com")
+      self.assertEqual(result["os"], "linux")
+      self.assertEqual(result["beparams"][constants.BE_MINMEM], 128)
+      self.assertEqual(len(result["hvparams"]), 1)
+      self.assertEqual(len(result["nics"]), 1)
+      self.assertEqual(result["nics"][0]["nicparams"][constants.NIC_MODE],
+                       "mymode")
+
+    # Generic object serialization
+    result = runner._encoder((rpc_defs.ED_OBJECT_DICT, inst))
+    _CheckBasics(result)
+
+    result = runner._encoder((rpc_defs.ED_OBJECT_DICT_LIST, 5 * [inst]))
+    map(_CheckBasics, result)
+
+    # Just an instance
+    result = runner._encoder((rpc_defs.ED_INST_DICT, inst))
+    _CheckBasics(result)
+    self.assertEqual(result["beparams"][constants.BE_MAXMEM], 256)
+    self.assertEqual(result["hvparams"][constants.HT_KVM], {
+      constants.HV_BLOCKDEV_PREFIX: "bar",
+      constants.HV_ROOT_PATH: "/tmp",
+      })
+    self.assertEqual(result["osparams"], {
+      "role": "unknown",
+      })
+
+    # Instance with OS parameters
+    result = runner._encoder((rpc_defs.ED_INST_DICT_OSP_DP, (inst, {
+      "role": "webserver",
+      "other": "field",
+      })))
+    _CheckBasics(result)
+    self.assertEqual(result["beparams"][constants.BE_MAXMEM], 256)
+    self.assertEqual(result["hvparams"][constants.HT_KVM], {
+      constants.HV_BLOCKDEV_PREFIX: "bar",
+      constants.HV_ROOT_PATH: "/tmp",
+      })
+    self.assertEqual(result["osparams"], {
+      "role": "webserver",
+      "other": "field",
+      })
+
+    # Instance with hypervisor and backend parameters
+    result = runner._encoder((rpc_defs.ED_INST_DICT_HVP_BEP, (inst, {
+      constants.HT_KVM: {
+        constants.HV_BOOT_ORDER: "xyz",
+        },
+      }, {
+      constants.BE_VCPUS: 100,
+      constants.BE_MAXMEM: 4096,
+      })))
+    _CheckBasics(result)
+    self.assertEqual(result["beparams"][constants.BE_MAXMEM], 4096)
+    self.assertEqual(result["beparams"][constants.BE_VCPUS], 100)
+    self.assertEqual(result["hvparams"][constants.HT_KVM], {
+      constants.HV_BOOT_ORDER: "xyz",
+      })
 
 
 if __name__ == "__main__":
index 79ede58..73f4143 100755 (executable)
@@ -23,6 +23,7 @@
 from ganeti import constants
 from ganeti import errors
 from ganeti import runtime
+from ganeti import ht
 
 import testutils
 import unittest
@@ -138,5 +139,37 @@ class TestErrors(unittest.TestCase):
                       self.resolver.LookupGroup, "does-not-exist-foo")
 
 
+class TestArchInfo(unittest.TestCase):
+  EXP_TYPES = \
+    ht.TAnd(ht.TIsLength(2),
+            ht.TItems([
+              ht.TNonEmptyString,
+              ht.TNonEmptyString,
+              ]))
+
+  def setUp(self):
+    self.assertTrue(runtime._arch is None)
+
+  def tearDown(self):
+    runtime._arch = None
+
+  def testNotInitialized(self):
+    self.assertRaises(errors.ProgrammerError, runtime.GetArchInfo)
+
+  def testInitializeMultiple(self):
+    runtime.InitArchInfo()
+
+    self.assertRaises(errors.ProgrammerError, runtime.InitArchInfo)
+
+  def testNormal(self):
+    runtime.InitArchInfo()
+
+    info = runtime.GetArchInfo()
+
+    self.assertTrue(self.EXP_TYPES(info),
+                    msg=("Doesn't match expected type description: %s" %
+                         self.EXP_TYPES))
+
+
 if __name__ == "__main__":
   testutils.GanetiTestProgram()
index 9d8d656..46aafc2 100755 (executable)
@@ -16,7 +16,7 @@
 # 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
-# 0.0510-1301, USA.
+# 02110-1301, USA.
 
 
 """Script for unittesting the serializer module"""
@@ -52,10 +52,9 @@ class TestSerializer(testutils.GanetiTestCase):
     ]
 
   def _TestSerializer(self, dump_fn, load_fn):
-    for indent in [True, False]:
-      for data in self._TESTDATA:
-        self.failUnless(dump_fn(data, indent=indent).endswith("\n"))
-        self.assertEqualValues(load_fn(dump_fn(data, indent=indent)), data)
+    for data in self._TESTDATA:
+      self.failUnless(dump_fn(data).endswith("\n"))
+      self.assertEqualValues(load_fn(dump_fn(data)), data)
 
   def testGeneric(self):
     self._TestSerializer(serializer.Dump, serializer.Load)
index f619b81..48700e9 100755 (executable)
@@ -21,8 +21,6 @@
 
 """Script for testing ganeti.tools.ensure_dirs"""
 
-import errno
-import stat
 import unittest
 import os.path
 
@@ -31,127 +29,7 @@ from ganeti.tools import ensure_dirs
 import testutils
 
 
-def _MockStatResult(cb, mode, uid, gid):
-  def _fn(path):
-    if cb:
-      cb()
-    return {
-      stat.ST_MODE: mode,
-      stat.ST_UID: uid,
-      stat.ST_GID: gid,
-      }
-  return _fn
-
-
-def _RaiseNoEntError():
-  raise EnvironmentError(errno.ENOENT, "not found")
-
-
-def _OtherStatRaise():
-  raise EnvironmentError()
-
-
 class TestEnsureDirsFunctions(unittest.TestCase):
-  UID_A = 16024
-  UID_B = 25850
-  GID_A = 14028
-  GID_B = 29801
-
-  def setUp(self):
-    self._chown_calls = []
-    self._chmod_calls = []
-    self._mkdir_calls = []
-
-  def tearDown(self):
-    self.assertRaises(IndexError, self._mkdir_calls.pop)
-    self.assertRaises(IndexError, self._chmod_calls.pop)
-    self.assertRaises(IndexError, self._chown_calls.pop)
-
-  def _FakeMkdir(self, path):
-    self._mkdir_calls.append(path)
-
-  def _FakeChown(self, path, uid, gid):
-    self._chown_calls.append((path, uid, gid))
-
-  def _ChmodWrapper(self, cb):
-    def _fn(path, mode):
-      self._chmod_calls.append((path, mode))
-      if cb:
-        cb()
-    return _fn
-
-  def _VerifyEnsure(self, path, mode, uid=-1, gid=-1):
-    self.assertEqual(path, "/ganeti-qa-non-test")
-    self.assertEqual(mode, 0700)
-    self.assertEqual(uid, self.UID_A)
-    self.assertEqual(gid, self.GID_A)
-
-  def testEnsureDir(self):
-    is_dir_stat = _MockStatResult(None, stat.S_IFDIR, 0, 0)
-    ensure_dirs.EnsureDir("/ganeti-qa-non-test", 0700, self.UID_A, self.GID_A,
-                          _lstat_fn=is_dir_stat, _ensure_fn=self._VerifyEnsure)
-
-  def testEnsureDirErrors(self):
-    self.assertRaises(ensure_dirs.EnsureError, ensure_dirs.EnsureDir,
-                      "/ganeti-qa-non-test", 0700, 0, 0,
-                      _lstat_fn=_MockStatResult(None, 0, 0, 0))
-    self.assertRaises(IndexError, self._mkdir_calls.pop)
-
-    other_stat_raise = _MockStatResult(_OtherStatRaise, stat.S_IFDIR, 0, 0)
-    self.assertRaises(ensure_dirs.EnsureError, ensure_dirs.EnsureDir,
-                      "/ganeti-qa-non-test", 0700, 0, 0,
-                      _lstat_fn=other_stat_raise)
-    self.assertRaises(IndexError, self._mkdir_calls.pop)
-
-    non_exist_stat = _MockStatResult(_RaiseNoEntError, stat.S_IFDIR, 0, 0)
-    ensure_dirs.EnsureDir("/ganeti-qa-non-test", 0700, self.UID_A, self.GID_A,
-                          _lstat_fn=non_exist_stat, _mkdir_fn=self._FakeMkdir,
-                          _ensure_fn=self._VerifyEnsure)
-    self.assertEqual(self._mkdir_calls.pop(0), "/ganeti-qa-non-test")
-
-  def testEnsurePermissionNoEnt(self):
-    self.assertRaises(ensure_dirs.EnsureError, ensure_dirs.EnsurePermission,
-                      "/ganeti-qa-non-test", 0600,
-                      _chmod_fn=NotImplemented, _chown_fn=NotImplemented,
-                      _stat_fn=_MockStatResult(_RaiseNoEntError, 0, 0, 0))
-
-  def testEnsurePermissionNoEntMustNotExist(self):
-    ensure_dirs.EnsurePermission("/ganeti-qa-non-test", 0600, must_exist=False,
-                                 _chmod_fn=NotImplemented,
-                                 _chown_fn=NotImplemented,
-                                 _stat_fn=_MockStatResult(_RaiseNoEntError,
-                                                          0, 0, 0))
-
-  def testEnsurePermissionOtherErrorMustNotExist(self):
-    self.assertRaises(ensure_dirs.EnsureError, ensure_dirs.EnsurePermission,
-                      "/ganeti-qa-non-test", 0600, must_exist=False,
-                      _chmod_fn=NotImplemented, _chown_fn=NotImplemented,
-                      _stat_fn=_MockStatResult(_OtherStatRaise, 0, 0, 0))
-
-  def testEnsurePermissionNoChanges(self):
-    ensure_dirs.EnsurePermission("/ganeti-qa-non-test", 0600,
-                                 _stat_fn=_MockStatResult(None, 0600, 0, 0),
-                                 _chmod_fn=self._ChmodWrapper(None),
-                                 _chown_fn=self._FakeChown)
-
-  def testEnsurePermissionChangeMode(self):
-    ensure_dirs.EnsurePermission("/ganeti-qa-non-test", 0444,
-                                 _stat_fn=_MockStatResult(None, 0600, 0, 0),
-                                 _chmod_fn=self._ChmodWrapper(None),
-                                 _chown_fn=self._FakeChown)
-    self.assertEqual(self._chmod_calls.pop(0), ("/ganeti-qa-non-test", 0444))
-
-  def testEnsurePermissionSetUidGid(self):
-    ensure_dirs.EnsurePermission("/ganeti-qa-non-test", 0600,
-                                 uid=self.UID_B, gid=self.GID_B,
-                                 _stat_fn=_MockStatResult(None, 0600,
-                                                          self.UID_A,
-                                                          self.GID_A),
-                                 _chmod_fn=self._ChmodWrapper(None),
-                                 _chown_fn=self._FakeChown)
-    self.assertEqual(self._chown_calls.pop(0),
-                     ("/ganeti-qa-non-test", self.UID_B, self.GID_B))
-
   def testPaths(self):
     paths = [(path[0], path[1]) for path in ensure_dirs.GetPaths()]
 
index de4fbc9..3dd45ed 100755 (executable)
@@ -16,7 +16,7 @@
 # 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
-# 0.0510-1301, USA.
+# 02110-1301, USA.
 
 
 """Script for unittesting the uidpool module"""
index 96b4ff5..5d08e2e 100755 (executable)
@@ -26,6 +26,7 @@ import random
 import operator
 
 from ganeti import constants
+from ganeti import compat
 from ganeti.utils import algo
 
 import testutils
@@ -236,6 +237,16 @@ class TestInvertDict(unittest.TestCase):
                      { 1: "foo", 2: "bar", 5: "baz"})
 
 
+class TestInsertAtPos(unittest.TestCase):
+  def test(self):
+    a = [1, 5, 6]
+    b = [2, 3, 4]
+    self.assertEqual(algo.InsertAtPos(a, 1, b), [1, 2, 3, 4, 5, 6])
+    self.assertEqual(algo.InsertAtPos(a, 0, b), b + a)
+    self.assertEqual(algo.InsertAtPos(a, len(a), b), a + b)
+    self.assertEqual(algo.InsertAtPos(a, 2, b), [1, 5, 2, 3, 4, 6])
+
+
 class TimeMock:
   def __init__(self, values):
     self.values = values
@@ -272,5 +283,90 @@ class TestRunningTimeout(unittest.TestCase):
     self.assertRaises(ValueError, algo.RunningTimeout, -1.0, True)
 
 
+class TestJoinDisjointDicts(unittest.TestCase):
+  def setUp(self):
+    self.non_empty_dict = {"a": 1, "b": 2}
+    self.empty_dict = dict()
+
+  def testWithEmptyDicts(self):
+    self.assertEqual(self.empty_dict, algo.JoinDisjointDicts(self.empty_dict,
+      self.empty_dict))
+    self.assertEqual(self.non_empty_dict, algo.JoinDisjointDicts(
+      self.empty_dict, self.non_empty_dict))
+    self.assertEqual(self.non_empty_dict, algo.JoinDisjointDicts(
+      self.non_empty_dict, self.empty_dict))
+
+  def testNonDisjoint(self):
+    self.assertRaises(AssertionError, algo.JoinDisjointDicts,
+      self.non_empty_dict, self.non_empty_dict)
+
+  def testCommonCase(self):
+    dict_a = {"TEST1": 1, "TEST2": 2}
+    dict_b = {"TEST3": 3, "TEST4": 4}
+
+    result = dict_a.copy()
+    result.update(dict_b)
+
+    self.assertEqual(result, algo.JoinDisjointDicts(dict_a, dict_b))
+    self.assertEqual(result, algo.JoinDisjointDicts(dict_b, dict_a))
+
+
+class TestSequenceToDict(unittest.TestCase):
+  def testEmpty(self):
+    self.assertEqual(algo.SequenceToDict([]), {})
+    self.assertEqual(algo.SequenceToDict({}), {})
+
+  def testSimple(self):
+    data = [(i, str(i), "test%s" % i) for i in range(391)]
+    self.assertEqual(algo.SequenceToDict(data),
+      dict((i, (i, str(i), "test%s" % i))
+           for i in range(391)))
+
+  def testCustomKey(self):
+    data = [(i, hex(i), "test%s" % i) for i in range(100)]
+    self.assertEqual(algo.SequenceToDict(data, key=compat.snd),
+      dict((hex(i), (i, hex(i), "test%s" % i))
+           for i in range(100)))
+    self.assertEqual(algo.SequenceToDict(data,
+                                         key=lambda (a, b, val): hash(val)),
+      dict((hash("test%s" % i), (i, hex(i), "test%s" % i))
+           for i in range(100)))
+
+  def testDuplicate(self):
+    self.assertRaises(ValueError, algo.SequenceToDict,
+                      [(0, 0), (0, 0)])
+    self.assertRaises(ValueError, algo.SequenceToDict,
+                      [(i, ) for i in range(200)] + [(10, )])
+
+
+class TestFlatToDict(unittest.TestCase):
+  def testNormal(self):
+    data = [
+      ("lv/xenvg", {"foo": "bar", "bar": "baz"}),
+      ("lv/xenfoo", {"foo": "bar", "baz": "blubb"}),
+      ("san/foo", {"ip": "127.0.0.1", "port": 1337}),
+      ("san/blubb/blibb", 54),
+      ]
+    reference = {
+      "lv": {
+        "xenvg": {"foo": "bar", "bar": "baz"},
+        "xenfoo": {"foo": "bar", "baz": "blubb"},
+        },
+      "san": {
+        "foo": {"ip": "127.0.0.1", "port": 1337},
+        "blubb": {"blibb": 54},
+        },
+      }
+    self.assertEqual(algo.FlatToDict(data), reference)
+
+  def testUnlikeDepth(self):
+    data = [
+      ("san/foo", {"ip": "127.0.0.1", "port": 1337}),
+      ("san/foo/blubb", 23), # Another foo entry under san
+      ("san/blubb/blibb", 54),
+      ]
+    self.assertRaises(AssertionError, algo.FlatToDict, data)
+
+
 if __name__ == "__main__":
   testutils.GanetiTestProgram()
diff --git a/test/ganeti.utils.io_unittest-runasroot.py b/test/ganeti.utils.io_unittest-runasroot.py
new file mode 100644 (file)
index 0000000..909c08e
--- /dev/null
@@ -0,0 +1,97 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2006, 2007, 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.
+
+
+"""Script for testing ganeti.utils.io (tests that require root access)"""
+
+import os
+import tempfile
+import shutil
+import errno
+
+from ganeti import constants
+from ganeti import utils
+from ganeti import compat
+from ganeti import errors
+
+import testutils
+
+
+class TestWriteFile(testutils.GanetiTestCase):
+  def setUp(self):
+    testutils.GanetiTestCase.setUp(self)
+    self.tmpdir = None
+    self.tfile = tempfile.NamedTemporaryFile()
+    self.did_pre = False
+    self.did_post = False
+    self.did_write = False
+
+  def tearDown(self):
+    testutils.GanetiTestCase.tearDown(self)
+    if self.tmpdir:
+      shutil.rmtree(self.tmpdir)
+
+  def testFileUid(self):
+    self.tmpdir = tempfile.mkdtemp()
+    target = utils.PathJoin(self.tmpdir, "target")
+    tuid = os.geteuid() + 1
+    utils.WriteFile(target, data="data", uid=tuid + 1)
+    self.assertFileUid(target, tuid + 1)
+    utils.WriteFile(target, data="data", uid=tuid)
+    self.assertFileUid(target, tuid)
+    utils.WriteFile(target, data="data", uid=tuid + 1,
+                    keep_perms=utils.KP_IF_EXISTS)
+    self.assertFileUid(target, tuid)
+    utils.WriteFile(target, data="data", keep_perms=utils.KP_ALWAYS)
+    self.assertFileUid(target, tuid)
+
+  def testNewFileUid(self):
+    self.tmpdir = tempfile.mkdtemp()
+    target = utils.PathJoin(self.tmpdir, "target")
+    tuid = os.geteuid() + 1
+    utils.WriteFile(target, data="data", uid=tuid,
+                    keep_perms=utils.KP_IF_EXISTS)
+    self.assertFileUid(target, tuid)
+
+  def testFileGid(self):
+    self.tmpdir = tempfile.mkdtemp()
+    target = utils.PathJoin(self.tmpdir, "target")
+    tgid = os.getegid() + 1
+    utils.WriteFile(target, data="data", gid=tgid + 1)
+    self.assertFileGid(target, tgid + 1)
+    utils.WriteFile(target, data="data", gid=tgid)
+    self.assertFileGid(target, tgid)
+    utils.WriteFile(target, data="data", gid=tgid + 1,
+                    keep_perms=utils.KP_IF_EXISTS)
+    self.assertFileGid(target, tgid)
+    utils.WriteFile(target, data="data", keep_perms=utils.KP_ALWAYS)
+    self.assertFileGid(target, tgid)
+
+  def testNewFileGid(self):
+    self.tmpdir = tempfile.mkdtemp()
+    target = utils.PathJoin(self.tmpdir, "target")
+    tgid = os.getegid() + 1
+    utils.WriteFile(target, data="data", gid=tgid,
+                    keep_perms=utils.KP_IF_EXISTS)
+    self.assertFileGid(target, tgid)
+
+
+if __name__ == "__main__":
+  testutils.GanetiTestProgram()
index 78753b7..109232a 100755 (executable)
@@ -28,6 +28,8 @@ import shutil
 import glob
 import time
 import signal
+import stat
+import errno
 
 from ganeti import constants
 from ganeti import utils
@@ -237,9 +239,31 @@ class TestListVisibleFiles(unittest.TestCase):
     self.failUnlessRaises(errors.ProgrammerError, utils.ListVisibleFiles,
                           "/bin/../tmp")
 
+  def testMountpoint(self):
+    lvfmp_fn = compat.partial(utils.ListVisibleFiles,
+                              _is_mountpoint=lambda _: True)
+    self.assertEqual(lvfmp_fn(self.path), [])
+
+    # Create "lost+found" as a regular file
+    self._CreateFiles(["foo", "bar", ".baz", "lost+found"])
+    self.assertEqual(set(lvfmp_fn(self.path)),
+                     set(["foo", "bar", "lost+found"]))
+
+    # Replace "lost+found" with a directory
+    laf_path = utils.PathJoin(self.path, "lost+found")
+    utils.RemoveFile(laf_path)
+    os.mkdir(laf_path)
+    self.assertEqual(set(lvfmp_fn(self.path)), set(["foo", "bar"]))
+
+  def testLostAndFoundNoMountpoint(self):
+    files = ["foo", "bar", ".Hello World", "lost+found"]
+    expected = ["foo", "bar", "lost+found"]
+    self._test(files, expected)
+
 
-class TestWriteFile(unittest.TestCase):
+class TestWriteFile(testutils.GanetiTestCase):
   def setUp(self):
+    testutils.GanetiTestCase.setUp(self)
     self.tmpdir = None
     self.tfile = tempfile.NamedTemporaryFile()
     self.did_pre = False
@@ -247,6 +271,7 @@ class TestWriteFile(unittest.TestCase):
     self.did_write = False
 
   def tearDown(self):
+    testutils.GanetiTestCase.tearDown(self)
     if self.tmpdir:
       shutil.rmtree(self.tmpdir)
 
@@ -275,6 +300,14 @@ class TestWriteFile(unittest.TestCase):
     self.assertRaises(errors.ProgrammerError, utils.WriteFile, self.tfile.name)
     self.assertRaises(errors.ProgrammerError, utils.WriteFile,
                       self.tfile.name, data="test", atime=0)
+    self.assertRaises(errors.ProgrammerError, utils.WriteFile, self.tfile.name,
+                      mode=0400, keep_perms=utils.KP_ALWAYS)
+    self.assertRaises(errors.ProgrammerError, utils.WriteFile, self.tfile.name,
+                      uid=0, keep_perms=utils.KP_ALWAYS)
+    self.assertRaises(errors.ProgrammerError, utils.WriteFile, self.tfile.name,
+                      gid=0, keep_perms=utils.KP_ALWAYS)
+    self.assertRaises(errors.ProgrammerError, utils.WriteFile, self.tfile.name,
+                      mode=0400, uid=0, keep_perms=utils.KP_ALWAYS)
 
   def testPreWrite(self):
     utils.WriteFile(self.tfile.name, data="", prewrite=self.markPre)
@@ -371,6 +404,28 @@ class TestWriteFile(unittest.TestCase):
     self.assertTrue("test" in os.listdir(self.tmpdir))
     self.assertEqual(len(os.listdir(self.tmpdir)), 2)
 
+  def testFileMode(self):
+    self.tmpdir = tempfile.mkdtemp()
+    target = utils.PathJoin(self.tmpdir, "target")
+    self.assertRaises(OSError, utils.WriteFile, target, data="data",
+                      keep_perms=utils.KP_ALWAYS)
+    # All masks have only user bits set, to avoid interactions with umask
+    utils.WriteFile(target, data="data", mode=0200)
+    self.assertFileMode(target, 0200)
+    utils.WriteFile(target, data="data", mode=0400,
+                    keep_perms=utils.KP_IF_EXISTS)
+    self.assertFileMode(target, 0200)
+    utils.WriteFile(target, data="data", keep_perms=utils.KP_ALWAYS)
+    self.assertFileMode(target, 0200)
+    utils.WriteFile(target, data="data", mode=0700)
+    self.assertFileMode(target, 0700)
+
+  def testNewFileMode(self):
+    self.tmpdir = tempfile.mkdtemp()
+    target = utils.PathJoin(self.tmpdir, "target")
+    utils.WriteFile(target, data="data", mode=0400,
+                    keep_perms=utils.KP_IF_EXISTS)
+    self.assertFileMode(target, 0400)
 
 class TestFileID(testutils.GanetiTestCase):
   def testEquality(self):
@@ -505,12 +560,15 @@ class TestRename(unittest.TestCase):
     self.assert_(os.path.isdir(os.path.join(self.tmpdir, "test")))
     self.assert_(os.path.isfile(os.path.join(self.tmpdir, "test/xyz")))
 
-    utils.RenameFile(os.path.join(self.tmpdir, "test/xyz"),
-                     os.path.join(self.tmpdir, "test/foo/bar/baz"),
-                     mkdir=True)
-    self.assert_(os.path.isdir(os.path.join(self.tmpdir, "test")))
-    self.assert_(os.path.isdir(os.path.join(self.tmpdir, "test/foo/bar")))
-    self.assert_(os.path.isfile(os.path.join(self.tmpdir, "test/foo/bar/baz")))
+    self.assertRaises(EnvironmentError, utils.RenameFile,
+                      os.path.join(self.tmpdir, "test/xyz"),
+                      os.path.join(self.tmpdir, "test/foo/bar/baz"),
+                      mkdir=True)
+
+    self.assertTrue(os.path.exists(os.path.join(self.tmpdir, "test/xyz")))
+    self.assertFalse(os.path.exists(os.path.join(self.tmpdir, "test/foo/bar")))
+    self.assertFalse(os.path.exists(os.path.join(self.tmpdir,
+                                                 "test/foo/bar/baz")))
 
 
 class TestMakedirs(unittest.TestCase):
@@ -585,6 +643,32 @@ class TestIsNormAbsPath(unittest.TestCase):
     self._pathTestHelper("/etc/", False)
 
 
+class TestIsBelowDir(unittest.TestCase):
+  """Testing case for IsBelowDir"""
+
+  def testSamePrefix(self):
+    self.assertTrue(utils.IsBelowDir("/a/b", "/a/b/c"))
+    self.assertTrue(utils.IsBelowDir("/a/b/", "/a/b/e"))
+
+  def testSamePrefixButDifferentDir(self):
+    self.assertFalse(utils.IsBelowDir("/a/b", "/a/bc/d"))
+    self.assertFalse(utils.IsBelowDir("/a/b/", "/a/bc/e"))
+
+  def testSamePrefixButDirTraversal(self):
+    self.assertFalse(utils.IsBelowDir("/a/b", "/a/b/../c"))
+    self.assertFalse(utils.IsBelowDir("/a/b/", "/a/b/../d"))
+
+  def testSamePrefixAndTraversal(self):
+    self.assertTrue(utils.IsBelowDir("/a/b", "/a/b/c/../d"))
+    self.assertTrue(utils.IsBelowDir("/a/b", "/a/b/c/./e"))
+    self.assertTrue(utils.IsBelowDir("/a/b", "/a/b/../b/./e"))
+
+  def testBothAbsPath(self):
+    self.assertRaises(ValueError, utils.IsBelowDir, "/a/b/c", "d")
+    self.assertRaises(ValueError, utils.IsBelowDir, "a/b/c", "/d")
+    self.assertRaises(ValueError, utils.IsBelowDir, "a/b/c", "d")
+
+
 class TestPathJoin(unittest.TestCase):
   """Testing case for PathJoin"""
 
@@ -659,7 +743,7 @@ class TestPidFileFunctions(unittest.TestCase):
     read_pid = utils.ReadPidFile(pid_file)
     self.failUnlessEqual(read_pid, os.getpid())
     self.failUnless(utils.IsProcessAlive(read_pid))
-    self.failUnlessRaises(errors.LockError, utils.WritePidFile,
+    self.failUnlessRaises(errors.PidFileLockError, utils.WritePidFile,
                           self.f_dpn('test'))
     os.close(fd)
     utils.RemoveFile(self.f_dpn("test"))
@@ -695,11 +779,28 @@ class TestPidFileFunctions(unittest.TestCase):
     read_pid = utils.ReadPidFile(pid_file)
     self.failUnlessEqual(read_pid, new_pid)
     self.failUnless(utils.IsProcessAlive(new_pid))
+
+    # Try writing to locked file
+    try:
+      utils.WritePidFile(pid_file)
+    except errors.PidFileLockError, err:
+      errmsg = str(err)
+      self.assertTrue(errmsg.endswith(" %s" % new_pid),
+                      msg=("Error message ('%s') didn't contain correct"
+                           " PID (%s)" % (errmsg, new_pid)))
+    else:
+      self.fail("Writing to locked file didn't fail")
+
     utils.KillProcess(new_pid, waitpid=True)
     self.failIf(utils.IsProcessAlive(new_pid))
     utils.RemoveFile(self.f_dpn('child'))
     self.failUnlessRaises(errors.ProgrammerError, utils.KillProcess, 0)
 
+  def testExceptionType(self):
+    # Make sure the PID lock error is a subclass of LockError in case some code
+    # depends on it
+    self.assertTrue(issubclass(errors.PidFileLockError, errors.LockError))
+
   def tearDown(self):
     shutil.rmtree(self.dir)
 
@@ -775,5 +876,127 @@ class TestNewUUID(unittest.TestCase):
     self.failUnless(utils.UUID_RE.match(utils.NewUUID()))
 
 
+def _MockStatResult(cb, mode, uid, gid):
+  def _fn(path):
+    if cb:
+      cb()
+    return {
+      stat.ST_MODE: mode,
+      stat.ST_UID: uid,
+      stat.ST_GID: gid,
+      }
+  return _fn
+
+
+def _RaiseNoEntError():
+  raise EnvironmentError(errno.ENOENT, "not found")
+
+
+def _OtherStatRaise():
+  raise EnvironmentError()
+
+
+class TestPermissionEnforcements(unittest.TestCase):
+  UID_A = 16024
+  UID_B = 25850
+  GID_A = 14028
+  GID_B = 29801
+
+  def setUp(self):
+    self._chown_calls = []
+    self._chmod_calls = []
+    self._mkdir_calls = []
+
+  def tearDown(self):
+    self.assertRaises(IndexError, self._mkdir_calls.pop)
+    self.assertRaises(IndexError, self._chmod_calls.pop)
+    self.assertRaises(IndexError, self._chown_calls.pop)
+
+  def _FakeMkdir(self, path):
+    self._mkdir_calls.append(path)
+
+  def _FakeChown(self, path, uid, gid):
+    self._chown_calls.append((path, uid, gid))
+
+  def _ChmodWrapper(self, cb):
+    def _fn(path, mode):
+      self._chmod_calls.append((path, mode))
+      if cb:
+        cb()
+    return _fn
+
+  def _VerifyPerm(self, path, mode, uid=-1, gid=-1):
+    self.assertEqual(path, "/ganeti-qa-non-test")
+    self.assertEqual(mode, 0700)
+    self.assertEqual(uid, self.UID_A)
+    self.assertEqual(gid, self.GID_A)
+
+  def testMakeDirWithPerm(self):
+    is_dir_stat = _MockStatResult(None, stat.S_IFDIR, 0, 0)
+    utils.MakeDirWithPerm("/ganeti-qa-non-test", 0700, self.UID_A, self.GID_A,
+                          _lstat_fn=is_dir_stat, _perm_fn=self._VerifyPerm)
+
+  def testDirErrors(self):
+    self.assertRaises(errors.GenericError, utils.MakeDirWithPerm,
+                      "/ganeti-qa-non-test", 0700, 0, 0,
+                      _lstat_fn=_MockStatResult(None, 0, 0, 0))
+    self.assertRaises(IndexError, self._mkdir_calls.pop)
+
+    other_stat_raise = _MockStatResult(_OtherStatRaise, stat.S_IFDIR, 0, 0)
+    self.assertRaises(errors.GenericError, utils.MakeDirWithPerm,
+                      "/ganeti-qa-non-test", 0700, 0, 0,
+                      _lstat_fn=other_stat_raise)
+    self.assertRaises(IndexError, self._mkdir_calls.pop)
+
+    non_exist_stat = _MockStatResult(_RaiseNoEntError, stat.S_IFDIR, 0, 0)
+    utils.MakeDirWithPerm("/ganeti-qa-non-test", 0700, self.UID_A, self.GID_A,
+                          _lstat_fn=non_exist_stat, _mkdir_fn=self._FakeMkdir,
+                          _perm_fn=self._VerifyPerm)
+    self.assertEqual(self._mkdir_calls.pop(0), "/ganeti-qa-non-test")
+
+  def testEnforcePermissionNoEnt(self):
+    self.assertRaises(errors.GenericError, utils.EnforcePermission,
+                      "/ganeti-qa-non-test", 0600,
+                      _chmod_fn=NotImplemented, _chown_fn=NotImplemented,
+                      _stat_fn=_MockStatResult(_RaiseNoEntError, 0, 0, 0))
+
+  def testEnforcePermissionNoEntMustNotExist(self):
+    utils.EnforcePermission("/ganeti-qa-non-test", 0600, must_exist=False,
+                            _chmod_fn=NotImplemented,
+                            _chown_fn=NotImplemented,
+                            _stat_fn=_MockStatResult(_RaiseNoEntError,
+                                                          0, 0, 0))
+
+  def testEnforcePermissionOtherErrorMustNotExist(self):
+    self.assertRaises(errors.GenericError, utils.EnforcePermission,
+                      "/ganeti-qa-non-test", 0600, must_exist=False,
+                      _chmod_fn=NotImplemented, _chown_fn=NotImplemented,
+                      _stat_fn=_MockStatResult(_OtherStatRaise, 0, 0, 0))
+
+  def testEnforcePermissionNoChanges(self):
+    utils.EnforcePermission("/ganeti-qa-non-test", 0600,
+                            _stat_fn=_MockStatResult(None, 0600, 0, 0),
+                            _chmod_fn=self._ChmodWrapper(None),
+                            _chown_fn=self._FakeChown)
+
+  def testEnforcePermissionChangeMode(self):
+    utils.EnforcePermission("/ganeti-qa-non-test", 0444,
+                            _stat_fn=_MockStatResult(None, 0600, 0, 0),
+                            _chmod_fn=self._ChmodWrapper(None),
+                            _chown_fn=self._FakeChown)
+    self.assertEqual(self._chmod_calls.pop(0), ("/ganeti-qa-non-test", 0444))
+
+  def testEnforcePermissionSetUidGid(self):
+    utils.EnforcePermission("/ganeti-qa-non-test", 0600,
+                            uid=self.UID_B, gid=self.GID_B,
+                            _stat_fn=_MockStatResult(None, 0600,
+                                                     self.UID_A,
+                                                     self.GID_A),
+                            _chmod_fn=self._ChmodWrapper(None),
+                            _chown_fn=self._FakeChown)
+    self.assertEqual(self._chown_calls.pop(0),
+                     ("/ganeti-qa-non-test", self.UID_B, self.GID_B))
+
+
 if __name__ == "__main__":
   testutils.GanetiTestProgram()
index c3480c2..2c61489 100755 (executable)
@@ -44,6 +44,7 @@ class TestEtcHosts(testutils.GanetiTestCase):
       handle.write("192.0.2.1 router gw\n")
     finally:
       handle.close()
+    os.chmod(self.tmpname, 0644)
 
   def testSettingNewIp(self):
     utils.SetEtcHostsEntry(self.tmpname, "198.51.100.4", "myhost.example.com",
index 03c8d64..b787dc8 100755 (executable)
@@ -315,6 +315,27 @@ class TestShellWriter(unittest.TestCase):
     sw = None
     self.assertEqual(buf.getvalue(), "")
 
+  def testEmptyLines(self):
+    buf = StringIO()
+    sw = utils.ShellWriter(buf)
+
+    def _AddLevel(level):
+      if level == 6:
+        return
+      sw.IncIndent()
+      try:
+        # Add empty line, it should not be indented
+        sw.Write("")
+        sw.Write(str(level))
+        _AddLevel(level + 1)
+      finally:
+        sw.DecIndent()
+
+    _AddLevel(1)
+
+    self.assertEqual(buf.getvalue(),
+                     "".join("\n%s%s\n" % (i * "  ", i) for i in range(1, 6)))
+
 
 class TestNormalizeAndValidateMac(unittest.TestCase):
   def testInvalid(self):
@@ -408,21 +429,25 @@ class TestFormatTime(unittest.TestCase):
   """Testing case for FormatTime"""
 
   @staticmethod
-  def _TestInProcess(tz, timestamp, expected):
+  def _TestInProcess(tz, timestamp, usecs, expected):
     os.environ["TZ"] = tz
     time.tzset()
-    return utils.FormatTime(timestamp) == expected
+    return utils.FormatTime(timestamp, usecs=usecs) == expected
 
   def _Test(self, *args):
     # Need to use separate process as we want to change TZ
     self.assert_(utils.RunInSeparateProcess(self._TestInProcess, *args))
 
   def test(self):
-    self._Test("UTC", 0, "1970-01-01 00:00:00")
-    self._Test("America/Sao_Paulo", 1292606926, "2010-12-17 15:28:46")
-    self._Test("Europe/London", 1292606926, "2010-12-17 17:28:46")
-    self._Test("Europe/Zurich", 1292606926, "2010-12-17 18:28:46")
-    self._Test("Australia/Sydney", 1292606926, "2010-12-18 04:28:46")
+    self._Test("UTC", 0, None, "1970-01-01 00:00:00")
+    self._Test("America/Sao_Paulo", 1292606926, None, "2010-12-17 15:28:46")
+    self._Test("Europe/London", 1292606926, None, "2010-12-17 17:28:46")
+    self._Test("Europe/Zurich", 1292606926, None, "2010-12-17 18:28:46")
+    self._Test("Europe/Zurich", 1332944288, 8787, "2012-03-28 16:18:08.008787")
+    self._Test("Australia/Sydney", 1292606926, None, "2010-12-18 04:28:46")
+    self._Test("Australia/Sydney", 1292606926, None, "2010-12-18 04:28:46")
+    self._Test("Australia/Sydney", 1292606926, 999999,
+               "2010-12-18 04:28:46.999999")
 
   def testNone(self):
     self.failUnlessEqual(utils.FormatTime(None), "N/A")
@@ -524,5 +549,32 @@ class TestOrdinal(unittest.TestCase):
       self.assertEqual(utils.FormatOrdinal(value), ordinal)
 
 
+class TestTruncate(unittest.TestCase):
+  def _Test(self, text, length):
+    result = utils.Truncate(text, length)
+    self.assertTrue(len(result) <= length)
+    return result
+
+  def test(self):
+    self.assertEqual(self._Test("", 80), "")
+    self.assertEqual(self._Test("abc", 4), "abc")
+    self.assertEqual(self._Test("Hello World", 80), "Hello World")
+    self.assertEqual(self._Test("Hello World", 4), "H...")
+    self.assertEqual(self._Test("Hello World", 5), "He...")
+
+    for i in [4, 10, 100]:
+      data = i * "FooBarBaz"
+      self.assertEqual(self._Test(data, len(data)), data)
+
+    for (length, exp) in [(8, u"T\u00e4st\u2026xyz"), (7, u"T\u00e4st...")]:
+      self.assertEqual(self._Test(u"T\u00e4st\u2026xyz", length), exp)
+
+    self.assertEqual(self._Test(range(100), 20), "[0, 1, 2, 3, 4, 5...")
+
+  def testError(self):
+    for i in range(4):
+      self.assertRaises(AssertionError, utils.Truncate, "", i)
+
+
 if __name__ == "__main__":
   testutils.GanetiTestProgram()
index 2249e89..007dca8 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 #
 
-# Copyright (C) 2006, 2007, 2010, 2011 Google Inc.
+# Copyright (C) 2006, 2007, 2010, 2011, 2012 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
@@ -164,6 +164,43 @@ class TestCertVerification(testutils.GanetiTestCase):
     # Not checking return value as this certificate is expired
     utils.VerifyX509Certificate(cert, 30, 7)
 
+  @staticmethod
+  def _GenCert(key, before, validity):
+    # Urgh... mostly copied from x509.py :(
+
+    # Create self-signed certificate
+    cert = OpenSSL.crypto.X509()
+    cert.set_serial_number(1)
+    if before != 0:
+      cert.gmtime_adj_notBefore(int(before))
+    cert.gmtime_adj_notAfter(validity)
+    cert.set_issuer(cert.get_subject())
+    cert.set_pubkey(key)
+    cert.sign(key, constants.X509_CERT_SIGN_DIGEST)
+    return cert
+
+  def testClockSkew(self):
+    SKEW = constants.NODE_MAX_CLOCK_SKEW
+    # Create private and public key
+    key = OpenSSL.crypto.PKey()
+    key.generate_key(OpenSSL.crypto.TYPE_RSA, constants.RSA_KEY_BITS)
+
+    validity = 7 * 86400
+    # skew small enough, accepting cert; note that this is a timed
+    # test, and could fail if the machine is so loaded that the next
+    # few lines take more than NODE_MAX_CLOCK_SKEW / 2
+    for before in [-1, 0, SKEW / 4, SKEW / 2]:
+      cert = self._GenCert(key, before, validity)
+      result = utils.VerifyX509Certificate(cert, 1, 2)
+      self.assertEqual(result, (None, None))
+
+    # skew too great, not accepting certs
+    for before in [SKEW * 2, SKEW * 10]:
+      cert = self._GenCert(key, before, validity)
+      (status, msg) = utils.VerifyX509Certificate(cert, 1, 2)
+      self.assertEqual(status, utils.CERT_WARNING)
+      self.assertTrue(msg.startswith("Certificate not yet valid"))
+
 
 class TestVerifyCertificateInner(unittest.TestCase):
   def test(self):
index e557b2b..2131674 100755 (executable)
@@ -61,6 +61,22 @@ class TestParseCpuMask(unittest.TestCase):
       self.assertRaises(errors.ParseError, utils.ParseCpuMask, data)
 
 
+class TestParseMultiCpuMask(unittest.TestCase):
+  """Test case for the ParseMultiCpuMask function."""
+
+  def testWellFormed(self):
+    self.assertEqual(utils.ParseMultiCpuMask(""), [])
+    self.assertEqual(utils.ParseMultiCpuMask("1"), [[1]])
+    self.assertEqual(utils.ParseMultiCpuMask("0-2,4,5-5"), [[0, 1, 2, 4, 5]])
+    self.assertEqual(utils.ParseMultiCpuMask("all"), [[-1]])
+    self.assertEqual(utils.ParseMultiCpuMask("0-2:all:4,6-8"),
+      [[0, 1, 2], [-1], [4, 6, 7, 8]])
+
+  def testInvalidInput(self):
+    for data in ["garbage", "0,", "0-1-2", "2-1", "1-a", "all-all"]:
+      self.assertRaises(errors.ParseError, utils.ParseCpuMask, data)
+
+
 class TestGetMounts(unittest.TestCase):
   """Test case for GetMounts()."""
 
@@ -299,5 +315,59 @@ class TestTryConvert(unittest.TestCase):
       self.assertEqual(utils.TryConvert(fn, src), result)
 
 
+class TestVerifyDictOptions(unittest.TestCase):
+  def setUp(self):
+    self.defaults = {
+      "first_key": "foobar",
+      "foobar": {
+        "key1": "value2",
+        "key2": "value1",
+        },
+      "another_key": "another_value",
+      }
+
+  def test(self):
+    some_keys = {
+      "first_key": "blubb",
+      "foobar": {
+        "key2": "foo",
+        },
+      }
+    utils.VerifyDictOptions(some_keys, self.defaults)
+
+  def testInvalid(self):
+    some_keys = {
+      "invalid_key": "blubb",
+      "foobar": {
+        "key2": "foo",
+        },
+      }
+    self.assertRaises(errors.OpPrereqError, utils.VerifyDictOptions,
+                      some_keys, self.defaults)
+
+  def testNestedInvalid(self):
+    some_keys = {
+      "foobar": {
+        "key2": "foo",
+        "key3": "blibb"
+        },
+      }
+    self.assertRaises(errors.OpPrereqError, utils.VerifyDictOptions,
+                      some_keys, self.defaults)
+
+  def testMultiInvalid(self):
+    some_keys = {
+        "foobar": {
+          "key1": "value3",
+          "key6": "Right here",
+        },
+        "invalid_with_sub": {
+          "sub1": "value3",
+        },
+      }
+    self.assertRaises(errors.OpPrereqError, utils.VerifyDictOptions,
+                      some_keys, self.defaults)
+
+
 if __name__ == '__main__':
   testutils.GanetiTestProgram()
index 89b3b1a..1ad8d74 100755 (executable)
@@ -170,6 +170,53 @@ class TestWorkerpool(unittest.TestCase):
       wp.TerminateWorkers()
       self._CheckWorkerCount(wp, 0)
 
+  def testActive(self):
+    ctx = CountingContext()
+    wp = workerpool.WorkerPool("TestActive", 5, CountingBaseWorker)
+    try:
+      self._CheckWorkerCount(wp, 5)
+      self.assertTrue(wp._active)
+
+      # Process some tasks
+      for _ in range(10):
+        wp.AddTask((ctx, None))
+
+      wp.Quiesce()
+      self._CheckNoTasks(wp)
+      self.assertEquals(ctx.GetDoneTasks(), 10)
+
+      # Repeat a few times
+      for count in range(10):
+        # Deactivate pool
+        wp.SetActive(False)
+        self._CheckNoTasks(wp)
+
+        # Queue some more tasks
+        for _ in range(10):
+          wp.AddTask((ctx, None))
+
+        for _ in range(5):
+          # Short delays to give other threads a chance to cause breakage
+          time.sleep(.01)
+          wp.AddTask((ctx, "Hello world %s" % 999))
+          self.assertFalse(wp._active)
+
+        self.assertEquals(ctx.GetDoneTasks(), 10 + (count * 15))
+
+        # Start processing again
+        wp.SetActive(True)
+        self.assertTrue(wp._active)
+
+        # Wait for tasks to finish
+        wp.Quiesce()
+        self._CheckNoTasks(wp)
+        self.assertEquals(ctx.GetDoneTasks(), 10 + (count * 15) + 15)
+
+        self._CheckWorkerCount(wp, 5)
+    finally:
+      wp.TerminateWorkers()
+      self._CheckWorkerCount(wp, 0)
+
   def testChecksum(self):
     # Tests whether all tasks are run and, since we're only using a single
     # thread, whether everything is started in order.
diff --git a/test/gnt-cli.test b/test/gnt-cli.test
new file mode 100644 (file)
index 0000000..1c1f936
--- /dev/null
@@ -0,0 +1,72 @@
+# test the various gnt-commands for common options
+$SCRIPTS/gnt-node --help
+>>>/Usage:/
+>>>2
+>>>= 1
+$SCRIPTS/gnt-node --version
+>>>/^gnt-/
+>>>2
+>>>= 0
+
+$SCRIPTS/gnt-instance --help
+>>>/Usage:/
+>>>2
+>>>= 1
+$SCRIPTS/gnt-instance --version
+>>>/^gnt-instance/
+>>>2
+>>>= 0
+
+$SCRIPTS/gnt-os --help
+>>>/Usage:/
+>>>2
+>>>= 1
+$SCRIPTS/gnt-os --version
+>>>/^gnt-/
+>>>2
+>>>= 0
+
+$SCRIPTS/gnt-group --help
+>>>/Usage:/
+>>>2
+>>>= 1
+$SCRIPTS/gnt-group --version
+>>>/^gnt-/
+>>>2
+>>>= 0
+
+$SCRIPTS/gnt-job --help
+>>>/Usage:/
+>>>2
+>>>= 1
+$SCRIPTS/gnt-job --version
+>>>/^gnt-/
+>>>2
+>>>= 0
+
+$SCRIPTS/gnt-cluster --help
+>>>/Usage:/
+>>>2
+>>>= 1
+$SCRIPTS/gnt-cluster --version
+>>>/^gnt-/
+>>>2
+>>>= 0
+
+$SCRIPTS/gnt-backup --help
+>>>/Usage:/
+>>>2
+>>>= 1
+$SCRIPTS/gnt-backup --version
+>>>/^gnt-/
+>>>2
+>>>= 0
+
+$SCRIPTS/gnt-debug --help
+>>>/Usage:/
+>>>2
+>>>= 1
+$SCRIPTS/gnt-debug --version
+>>>/^gnt-/
+>>>2
+>>>= 0
diff --git a/test/htools-balancing.test b/test/htools-balancing.test
new file mode 100644 (file)
index 0000000..6c38d85
--- /dev/null
@@ -0,0 +1,72 @@
+### std tests
+
+# test basic parsing
+./test/hinfo -v -v -p --print-instances $BACKEND_BAL_STD
+>>>= 0
+./test/hbal -v -v -v -p --print-instances $BACKEND_BAL_STD -G group-01
+>>> !/(Nothing to do, exiting|No solution found)/
+>>>2 !/(Nothing to do, exiting|No solution found)/
+>>>= 0
+
+# test command output
+./test/hbal $BACKEND_BAL_STD -G group-01 -C -S $T/simu-rebal.standard
+>>> /gnt-instance (failover|migrate|replace-disks)/
+>>>= 0
+
+# test saving commands
+./test/hbal $BACKEND_BAL_STD -G group-01 -C$T/rebal-cmds.standard
+>>>= 0
+# and now check the file (depends on previous test)
+cat $T/rebal-cmds.standard
+>>> /gnt-instance (failover|migrate|replace-disks)/
+>>>= 0
+
+# state saved before rebalancing should be identical; depends on the
+# previous test
+diff -u $T/simu-rebal-merged.standard $T/simu-rebal.standard.original
+>>>
+>>>= 0
+
+# no double rebalance; depends on previous test
+./test/hbal -t $T/simu-rebal.standard.balanced -G group-01
+>>> /(Nothing to do, exiting|No solution found)/
+>>>= 0
+
+# hcheck sees no reason to rebalance after rebalancing was already done
+./test/hcheck -t$T/simu-rebal.standard.balanced --machine-readable
+>>> /HCHECK_INIT_CLUSTER_NEED_REBALANCE=0/
+>>>= 0
+
+### now tiered tests
+
+# test basic parsing
+./test/hinfo -v -v -p --print-instances $BACKEND_BAL_TIER
+>>>= 0
+./test/hbal -v -v -v -p --print-instances $BACKEND_BAL_TIER -G group-01
+>>> !/(Nothing to do, exiting|No solution found)/
+>>>2 !/(Nothing to do, exiting|No solution found)/
+>>>= 0
+
+# test command output
+./test/hbal $BACKEND_BAL_TIER -G group-01 -C -S $T/simu-rebal.tiered
+>>> /gnt-instance (failover|migrate|replace-disks)/
+>>>= 0
+
+# test saving commands
+./test/hbal $BACKEND_BAL_TIER -G group-01 -C$T/rebal-cmds.tiered
+>>>= 0
+# and now check the file (depends on previous test)
+cat $T/rebal-cmds.tiered
+>>> /gnt-instance (failover|migrate|replace-disks)/
+>>>= 0
+
+# state saved before rebalancing should be identical; depends on the
+# previous test
+diff -u $T/simu-rebal-merged.tiered $T/simu-rebal.tiered.original
+>>>
+>>>= 0
+
+# no double rebalance; depends on previous test
+./test/hbal -t $T/simu-rebal.tiered.balanced -G group-01
+>>> /(Nothing to do, exiting|No solution found)/
+>>>= 0
diff --git a/test/htools-basic.test b/test/htools-basic.test
new file mode 100644 (file)
index 0000000..348fce3
--- /dev/null
@@ -0,0 +1,25 @@
+# help/version tests
+./test/hail --version
+>>>= 0
+./test/hail --help
+>>>= 0
+./test/hbal --version
+>>>= 0
+./test/hbal --help
+>>>= 0
+./test/hspace --version
+>>>= 0
+./test/hspace --help
+>>>= 0
+./test/hscan --version
+>>>= 0
+./test/hscan --help
+>>>= 0
+./test/hinfo --version
+>>>= 0
+./test/hinfo --help
+>>>= 0
+./test/hcheck --version
+>>>= 0
+./test/hcheck --help
+>>>= 0
diff --git a/test/htools-dynutil.test b/test/htools-dynutil.test
new file mode 100644 (file)
index 0000000..64b0c16
--- /dev/null
@@ -0,0 +1,19 @@
+echo a > $T/dynu; ./test/hbal -U $T/dynu $BACKEND_DYNU
+>>>2 /Cannot parse line/
+>>>= !0
+
+echo a b c d e f g h > $T/dynu; ./test/hbal -U $T/dynu $BACKEND_DYNU
+>>>2 /Cannot parse line/
+>>>= !0
+
+echo inst cpu mem dsk net >$T/dynu; ./test/hbal -U $T/dynu $BACKEND_DYNU
+>>>2 /cannot parse string '(cpu|mem|dsk|net)'/
+>>>= !0
+
+# unknown instances are currently just ignored
+echo no-such-inst 2 2 2 2 > $T/dynu; ./test/hbal -U $T/dynu $BACKEND_DYNU
+>>>= 0
+
+# new-0 is the name of the first instance allocated by hspace
+echo new-0 2 2 2 2 > $T/dynu; ./test/hbal -U $T/dynu $BACKEND_DYNU
+>>>= 0
diff --git a/test/htools-excl.test b/test/htools-excl.test
new file mode 100644 (file)
index 0000000..6d97377
--- /dev/null
@@ -0,0 +1,10 @@
+./test/hbal $BACKEND_EXCL --exclude-instances no-such-instance
+>>>2 /Unknown instance/
+>>>= !0
+
+./test/hbal $BACKEND_EXCL --select-instances no-such-instances
+>>>2 /Unknown instance/
+>>>= !0
+
+./test/hbal $BACKEND_EXCL --exclude-instances new-0 --select-instances new-1
+>>>= 0
diff --git a/test/htools-hail.test b/test/htools-hail.test
new file mode 100644 (file)
index 0000000..a0bddf1
--- /dev/null
@@ -0,0 +1,77 @@
+# test that on invalid files it can't parse the request
+./test/hail /dev/null
+>>>2 /Invalid JSON/
+>>>= !0
+
+# another invalid example
+echo '[]' | ./test/hail -
+>>>2 /Unable to read JSObject/
+>>>= !0
+
+# empty dict
+echo '{}' | ./test/hail -
+>>>2 /key 'request' not found/
+>>>= !0
+
+echo '{"request": 0}' | ./test/hail -
+>>>2 /key 'request'/
+>>>= !0
+
+./test/hail $TESTDATA_DIR/hail-invalid-reloc.json
+>>>2 /key 'name': Unable to read String/
+>>>= !0
+
+# and now start the real tests
+./test/hail $TESTDATA_DIR/hail-alloc-drbd.json
+>>> /"success":true,.*,"result":\["node2","node1"\]/
+>>>= 0
+
+./test/hail $TESTDATA_DIR/hail-reloc-drbd.json
+>>> /"success":true,.*,"result":\["node1"\]/
+>>>= 0
+
+./test/hail $TESTDATA_DIR/hail-node-evac.json
+>>> /"success":true,"info":"Request successful: 0 instances failed to move and 1 were moved successfully"/
+>>>= 0
+
+./test/hail $TESTDATA_DIR/hail-change-group.json
+>>> /"success":true,"info":"Request successful: 0 instances failed to move and 1 were moved successfully"/
+>>>= 0
+
+# check that hail can use the simu backend
+./test/hail --simu p,8,8T,16g,16 $TESTDATA_DIR/hail-alloc-drbd.json
+>>> /"success":true,/
+>>>= 0
+
+# check that hail can use the text backend
+./test/hail -t $T/simu-rebal-merged.standard $TESTDATA_DIR/hail-alloc-drbd.json
+>>> /"success":true,/
+>>>= 0
+
+# check that hail can use the simu backend
+./test/hail -t $T/simu-rebal-merged.standard $TESTDATA_DIR/hail-alloc-drbd.json
+>>> /"success":true,/
+>>>= 0
+
+# check that hail pre/post saved state differs after allocation
+./test/hail -v -v -v -p $TESTDATA_DIR/hail-alloc-drbd.json -S $T/hail-alloc >/dev/null 2>&1 && ! diff -q $T/hail-alloc.pre-ialloc $T/hail-alloc.post-ialloc
+>>> /Files .* and .* differ/
+>>>= 0
+
+# check that hail pre/post saved state differs after relocation
+./test/hail -v -v -v -p $TESTDATA_DIR/hail-reloc-drbd.json -S $T/hail-reloc >/dev/null 2>&1 && ! diff -q $T/hail-reloc.pre-ialloc $T/hail-reloc.post-ialloc
+>>> /Files .* and .* differ/
+>>>= 0
+
+# evac tests
+./test/hail $T/hail-node-evac.json.primary-only
+>>> /"success":true,"info":"Request successful: 0 instances failed to move and 1 were moved successfully"/
+>>>= 0
+
+./test/hail $T/hail-node-evac.json.secondary-only
+>>> /"success":true,"info":"Request successful: 0 instances failed to move and 1 were moved successfully"/
+>>>= 0
+
+./test/hail $T/hail-node-evac.json.all
+>>> /"success":true,"info":"Request successful: 0 instances failed to move and 1 were moved successfully"/
+>>>= 0
diff --git a/test/htools-hspace.test b/test/htools-hspace.test
new file mode 100644 (file)
index 0000000..6c93d37
--- /dev/null
@@ -0,0 +1,8 @@
+# test that hspace machine readable output looks correct
+./test/hspace --simu p,4,8T,64g,16 --machine-readable --disk-template drbd -l 8
+>>> /^HTS_OK=1/
+>>>= 0
+
+# test again via a file and shell parsing
+./test/hspace --simu p,4,8T,64g,16 --machine-readable --disk-template drbd -l 8 > $T/capacity && sh -c ". $T/capacity && test x\$HTS_OK = x1"
+>>>= 0
diff --git a/test/htools-invalid.test b/test/htools-invalid.test
new file mode 100644 (file)
index 0000000..1ebc742
--- /dev/null
@@ -0,0 +1,44 @@
+# invalid option test
+./test/hail --no-such-option
+>>>= 2
+
+# invalid option test
+./test/hbal --no-such-option
+>>>= 2
+
+# invalid option test
+./test/hspace --no-such-option
+>>>= 2
+
+# invalid option test
+./test/hscan --no-such-option
+>>>= 2
+
+# invalid option test
+./test/hinfo --no-such-option
+>>>= 2
+
+# invalid option test
+./test/hcheck --no-such-option
+>>>= 2
+
+# extra arguments
+./test/hspace unexpected-argument
+>>>2
+Error: this program doesn't take any arguments.
+>>>=1
+
+./test/hbal unexpected-argument
+>>>2
+Error: this program doesn't take any arguments.
+>>>=1
+
+./test/hinfo unexpected-argument
+>>>2
+Error: this program doesn't take any arguments.
+>>>=1
+
+./test/hcheck unexpected-argument
+>>>2
+Error: this program doesn't take any arguments.
+>>>=1
diff --git a/test/htools-multi-group.test b/test/htools-multi-group.test
new file mode 100644 (file)
index 0000000..561c0ad
--- /dev/null
@@ -0,0 +1,45 @@
+# standard multi-group tests
+./test/hinfo -v -v -p --print-instances -t$T/simu-twogroups.standard
+>>>= 0
+./test/hbal -t$T/simu-twogroups.standard
+>>>= !0
+
+# hbal should not be able to balance
+./test/hbal -t$T/simu-twogroups.standard
+>>>2 /Found multiple node groups/
+>>>= !0
+
+# but hbal should be able to balance one node group
+./test/hbal -t$T/simu-twogroups.standard -G group-01
+>>>= 0
+# and it should not find an invalid group
+./test/hbal -t$T/simu-twogroups.standard -G no-such-group
+>>>= !0
+
+# tiered allocs multi-group tests
+./test/hinfo -v -v -p --print-instances -t$T/simu-twogroups.tiered
+>>>= 0
+./test/hbal -t$T/simu-twogroups.tiered
+>>>= !0
+
+# hbal should not be able to balance
+./test/hbal -t$T/simu-twogroups.tiered
+>>>2 /Found multiple node groups/
+>>>= !0
+
+# but hbal should be able to balance one node group
+./test/hbal -t$T/simu-twogroups.tiered -G group-01
+>>>= 0
+# and it should not find an invalid group
+./test/hbal -t$T/simu-twogroups.tiered -G no-such-group
+>>>= !0
+
+# hcheck should be able to run with multiple groups
+./test/hcheck -t$T/simu-twogroups.tiered --machine-readable
+>>> /HCHECK_OK=1/
+>>>= 0
+
+# hcheck should be able to improve a group with split instances
+./test/hbal -t $TESTDATA_DIR/hbal-split-insts.data -G group-01 -O node-01-001
+>>> /Cluster score improved from .* to .*/
+>>>= 0
diff --git a/test/htools-no-backend.test b/test/htools-no-backend.test
new file mode 100644 (file)
index 0000000..ab678dc
--- /dev/null
@@ -0,0 +1,21 @@
+# hail no input file
+./test/hail
+>>>= 1
+
+# hbal no backend
+./test/hbal
+>>>= 1
+
+# hspace no backend
+./test/hspace
+>>>= 1
+
+# hinfo no backend
+./test/hinfo
+>>>= 1
+
+# hbal multiple backends
+./test/hbal -t /dev/null -m localhost
+>>>2
+Error: Only one of the rapi, luxi, and data files options should be given.
+>>>= 1
diff --git a/test/htools-rapi.test b/test/htools-rapi.test
new file mode 100644 (file)
index 0000000..6e6f804
--- /dev/null
@@ -0,0 +1,11 @@
+# test loading data via RAPI
+./test/hinfo -v -v -p --print-instances -m $RAPI_URL
+>>>= 0
+
+./test/hbal -v -v -p --print-instances -m $RAPI_URL
+>>>= 0
+
+# this compares generated files from hscan
+diff -u $T/hscan/direct.hinfo $T/hscan/fromtext.hinfo
+>>>
+>>>= 0
diff --git a/test/htools-single-group.test b/test/htools-single-group.test
new file mode 100644 (file)
index 0000000..4337067
--- /dev/null
@@ -0,0 +1,29 @@
+# standard single-group tests
+./test/hinfo -v -v -p --print-instances -t$T/simu-onegroup.standard
+>>>= 0
+
+./test/hbal  -v -v -p --print-instances -t$T/simu-onegroup.standard
+>>>= 0
+
+# hbal should not be able to balance
+./test/hbal -t$T/simu-onegroup.standard
+>>> /(Nothing to do, exiting|No solution found)/
+>>>= 0
+
+
+# tiered single-group tests
+./test/hinfo -v -v -p --print-instances -t$T/simu-onegroup.tiered
+>>>= 0
+
+./test/hbal  -v -v -p --print-instances -t$T/simu-onegroup.tiered
+>>>= 0
+
+# hbal should not be able to balance
+./test/hbal -t$T/simu-onegroup.tiered
+>>> /(Nothing to do, exiting|No solution found)/
+>>>= 0
+
+# hcheck should not find reason to rebalance
+./test/hcheck -t$T/simu-onegroup.tiered --machine-readable
+>>> /HCHECK_INIT_CLUSTER_NEED_REBALANCE=0/
+>>>= 0
diff --git a/test/htools-text-backend.test b/test/htools-text-backend.test
new file mode 100644 (file)
index 0000000..b45e8bf
--- /dev/null
@@ -0,0 +1,30 @@
+# missing resources test
+./test/hbal -t $TESTDATA_DIR/missing-resources.data
+>>>2 /node node2 is missing .* ram and .* disk/
+>>>= 0
+./test/hinfo -t $TESTDATA_DIR/missing-resources.data
+>>>2 /node node2 is missing .* ram and .* disk/
+>>>= 0
+
+
+# common suffix test
+./test/hbal -t $TESTDATA_DIR/common-suffix.data -v -v
+>>>/Stripping common suffix of '\.example\.com' from names/
+>>>= 0
+./test/hinfo -t $TESTDATA_DIR/common-suffix.data -v -v
+>>>/Stripping common suffix of '\.example\.com' from names/
+>>>= 0
+
+
+# invalid node test
+./test/hbal -t $TESTDATA_DIR/invalid-node.data
+>>>2 /Unknown node '.*' for instance new-0/
+>>>= !0
+
+./test/hspace -t $TESTDATA_DIR/invalid-node.data
+>>>2 /Unknown node '.*' for instance new-0/
+>>>= !0
+
+./test/hinfo -t $TESTDATA_DIR/invalid-node.data
+>>>2 /Unknown node '.*' for instance new-0/
+>>>= !0
diff --git a/test/lockperf.py b/test/lockperf.py
new file mode 100755 (executable)
index 0000000..5128c33
--- /dev/null
@@ -0,0 +1,145 @@
+#!/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 testing lock performance"""
+
+import os
+import sys
+import time
+import optparse
+import threading
+import resource
+
+from ganeti import locking
+
+
+def ParseOptions():
+  """Parses the command line options.
+
+  In case of command line errors, it will show the usage and exit the
+  program.
+
+  @return: the options in a tuple
+
+  """
+  parser = optparse.OptionParser()
+  parser.add_option("-t", dest="thread_count", default=1, type="int",
+                    help="Number of threads", metavar="NUM")
+  parser.add_option("-d", dest="duration", default=5, type="float",
+                    help="Duration", metavar="SECS")
+
+  (opts, args) = parser.parse_args()
+
+  if opts.thread_count < 1:
+    parser.error("Number of threads must be at least 1")
+
+  return (opts, args)
+
+
+class State:
+  def __init__(self, thread_count):
+    """Initializes this class.
+
+    """
+    self.verify = [0 for _ in range(thread_count)]
+    self.counts = [0 for _ in range(thread_count)]
+    self.total_count = 0
+
+
+def _Counter(lock, state, me):
+  """Thread function for acquiring locks.
+
+  """
+  counts = state.counts
+  verify = state.verify
+
+  while True:
+    lock.acquire()
+    try:
+      verify[me] = 1
+
+      counts[me] += 1
+
+      state.total_count += 1
+
+      if state.total_count % 1000 == 0:
+        sys.stdout.write(" %8d\r" % state.total_count)
+        sys.stdout.flush()
+
+      if sum(verify) != 1:
+        print "Inconsistent state!"
+        os._exit(1) # pylint: disable=W0212
+
+      verify[me] = 0
+    finally:
+      lock.release()
+
+
+def main():
+  (opts, _) = ParseOptions()
+
+  lock = locking.SharedLock("TestLock")
+
+  state = State(opts.thread_count)
+
+  lock.acquire(shared=0)
+  try:
+    for i in range(opts.thread_count):
+      t = threading.Thread(target=_Counter, args=(lock, state, i))
+      t.setDaemon(True)
+      t.start()
+
+    start = time.clock()
+  finally:
+    lock.release()
+
+  while True:
+    if (time.clock() - start) > opts.duration:
+      break
+    time.sleep(0.1)
+
+  # Make sure we get a consistent view
+  lock.acquire(shared=0)
+
+  lock_cputime = time.clock() - start
+
+  res = resource.getrusage(resource.RUSAGE_SELF)
+
+  print "Total number of acquisitions: %s" % state.total_count
+  print "Per-thread acquisitions:"
+  for (i, count) in enumerate(state.counts):
+    print ("  Thread %s: %d (%0.1f%%)" %
+           (i, count, (100.0 * count / state.total_count)))
+
+  print "Benchmark CPU time: %0.3fs" % lock_cputime
+  print ("Average time per lock acquisition: %0.5fms" %
+         (1000.0 * lock_cputime / state.total_count))
+  print "Process:"
+  print "  User time: %0.3fs" % res.ru_utime
+  print "  System time: %0.3fs" % res.ru_stime
+  print "  Total time: %0.3fs" % (res.ru_utime + res.ru_stime)
+
+  # Exit directly without attempting to clean up threads
+  os._exit(0) # pylint: disable=W0212
+
+
+if __name__ == "__main__":
+  main()
index 61eafbc..3d5d68c 100644 (file)
@@ -109,3 +109,9 @@ class FakeGetentResolver:
 
     self.daemons_gid = gid
     self.admin_gid = gid
+
+  def LookupUid(self, uid):
+    return "user%s" % uid
+
+  def LookupGid(self, gid):
+    return "group%s" % gid
diff --git a/test/pycurl_reset_unittest.py b/test/pycurl_reset_unittest.py
new file mode 100755 (executable)
index 0000000..7e3e43b
--- /dev/null
@@ -0,0 +1,74 @@
+#!/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 testing for an issue in PycURL"""
+
+import sys
+import warnings
+import unittest
+import textwrap
+import pycurl
+
+import testutils
+
+
+DETAILS = [
+  ("PycURL 7.19.0 added a new function named \"reset\" on \"pycurl.Curl\""
+   " objects to release all references to other resources. Unfortunately that"
+   " version contains a bug with reference counting on the \"None\" singleton,"
+   " leading to a crash of the Python interpreter after a certain amount of"
+   " performed requests. Your system uses a version of PycURL affected by this"
+   " issue. A patch is available at [1]. A detailed description can be found"
+   " at [2].\n"),
+  "\n",
+  ("[1] http://sf.net/tracker/?"
+   "func=detail&aid=2893665&group_id=28236&atid=392777\n"),
+  "[2] https://bugzilla.redhat.com/show_bug.cgi?id=624559",
+  ]
+
+
+class TestPyCurlReset(unittest.TestCase):
+  def test(self):
+    start_refcount = sys.getrefcount(None)
+    abort_refcount = int(start_refcount * 0.8)
+
+    assert start_refcount > 100
+
+    curl = pycurl.Curl()
+    try:
+      reset_fn = curl.reset
+    except AttributeError:
+      pass
+    else:
+      for i in range(start_refcount * 2):
+        reset_fn()
+        # The bug can be detected if calling "reset" several times continously
+        # reduces the number of references
+        if sys.getrefcount(None) < abort_refcount:
+          print >>sys.stderr, "#" * 78
+          for line in DETAILS:
+            print >>sys.stderr, textwrap.fill(line, width=78)
+          print >>sys.stderr, "#" * 78
+          break
+
+
+if __name__ == "__main__":
+  testutils.GanetiTestProgram()
diff --git a/test/qa.qa_config_unittest.py b/test/qa.qa_config_unittest.py
new file mode 100755 (executable)
index 0000000..fd73322
--- /dev/null
@@ -0,0 +1,137 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2012 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 testing qa.qa_config"""
+
+import unittest
+
+from qa import qa_config
+
+import testutils
+
+
+class TestTestEnabled(unittest.TestCase):
+  def testSimple(self):
+    for name in ["test", ["foobar"], ["a", "b"]]:
+      self.assertTrue(qa_config.TestEnabled(name, _cfg={}))
+
+    for default in [False, True]:
+      self.assertFalse(qa_config.TestEnabled("foo", _cfg={
+        "tests": {
+          "default": default,
+          "foo": False,
+          },
+        }))
+
+      self.assertTrue(qa_config.TestEnabled("bar", _cfg={
+        "tests": {
+          "default": default,
+          "bar": True,
+          },
+        }))
+
+  def testEitherWithDefault(self):
+    names = qa_config.Either("one")
+
+    self.assertTrue(qa_config.TestEnabled(names, _cfg={
+      "tests": {
+        "default": True,
+        },
+      }))
+
+    self.assertFalse(qa_config.TestEnabled(names, _cfg={
+      "tests": {
+        "default": False,
+        },
+      }))
+
+  def testEither(self):
+    names = [qa_config.Either(["one", "two"]),
+             qa_config.Either("foo"),
+             "hello",
+             ["bar", "baz"]]
+
+    self.assertTrue(qa_config.TestEnabled(names, _cfg={
+      "tests": {
+        "default": True,
+        },
+      }))
+
+    self.assertFalse(qa_config.TestEnabled(names, _cfg={
+      "tests": {
+        "default": False,
+        },
+      }))
+
+    for name in ["foo", "bar", "baz", "hello"]:
+      self.assertFalse(qa_config.TestEnabled(names, _cfg={
+        "tests": {
+          "default": True,
+          name: False,
+          },
+        }))
+
+    self.assertFalse(qa_config.TestEnabled(names, _cfg={
+      "tests": {
+        "default": True,
+        "one": False,
+        "two": False,
+        },
+      }))
+
+    self.assertTrue(qa_config.TestEnabled(names, _cfg={
+      "tests": {
+        "default": True,
+        "one": False,
+        "two": True,
+        },
+      }))
+
+    self.assertFalse(qa_config.TestEnabled(names, _cfg={
+      "tests": {
+        "default": True,
+        "one": True,
+        "two": True,
+        "foo": False,
+        },
+      }))
+
+  def testEitherNestedWithAnd(self):
+    names = qa_config.Either([["one", "two"], "foo"])
+
+    self.assertTrue(qa_config.TestEnabled(names, _cfg={
+      "tests": {
+        "default": True,
+        },
+      }))
+
+    for name in ["one", "two"]:
+      self.assertFalse(qa_config.TestEnabled(names, _cfg={
+        "tests": {
+          "default": True,
+          "foo": False,
+          name: False,
+          },
+        }))
+
+
+if __name__ == "__main__":
+  testutils.GanetiTestProgram()
index 396c393..6811f76 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 #
 
-# Copyright (C) 2010 Google Inc.
+# Copyright (C) 2010, 2012 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
@@ -47,6 +47,14 @@ class TestResetTempfileModule(unittest.TestCase):
     shutil.rmtree(self.tmpdir)
 
   def testNoReset(self):
+    if ((sys.hexversion >= 0x020703F0 and sys.hexversion < 0x03000000) or
+        sys.hexversion >= 0x030203F0):
+      # We can't test the no_reset case on Python 2.7+
+      return
+    # evil Debian sid...
+    if (hasattr(tempfile._RandomNameSequence, "rng") and
+        type(tempfile._RandomNameSequence.rng) == property):
+      return
     self._Test(False)
 
   def testReset(self):
index 2578c3d..1a47a66 100644 (file)
@@ -136,6 +136,32 @@ class GanetiTestCase(unittest.TestCase):
     actual_mode = stat.S_IMODE(st.st_mode)
     self.assertEqual(actual_mode, expected_mode)
 
+  def assertFileUid(self, file_name, expected_uid):
+    """Checks that the user id of a file is what we expect.
+
+    @type file_name: str
+    @param file_name: the file whose contents we should check
+    @type expected_uid: int
+    @param expected_uid: the user id we expect
+
+    """
+    st = os.stat(file_name)
+    actual_uid = st.st_uid
+    self.assertEqual(actual_uid, expected_uid)
+
+  def assertFileGid(self, file_name, expected_gid):
+    """Checks that the group id of a file is what we expect.
+
+    @type file_name: str
+    @param file_name: the file whose contents we should check
+    @type expected_gid: int
+    @param expected_gid: the group id we expect
+
+    """
+    st = os.stat(file_name)
+    actual_gid = st.st_gid
+    self.assertEqual(actual_gid, expected_gid)
+
   def assertEqualValues(self, first, second, msg=None):
     """Compares two values whether they're equal.
 
index c5d1612..bf93c72 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 #
 
-# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 Google Inc.
+# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012 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
@@ -124,6 +124,14 @@ OPTIONS = [
   cli.cli_option("--disk-growth", dest="disk_growth", help="Disk growth",
                  default="128m", type="string", metavar="<size,size,...>"),
   cli.cli_option("--mem-size", dest="mem_size", help="Memory size",
+                 default=None, type="unit", metavar="<size>",
+                 completion_suggest=("128M 256M 512M 1G 4G 8G"
+                                     " 12G 16G").split()),
+  cli.cli_option("--maxmem-size", dest="maxmem_size", help="Max Memory size",
+                 default=256, type="unit", metavar="<size>",
+                 completion_suggest=("128M 256M 512M 1G 4G 8G"
+                                     " 12G 16G").split()),
+  cli.cli_option("--minmem-size", dest="minmem_size", help="Min Memory size",
                  default=128, type="unit", metavar="<size>",
                  completion_suggest=("128M 256M 512M 1G 4G 8G"
                                      " 12G 16G").split()),
@@ -178,7 +186,7 @@ OPTIONS = [
                  const=[], default=[{}]),
   cli.cli_option("--no-confd", dest="do_confd_tests",
                  help="Skip confd queries",
-                 action="store_false", default=True),
+                 action="store_false", default=constants.ENABLE_CONFD),
   cli.cli_option("--rename", dest="rename", default=None,
                  help=("Give one unused instance name which is taken"
                        " to start the renaming sequence"),
@@ -442,11 +450,19 @@ class Burner(object):
     if len(args) < 1 or options.os is None:
       Usage()
 
+    if options.mem_size:
+      options.maxmem_size = options.mem_size
+      options.minmem_size = options.mem_size
+    elif options.minmem_size > options.maxmem_size:
+      Err("Maximum memory lower than minimum memory")
+
     supported_disk_templates = (constants.DT_DISKLESS,
                                 constants.DT_FILE,
                                 constants.DT_SHARED_FILE,
                                 constants.DT_PLAIN,
-                                constants.DT_DRBD8)
+                                constants.DT_DRBD8,
+                                constants.DT_RBD,
+                                )
     if options.disk_template not in supported_disk_templates:
       Err("Unknown disk template '%s'" % options.disk_template)
 
@@ -476,7 +492,8 @@ class Burner(object):
     self.opts = options
     self.instances = args
     self.bep = {
-      constants.BE_MEMORY: options.mem_size,
+      constants.BE_MINMEM: options.minmem_size,
+      constants.BE_MAXMEM: options.maxmem_size,
       constants.BE_VCPUS: options.vcpu_count,
       }
 
@@ -589,6 +606,18 @@ class Burner(object):
       self.ExecOrQueue(instance, [op], post_process=remove_instance(instance))
 
   @_DoBatch(False)
+  def BurnModifyRuntimeMemory(self):
+    """Alter the runtime memory."""
+    Log("Setting instance runtime memory")
+    for instance in self.instances:
+      Log("instance %s", instance, indent=1)
+      tgt_mem = self.bep[constants.BE_MINMEM]
+      op = opcodes.OpInstanceSetParams(instance_name=instance,
+                                       runtime_mem=tgt_mem)
+      Log("Set memory to %s MB", tgt_mem, indent=2)
+      self.ExecOrQueue(instance, [op])
+
+  @_DoBatch(False)
   def BurnGrowDisks(self):
     """Grow both the os and the swap disks by the requested amount, if any."""
     Log("Growing disks")
@@ -876,17 +905,21 @@ class Burner(object):
 
   @_DoBatch(False)
   def BurnAddRemoveNICs(self):
-    """Add and remove an extra NIC for the instances."""
+    """Add, change and remove an extra NIC for the instances."""
     Log("Adding and removing NICs")
     for instance in self.instances:
       Log("instance %s", instance, indent=1)
       op_add = opcodes.OpInstanceSetParams(\
         instance_name=instance, nics=[(constants.DDM_ADD, {})])
+      op_chg = opcodes.OpInstanceSetParams(\
+        instance_name=instance, nics=[(constants.DDM_MODIFY,
+                                       -1, {"mac": constants.VALUE_GENERATE})])
       op_rem = opcodes.OpInstanceSetParams(\
         instance_name=instance, nics=[(constants.DDM_REMOVE, {})])
       Log("adding a NIC", indent=2)
+      Log("changing a NIC", indent=2)
       Log("removing last NIC", indent=2)
-      self.ExecOrQueue(instance, [op_add, op_rem])
+      self.ExecOrQueue(instance, [op_add, op_chg, op_rem])
 
   def ConfdCallback(self, reply):
     """Callback for confd queries"""
@@ -991,9 +1024,16 @@ class Burner(object):
       Err("When one node is available/selected the disk template must"
           " be 'diskless', 'file' or 'plain'")
 
+    if opts.do_confd_tests and not constants.ENABLE_CONFD:
+      Err("You selected confd tests but confd was disabled at configure time")
+
     has_err = True
     try:
       self.BurnCreateInstances()
+
+      if self.bep[constants.BE_MINMEM] < self.bep[constants.BE_MAXMEM]:
+        self.BurnModifyRuntimeMemory()
+
       if opts.do_replace1 and opts.disk_template in constants.DTS_INT_MIRROR:
         self.BurnReplaceDisks1D8()
       if (opts.do_replace2 and len(self.nodes) > 2 and
index b44ea6c..81dce1d 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 #
 
-# Copyright (C) 2007, 2008, 2009, 2010 Google Inc.
+# Copyright (C) 2007, 2008, 2009, 2010, 2011, 2012 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
@@ -46,6 +46,12 @@ options = None
 args = None
 
 
+#: Target major version we will upgrade to
+TARGET_MAJOR = 2
+#: Target minor version we will upgrade to
+TARGET_MINOR = 6
+
+
 class Error(Exception):
   """Generic exception"""
   pass
@@ -97,7 +103,7 @@ def main():
 
   # Option parsing
   parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
-  parser.add_option('--dry-run', dest='dry_run',
+  parser.add_option("--dry-run", dest="dry_run",
                     action="store_true",
                     help="Try to do the conversion, but don't write"
                          " output file")
@@ -107,7 +113,7 @@ def main():
   parser.add_option("--ignore-hostname", dest="ignore_hostname",
                     action="store_true", default=False,
                     help="Don't abort if hostname doesn't match")
-  parser.add_option('--path', help="Convert configuration in this"
+  parser.add_option("--path", help="Convert configuration in this"
                     " directory instead of '%s'" % constants.DATA_DIR,
                     default=constants.DATA_DIR, dest="data_dir")
   parser.add_option("--no-verify",
@@ -122,6 +128,8 @@ def main():
   options.SERVER_PEM_PATH = options.data_dir + "/server.pem"
   options.KNOWN_HOSTS_PATH = options.data_dir + "/known_hosts"
   options.RAPI_CERT_FILE = options.data_dir + "/rapi.pem"
+  options.SPICE_CERT_FILE = options.data_dir + "/spice.pem"
+  options.SPICE_CACERT_FILE = options.data_dir + "/spice-ca.pem"
   options.RAPI_USERS_FILE = options.data_dir + "/rapi/users"
   options.RAPI_USERS_FILE_PRE24 = options.data_dir + "/rapi_users"
   options.CONFD_HMAC_KEY = options.data_dir + "/hmac.key"
@@ -173,13 +181,29 @@ def main():
                 " configuration file")
 
   # Upgrade from 2.0/2.1/2.2/2.3 to 2.4
-  if config_major == 2 and config_minor in (0, 1, 2, 3, 4):
+  if config_major == 2 and config_minor in (0, 1, 2, 3, 4, 5):
     if config_revision != 0:
       logging.warning("Config revision is %s, not 0", config_revision)
 
-    config_data["version"] = constants.BuildVersion(2, 5, 0)
-
-  elif config_major == 2 and config_minor == 5:
+    config_data["version"] = constants.BuildVersion(TARGET_MAJOR,
+                                                    TARGET_MINOR, 0)
+
+    if "instances" not in config_data:
+      raise Error("Can't find the 'instances' key in the configuration!")
+    for instance, iobj in config_data["instances"].items():
+      if "disks" not in iobj:
+        raise Error("Instance '%s' doesn't have a disks entry?!" % instance)
+      disks = iobj["disks"]
+      for idx, dobj in enumerate(disks):
+        expected = "disk/%s" % idx
+        current = dobj.get("iv_name", "")
+        if current != expected:
+          logging.warning("Updating iv_name for instance %s/disk %s"
+                          " from '%s' to '%s'",
+                          instance, idx, current, expected)
+          dobj["iv_name"] = expected
+
+  elif config_major == TARGET_MAJOR and config_minor == TARGET_MINOR:
     logging.info("No changes necessary")
 
   else:
@@ -222,11 +246,13 @@ def main():
                     backup=True)
 
     if not options.dry_run:
-      bootstrap.GenerateClusterCrypto(False, False, False, False,
-                                      nodecert_file=options.SERVER_PEM_PATH,
-                                      rapicert_file=options.RAPI_CERT_FILE,
-                                      hmackey_file=options.CONFD_HMAC_KEY,
-                                      cds_file=options.CDS_FILE)
+      bootstrap.GenerateClusterCrypto(False, False, False, False, False,
+                                     nodecert_file=options.SERVER_PEM_PATH,
+                                     rapicert_file=options.RAPI_CERT_FILE,
+                                     spicecert_file=options.SPICE_CERT_FILE,
+                                     spicecacert_file=options.SPICE_CACERT_FILE,
+                                     hmackey_file=options.CONFD_HMAC_KEY,
+                                     cds_file=options.CDS_FILE)
 
   except Exception:
     logging.critical("Writing configuration failed. It is probably in an"
index 83187c3..5bf426d 100755 (executable)
@@ -57,26 +57,26 @@ NoDefault = object()
 
 # Dictionary with instance old keys, and new hypervisor keys
 INST_HV_CHG = {
-  'hvm_pae': constants.HV_PAE,
-  'vnc_bind_address': constants.HV_VNC_BIND_ADDRESS,
-  'initrd_path': constants.HV_INITRD_PATH,
-  'hvm_nic_type': constants.HV_NIC_TYPE,
-  'kernel_path': constants.HV_KERNEL_PATH,
-  'hvm_acpi': constants.HV_ACPI,
-  'hvm_cdrom_image_path': constants.HV_CDROM_IMAGE_PATH,
-  'hvm_boot_order': constants.HV_BOOT_ORDER,
-  'hvm_disk_type': constants.HV_DISK_TYPE,
+  "hvm_pae": constants.HV_PAE,
+  "vnc_bind_address": constants.HV_VNC_BIND_ADDRESS,
+  "initrd_path": constants.HV_INITRD_PATH,
+  "hvm_nic_type": constants.HV_NIC_TYPE,
+  "kernel_path": constants.HV_KERNEL_PATH,
+  "hvm_acpi": constants.HV_ACPI,
+  "hvm_cdrom_image_path": constants.HV_CDROM_IMAGE_PATH,
+  "hvm_boot_order": constants.HV_BOOT_ORDER,
+  "hvm_disk_type": constants.HV_DISK_TYPE,
   }
 
 # Instance beparams changes
 INST_BE_CHG = {
-  'vcpus': constants.BE_VCPUS,
-  'memory': constants.BE_MEMORY,
-  'auto_balance': constants.BE_AUTO_BALANCE,
+  "vcpus": constants.BE_VCPUS,
+  "memory": constants.BE_MEMORY,
+  "auto_balance": constants.BE_AUTO_BALANCE,
   }
 
 # Field names
-F_SERIAL = 'serial_no'
+F_SERIAL = "serial_no"
 
 
 class Error(Exception):
@@ -97,7 +97,7 @@ def ReadFile(file_name, default=NoDefault):
   """
   logging.debug("Reading %s", file_name)
   try:
-    fh = open(file_name, 'r')
+    fh = open(file_name, "r")
   except IOError, err:
     if default is not NoDefault and err.errno == errno.ENOENT:
       return default
@@ -161,17 +161,17 @@ def Cluster12To20(cluster):
   """
   logging.info("Upgrading the cluster object")
   # Upgrade the configuration version
-  if 'config_version' in cluster:
-    del cluster['config_version']
+  if "config_version" in cluster:
+    del cluster["config_version"]
 
   # Add old ssconf keys back to config
   logging.info(" - importing ssconf keys")
-  for key in ('master_node', 'master_ip', 'master_netdev', 'cluster_name'):
+  for key in ("master_node", "master_ip", "master_netdev", "cluster_name"):
     if key not in cluster:
       cluster[key] = ReadFile(SsconfName(key)).strip()
 
-  if 'default_hypervisor' not in cluster:
-    old_hyp = ReadFile(SsconfName('hypervisor')).strip()
+  if "default_hypervisor" not in cluster:
+    old_hyp = ReadFile(SsconfName("hypervisor")).strip()
     if old_hyp == "xen-3.0":
       hyp = "xen-pvm"
     elif old_hyp == "xen-hvm-3.1":
@@ -182,24 +182,24 @@ def Cluster12To20(cluster):
       raise Error("Unknown old hypervisor name '%s'" % old_hyp)
 
     logging.info("Setting the default and enabled hypervisor")
-    cluster['default_hypervisor'] = hyp
-    cluster['enabled_hypervisors'] = [hyp]
+    cluster["default_hypervisor"] = hyp
+    cluster["enabled_hypervisors"] = [hyp]
 
   # hv/be params
-  if 'hvparams' not in cluster:
+  if "hvparams" not in cluster:
     logging.info(" - adding hvparams")
-    cluster['hvparams'] = constants.HVC_DEFAULTS
-  if 'beparams' not in cluster:
+    cluster["hvparams"] = constants.HVC_DEFAULTS
+  if "beparams" not in cluster:
     logging.info(" - adding beparams")
-    cluster['beparams'] = {constants.PP_DEFAULT: constants.BEC_DEFAULTS}
+    cluster["beparams"] = {constants.PP_DEFAULT: constants.BEC_DEFAULTS}
 
   # file storage
-  if 'file_storage_dir' not in cluster:
-    cluster['file_storage_dir'] = constants.DEFAULT_FILE_STORAGE_DIR
+  if "file_storage_dir" not in cluster:
+    cluster["file_storage_dir"] = constants.DEFAULT_FILE_STORAGE_DIR
 
   # candidate pool size
-  if 'candidate_pool_size' not in cluster:
-    cluster['candidate_pool_size'] = constants.MASTER_POOL_SIZE_DEFAULT
+  if "candidate_pool_size" not in cluster:
+    cluster["candidate_pool_size"] = constants.MASTER_POOL_SIZE_DEFAULT
 
 
 def Node12To20(node):
@@ -209,9 +209,9 @@ def Node12To20(node):
   logging.info("Upgrading node %s", node['name'])
   if F_SERIAL not in node:
     node[F_SERIAL] = 1
-  if 'master_candidate' not in node:
-    node['master_candidate'] = True
-  for key in 'offline', 'drained':
+  if "master_candidate" not in node:
+    node["master_candidate"] = True
+  for key in "offline", "drained":
     if key not in node:
       node[key] = False
 
@@ -223,12 +223,12 @@ def Instance12To20(drbd_minors, secrets, hypervisor, instance):
   if F_SERIAL not in instance:
     instance[F_SERIAL] = 1
 
-  if 'hypervisor' not in instance:
-    instance['hypervisor'] = hypervisor
+  if "hypervisor" not in instance:
+    instance["hypervisor"] = hypervisor
 
   # hvparams changes
-  if 'hvparams' not in instance:
-    instance['hvparams'] = hvp = {}
+  if "hvparams" not in instance:
+    instance["hvparams"] = hvp = {}
   for old, new in INST_HV_CHG.items():
     if old in instance:
       if (instance[old] is not None and
@@ -238,8 +238,8 @@ def Instance12To20(drbd_minors, secrets, hypervisor, instance):
       del instance[old]
 
   # beparams changes
-  if 'beparams' not in instance:
-    instance['beparams'] = bep = {}
+  if "beparams" not in instance:
+    instance["beparams"] = bep = {}
   for old, new in INST_BE_CHG.items():
     if old in instance:
       if instance[old] is not None:
@@ -247,23 +247,23 @@ def Instance12To20(drbd_minors, secrets, hypervisor, instance):
       del instance[old]
 
   # disk changes
-  for disk in instance['disks']:
+  for disk in instance["disks"]:
     Disk12To20(drbd_minors, secrets, disk)
 
   # other instance changes
-  if 'status' in instance:
-    instance['admin_up'] = instance['status'] == 'up'
-    del instance['status']
+  if "status" in instance:
+    instance["admin_up"] = instance["status"] == "up"
+    del instance["status"]
 
 
 def Disk12To20(drbd_minors, secrets, disk):
   """Upgrades a disk from 1.2 to 2.0.
 
   """
-  if 'mode' not in disk:
-    disk['mode'] = constants.DISK_RDWR
-  if disk['dev_type'] == constants.LD_DRBD8:
-    old_lid = disk['logical_id']
+  if "mode" not in disk:
+    disk["mode"] = constants.DISK_RDWR
+  if disk["dev_type"] == constants.LD_DRBD8:
+    old_lid = disk["logical_id"]
     for node in old_lid[:2]:
       if node not in drbd_minors:
         raise Error("Can't find node '%s' while upgrading disk" % node)
@@ -271,9 +271,9 @@ def Disk12To20(drbd_minors, secrets, disk):
       minor = drbd_minors[node]
       old_lid.append(minor)
     old_lid.append(GenerateSecret(secrets))
-    del disk['physical_id']
-  if disk['children']:
-    for child in disk['children']:
+    del disk["physical_id"]
+  if disk["children"]:
+    for child in disk["children"]:
       Disk12To20(drbd_minors, secrets, child)
 
 
@@ -288,14 +288,14 @@ def main():
 
   # Option parsing
   parser = optparse.OptionParser(usage="%prog [--debug|--verbose] [--force]")
-  parser.add_option('--dry-run', dest='dry_run',
+  parser.add_option("--dry-run", dest="dry_run",
                     action="store_true",
                     help="Try to do the conversion, but don't write"
                          " output file")
   parser.add_option(cli.FORCE_OPT)
   parser.add_option(cli.DEBUG_OPT)
   parser.add_option(cli.VERBOSE_OPT)
-  parser.add_option('--path', help="Convert configuration in this"
+  parser.add_option("--path", help="Convert configuration in this"
                     " directory instead of '%s'" % constants.DATA_DIR,
                     default=constants.DATA_DIR, dest="data_dir")
   (options, args) = parser.parse_args()
@@ -327,7 +327,7 @@ def main():
     raise Error(("%s does not seem to be a known Ganeti configuration"
                  " directory") % options.data_dir)
 
-  config_version = ReadFile(SsconfName('config_version'), "1.2").strip()
+  config_version = ReadFile(SsconfName("config_version"), "1.2").strip()
   logging.info("Found configuration version %s", config_version)
 
   config_data = serializer.LoadJson(ReadFile(options.CONFIG_DATA_PATH))
@@ -343,8 +343,8 @@ def main():
     if old_config_version not in (3, ):
       raise Error("Unsupported configuration version: %s" %
                   old_config_version)
-    if 'version' not in config_data:
-      config_data['version'] = constants.BuildVersion(2, 0, 0)
+    if "version" not in config_data:
+      config_data["version"] = constants.BuildVersion(2, 0, 0)
     if F_SERIAL not in config_data:
       config_data[F_SERIAL] = 1
 
@@ -361,8 +361,8 @@ def main():
                   " instances using remote_raid1 disk template")
 
     # Build content of new known_hosts file
-    cluster_name = ReadFile(SsconfName('cluster_name')).rstrip()
-    cluster_key = cluster['rsahostkeypub']
+    cluster_name = ReadFile(SsconfName("cluster_name")).rstrip()
+    cluster_key = cluster["rsahostkeypub"]
     known_hosts = "%s ssh-rsa %s\n" % (cluster_name, cluster_key)
 
     Cluster12To20(cluster)
@@ -370,17 +370,17 @@ def main():
     # Add node attributes
     logging.info("Upgrading nodes")
     # stable-sort the names to have repeatable runs
-    for node_name in utils.NiceSort(config_data['nodes'].keys()):
-      Node12To20(config_data['nodes'][node_name])
+    for node_name in utils.NiceSort(config_data["nodes"].keys()):
+      Node12To20(config_data["nodes"][node_name])
 
     # Instance changes
     logging.info("Upgrading instances")
-    drbd_minors = dict.fromkeys(config_data['nodes'], 0)
+    drbd_minors = dict.fromkeys(config_data["nodes"], 0)
     secrets = set()
     # stable-sort the names to have repeatable runs
-    for instance_name in utils.NiceSort(config_data['instances'].keys()):
-      Instance12To20(drbd_minors, secrets, cluster['default_hypervisor'],
-                     config_data['instances'][instance_name])
+    for instance_name in utils.NiceSort(config_data["instances"].keys()):
+      Instance12To20(drbd_minors, secrets, cluster["default_hypervisor"],
+                     config_data["instances"][instance_name])
 
   else:
     logging.info("Found a Ganeti 2.0 configuration")
index 40bc259..f94a32f 100755 (executable)
@@ -40,7 +40,6 @@ from ganeti import constants
 from ganeti import errors
 from ganeti import ssh
 from ganeti import utils
-from ganeti import netutils
 
 
 _GROUPS_MERGE = "merge"
@@ -111,7 +110,7 @@ class MergerData(object):
 
   """
   def __init__(self, cluster, key_path, nodes, instances, master_node,
-               master_ip, config_path=None):
+               config_path=None):
     """Initialize the container.
 
     @param cluster: The name of the cluster
@@ -119,7 +118,6 @@ class MergerData(object):
     @param nodes: List of online nodes in the merging cluster
     @param instances: List of instances running on merging cluster
     @param master_node: Name of the master node
-    @param master_ip: Cluster IP
     @param config_path: Path to the merging cluster config
 
     """
@@ -128,7 +126,6 @@ class MergerData(object):
     self.nodes = nodes
     self.instances = instances
     self.master_node = master_node
-    self.master_ip = master_ip
     self.config_path = config_path
 
 
@@ -195,16 +192,16 @@ class Merger(object):
       utils.WriteFile(key_path, mode=0600, data=result.stdout)
 
       result = self._RunCmd(cluster, "gnt-node list -o name,offline"
-                            " --no-header --separator=,", private_key=key_path)
+                            " --no-headers --separator=,", private_key=key_path)
       if result.failed:
         raise errors.RemoteError("Unable to retrieve list of nodes from %s."
                                  " Fail reason: %s; output: %s" %
                                  (cluster, result.fail_reason, result.output))
-      nodes_statuses = [line.split(',') for line in result.stdout.splitlines()]
+      nodes_statuses = [line.split(",") for line in result.stdout.splitlines()]
       nodes = [node_status[0] for node_status in nodes_statuses
                if node_status[1] == "N"]
 
-      result = self._RunCmd(cluster, "gnt-instance list -o name --no-header",
+      result = self._RunCmd(cluster, "gnt-instance list -o name --no-headers",
                             private_key=key_path)
       if result.failed:
         raise errors.RemoteError("Unable to retrieve list of instances from"
@@ -221,17 +218,8 @@ class Merger(object):
                                  (cluster, result.fail_reason, result.output))
       master_node = result.stdout.strip()
 
-      path = utils.PathJoin(constants.DATA_DIR, "ssconf_%s" %
-                            constants.SS_MASTER_IP)
-      result = self._RunCmd(cluster, "cat %s" % path, private_key=key_path)
-      if result.failed:
-        raise errors.RemoteError("Unable to retrieve the master IP from"
-                                 " %s. Fail reason: %s; output: %s" %
-                                 (cluster, result.fail_reason, result.output))
-      master_ip = result.stdout.strip()
-
       self.merger_data.append(MergerData(cluster, key_path, nodes, instances,
-                                         master_node, master_ip))
+                                         master_node))
 
   def _PrepareAuthorizedKeys(self):
     """Prepare the authorized_keys on every merging node.
@@ -319,19 +307,9 @@ class Merger(object):
 
     """
     for data in self.merger_data:
-      master_ip_family = netutils.IPAddress.GetAddressFamily(data.master_ip)
-      master_ip_len = netutils.IP4Address.iplen
-      if master_ip_family == netutils.IP6Address.family:
-        master_ip_len = netutils.IP6Address.iplen
-      # Not using constants.IP_COMMAND_PATH because the command might run on a
-      # machine in which the ip path is different, so it's better to rely on
-      # $PATH.
-      cmd = "ip address del %s/%s dev $(cat %s)" % (
-             data.master_ip,
-             master_ip_len,
-             utils.PathJoin(constants.DATA_DIR, "ssconf_%s" %
-                            constants.SS_MASTER_NETDEV))
-      result = self._RunCmd(data.master_node, cmd, max_attempts=3)
+      result = self._RunCmd(data.master_node,
+                            "gnt-cluster deactivate-master-ip --yes")
+
       if result.failed:
         raise errors.RemoteError("Unable to remove master IP on %s."
                                  " Fail reason: %s; output: %s" %
@@ -670,6 +648,7 @@ class Merger(object):
     """
     for data in self.merger_data:
       for node in data.nodes:
+        logging.info("Readding node %s", node)
         result = utils.RunCmd(["gnt-node", "add", "--readd",
                                "--no-ssh-key-check", "--force-join", node])
         if result.failed:
@@ -739,12 +718,12 @@ class Merger(object):
                                   " mergees")
       logging.info("Disable watcher")
       self._DisableWatcher()
-      logging.info("Stop daemons on merging nodes")
-      self._StopDaemons()
       logging.info("Merging config")
       self._FetchRemoteConfig()
       logging.info("Removing master IPs on mergee master nodes")
       self._RemoveMasterIps()
+      logging.info("Stop daemons on merging nodes")
+      self._StopDaemons()
 
       logging.info("Stopping master daemon")
       self._KillMasterDaemon()
diff --git a/tools/confd-client b/tools/confd-client
new file mode 100755 (executable)
index 0000000..34b8b52
--- /dev/null
@@ -0,0 +1,281 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2006, 2007, 2008, 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.
+
+# pylint: disable=C0103
+
+"""confd client program
+
+This is can be used to test and debug confd daemon functionality.
+
+"""
+
+import sys
+import optparse
+import time
+
+from ganeti import constants
+from ganeti import cli
+from ganeti import utils
+
+from ganeti.confd import client as confd_client
+
+USAGE = ("\tconfd-client [--addr=host] [--hmac=key]")
+
+LOG_HEADERS = {
+  0: "- ",
+  1: "* ",
+  2: ""
+  }
+
+OPTIONS = [
+  cli.cli_option("--hmac", dest="hmac", default=None,
+                 help="Specify HMAC key instead of reading"
+                 " it from the filesystem",
+                 metavar="<KEY>"),
+  cli.cli_option("-a", "--address", dest="mc", default="localhost",
+                 help="Server IP to query (default: 127.0.0.1)",
+                 metavar="<ADDRESS>"),
+  cli.cli_option("-r", "--requests", dest="requests", default=100,
+                 help="Number of requests for the timing tests",
+                 type="int", metavar="<REQUESTS>"),
+  ]
+
+
+def Log(msg, *args, **kwargs):
+  """Simple function that prints out its argument.
+
+  """
+  if args:
+    msg = msg % args
+  indent = kwargs.get("indent", 0)
+  sys.stdout.write("%*s%s%s\n" % (2 * indent, "",
+                                  LOG_HEADERS.get(indent, "  "), msg))
+  sys.stdout.flush()
+
+
+def LogAtMost(msgs, count, **kwargs):
+  """Log at most count of given messages.
+
+  """
+  for m in msgs[:count]:
+    Log(m, **kwargs)
+  if len(msgs) > count:
+    Log("...", **kwargs)
+
+
+def Err(msg, exit_code=1):
+  """Simple error logging that prints to stderr.
+
+  """
+  sys.stderr.write(msg + "\n")
+  sys.stderr.flush()
+  sys.exit(exit_code)
+
+
+def Usage():
+  """Shows program usage information and exits the program."""
+
+  print >> sys.stderr, "Usage:"
+  print >> sys.stderr, USAGE
+  sys.exit(2)
+
+
+class TestClient(object):
+  """Confd test client."""
+
+  def __init__(self):
+    """Constructor."""
+    self.opts = None
+    self.cluster_master = None
+    self.instance_ips = None
+    self.is_timing = False
+    self.ParseOptions()
+
+  def ParseOptions(self):
+    """Parses the command line options.
+
+    In case of command line errors, it will show the usage and exit the
+    program.
+
+    """
+    parser = optparse.OptionParser(usage="\n%s" % USAGE,
+                                   version=("%%prog (ganeti) %s" %
+                                            constants.RELEASE_VERSION),
+                                   option_list=OPTIONS)
+
+    options, args = parser.parse_args()
+    if args:
+      Usage()
+
+    if options.hmac is None:
+      options.hmac = utils.ReadFile(constants.CONFD_HMAC_KEY)
+    self.hmac_key = options.hmac
+
+    self.mc_list = [options.mc]
+
+    self.opts = options
+
+  def ConfdCallback(self, reply):
+    """Callback for confd queries"""
+    if reply.type == confd_client.UPCALL_REPLY:
+      answer = reply.server_reply.answer
+      reqtype = reply.orig_request.type
+      if reply.server_reply.status != constants.CONFD_REPL_STATUS_OK:
+        Log("Query %s gave non-ok status %s: %s" % (reply.orig_request,
+                                                    reply.server_reply.status,
+                                                    reply.server_reply))
+        if self.is_timing:
+          Err("Aborting timing tests")
+        if reqtype == constants.CONFD_REQ_CLUSTER_MASTER:
+          Err("Cannot continue after master query failure")
+        if reqtype == constants.CONFD_REQ_INSTANCES_IPS_LIST:
+          Err("Cannot continue after instance IP list query failure")
+        return
+      if self.is_timing:
+        return
+      if reqtype == constants.CONFD_REQ_PING:
+        Log("Ping: OK")
+      elif reqtype == constants.CONFD_REQ_CLUSTER_MASTER:
+        Log("Master: OK (%s)", answer)
+        if self.cluster_master is None:
+          # only assign the first time, in the plain query
+          self.cluster_master = answer
+      elif reqtype == constants.CONFD_REQ_NODE_ROLE_BYNAME:
+        if answer == constants.CONFD_NODE_ROLE_MASTER:
+          Log("Node role for master: OK",)
+        else:
+          Err("Node role for master: wrong: %s" % answer)
+      elif reqtype == constants.CONFD_REQ_NODE_PIP_LIST:
+        Log("Node primary ip query: OK")
+        LogAtMost(answer, 5, indent=1)
+      elif reqtype == constants.CONFD_REQ_MC_PIP_LIST:
+        Log("Master candidates primary IP query: OK")
+        LogAtMost(answer, 5, indent=1)
+      elif reqtype == constants.CONFD_REQ_INSTANCES_IPS_LIST:
+        Log("Instance primary IP query: OK")
+        if not answer:
+          Log("no IPs received", indent=1)
+        else:
+          LogAtMost(answer, 5, indent=1)
+        self.instance_ips = answer
+      elif reqtype == constants.CONFD_REQ_NODE_PIP_BY_INSTANCE_IP:
+        Log("Instance IP to node IP query: OK")
+        if not answer:
+          Log("no mapping received", indent=1)
+        else:
+          LogAtMost(answer, 5, indent=1)
+      else:
+        Log("Unhandled reply %s, please fix the client", reqtype)
+        print answer
+
+  def DoConfdRequestReply(self, req):
+    self.confd_counting_callback.RegisterQuery(req.rsalt)
+    self.confd_client.SendRequest(req, async=False)
+    while not self.confd_counting_callback.AllAnswered():
+      if not self.confd_client.ReceiveReply():
+        Err("Did not receive all expected confd replies")
+        break
+
+  def TestConfd(self):
+    """Run confd queries for the cluster.
+
+    """
+    Log("Checking confd results")
+
+    filter_callback = confd_client.ConfdFilterCallback(self.ConfdCallback)
+    counting_callback = confd_client.ConfdCountingCallback(filter_callback)
+    self.confd_counting_callback = counting_callback
+
+    self.confd_client = confd_client.ConfdClient(self.hmac_key,
+                                                 self.mc_list,
+                                                 counting_callback)
+
+    tests = [
+      {"type": constants.CONFD_REQ_PING},
+      {"type": constants.CONFD_REQ_CLUSTER_MASTER},
+      {"type": constants.CONFD_REQ_CLUSTER_MASTER,
+       "query": {constants.CONFD_REQQ_FIELDS:
+                 [constants.CONFD_REQFIELD_NAME,
+                  constants.CONFD_REQFIELD_IP,
+                  constants.CONFD_REQFIELD_MNODE_PIP,
+                  ]}},
+      {"type": constants.CONFD_REQ_NODE_ROLE_BYNAME},
+      {"type": constants.CONFD_REQ_NODE_PIP_LIST},
+      {"type": constants.CONFD_REQ_MC_PIP_LIST},
+      {"type": constants.CONFD_REQ_INSTANCES_IPS_LIST,
+       "query": None},
+      {"type": constants.CONFD_REQ_NODE_PIP_BY_INSTANCE_IP},
+      ]
+
+    for kwargs in tests:
+      if kwargs["type"] == constants.CONFD_REQ_NODE_ROLE_BYNAME:
+        assert self.cluster_master is not None
+        kwargs["query"] = self.cluster_master
+      elif kwargs["type"] == constants.CONFD_REQ_NODE_PIP_BY_INSTANCE_IP:
+        kwargs["query"] = {constants.CONFD_REQQ_IPLIST: self.instance_ips}
+
+      # pylint: disable=W0142
+      # used ** magic
+      req = confd_client.ConfdClientRequest(**kwargs)
+      self.DoConfdRequestReply(req)
+
+  def TestTiming(self):
+    """Run timing tests.
+
+    """
+    # timing tests
+    if self.opts.requests <= 0:
+      return
+    Log("Timing tests")
+    self.is_timing = True
+    self.TimingOp("ping", {"type": constants.CONFD_REQ_PING})
+    self.TimingOp("instance ips",
+                  {"type": constants.CONFD_REQ_INSTANCES_IPS_LIST})
+
+  def TimingOp(self, name, kwargs):
+    """Run a single timing test.
+
+    """
+    start = time.time()
+    for _ in range(self.opts.requests):
+      # pylint: disable=W0142
+      req = confd_client.ConfdClientRequest(**kwargs)
+      self.DoConfdRequestReply(req)
+    stop = time.time()
+    per_req = 1000 * (stop - start) / self.opts.requests
+    Log("%.3fms per %s request", per_req, name, indent=1)
+
+  def Run(self):
+    """Run all the tests.
+
+    """
+    self.TestConfd()
+    self.TestTiming()
+
+
+def main():
+  """Main function.
+
+  """
+  return TestClient().Run()
+
+
+if __name__ == "__main__":
+  main()
diff --git a/tools/fmtjson b/tools/fmtjson
new file mode 100755 (executable)
index 0000000..0bfb114
--- /dev/null
@@ -0,0 +1,45 @@
+#!/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.
+
+"""Tool to format JSON data.
+
+"""
+
+import sys
+import simplejson
+
+
+def main():
+  """Main routine.
+
+  """
+  if len(sys.argv) > 1:
+    sys.stderr.write("Read JSON data from standard input and write a"
+                     " formatted version on standard output. There are"
+                     " no options or arguments.\n")
+    sys.exit(1)
+
+  data = simplejson.load(sys.stdin)
+  simplejson.dump(data, sys.stdout, indent=2, sort_keys=True)
+  sys.stdout.write("\n")
+
+
+if __name__ == "__main__":
+  main()
index 3d1affd..205eaeb 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 #
 
-# Copyright (C) 2006, 2007, 2011 Google Inc.
+# Copyright (C) 2006, 2007, 2011, 2012 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
@@ -208,7 +208,7 @@ def IsPartitioned(disk):
   Currently only md devices are used as is.
 
   """
-  return not (disk.startswith('md') or PART_RE.match(disk))
+  return not (disk.startswith("md") or PART_RE.match(disk))
 
 
 def DeviceName(disk):
@@ -219,9 +219,9 @@ def DeviceName(disk):
 
   """
   if IsPartitioned(disk):
-    device = '/dev/%s1' % disk
+    device = "/dev/%s1" % disk
   else:
-    device = '/dev/%s' % disk
+    device = "/dev/%s" % disk
   return device
 
 
@@ -268,7 +268,7 @@ def CheckPrereq():
     raise PrereqError("This tool runs as root only. Really.")
 
   osname, _, release, _, _ = os.uname()
-  if osname != 'Linux':
+  if osname != "Linux":
     raise PrereqError("This tool only runs on Linux"
                       " (detected OS: %s)." % osname)
 
@@ -336,7 +336,7 @@ def CheckSysDev(name, devnum):
 
   @param name: the device name, e.g. 'sda'
   @param devnum: the device number, e.g. 0x803 (2051 in decimal) for sda3
-  @raises L{SysconfigError}: in case of failure of the check
+  @raises SysconfigError: in case of failure of the check
 
   """
   path = "/dev/%s" % name
diff --git a/tools/master-ip-setup b/tools/master-ip-setup
new file mode 100755 (executable)
index 0000000..bad47d1
--- /dev/null
@@ -0,0 +1,89 @@
+#!/bin/bash
+#
+
+# 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.
+
+set -e -u
+
+USAGE_MSG="Usage: $0 {start|stop}"
+PATH=$PATH:/sbin:/usr/sbin:/usr/local/sbin
+
+# Start the master IP
+start() {
+  case $CLUSTER_IP_VERSION in
+    4)
+      ARP_COMMAND="arping -q -U -c 3 -I $MASTER_NETDEV -s $MASTER_IP $MASTER_IP"
+      ;;
+    6)
+      ARP_COMMAND="ndisc6 -q r 3 $MASTER_IP $MASTER_NETDEV"
+      ;;
+    *)
+      echo "Invalid cluster IP version specified: $CLUSTER_IP_VERSION" >&2
+      exit 1
+      ;;
+  esac
+
+  # Check if the master IP address is already configured on this machine
+  if fping -S 127.0.0.1 $MASTER_IP >/dev/null 2>&1; then
+    echo "Master IP address already configured on this machine. Doing nothing."
+    exit 0
+  fi
+
+  # Check if the master IP address is already configured on another machine
+  if fping $MASTER_IP >/dev/null 2>&1; then
+    echo "Error: master IP address configured on another machine." >&2
+    exit 1
+  fi
+
+  if ! ip addr add $MASTER_IP/$MASTER_NETMASK \
+     dev $MASTER_NETDEV label $MASTER_NETDEV:0; then
+    echo "Error during the activation of the master IP address" >&2
+    exit 1
+  fi
+
+  # Send gratuituous ARP to update neighbours' ARP cache
+  $ARP_COMMAND || :
+}
+
+# Stop the master IP
+stop() {
+  if ! ip addr del $MASTER_IP/$MASTER_NETMASK dev $MASTER_NETDEV; then
+    echo "Error during the deactivation of the master IP address" >&2
+    exit 1
+  fi
+}
+
+if (( $# < 1 )); then
+  echo $USAGE_MSG >&2
+  exit 1
+fi
+
+case "$1" in
+  start)
+    start
+    ;;
+  stop)
+    stop
+    ;;
+  *)
+    echo $USAGE_MSG >&2
+    exit 1
+    ;;
+esac
+
+exit 0
diff --git a/tools/ovfconverter b/tools/ovfconverter
new file mode 100755 (executable)
index 0000000..17024bf
--- /dev/null
@@ -0,0 +1,211 @@
+#!/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.
+
+
+"""Tool to translate between ovf and ganeti backup format.
+
+"""
+
+import logging
+import optparse
+import os
+
+from ganeti import cli
+from ganeti import constants
+from ganeti import errors
+from ganeti import ovf
+
+
+IMPORT_MODE = "import"
+EXPORT_MODE = "export"
+
+
+def CheckOptions(parser, options_dict, required, forbidden, excluding, mode):
+  """Performes check on the command line options.
+
+  Checks whether the required arguments are present and if none of the arguments
+  not supported for the current mode are given.
+
+  @type options_dict: list
+  @param options_dict: dictionary containing all the options from the command
+    line
+  @type required: list
+  @param required: list of pairs (option, argument) where 'option' is required
+    in mode 'mode'
+  @type forbidden: list
+  @param forbidden: list of pairs (option, argument) which are not allowed in
+    mode 'mode'
+  @type excluding: list
+  @param excluding: list of pairs (argument1, argument2); each pair contains
+    mutually exclusive arguments
+  @type mode: string
+  @param mode: current mode of the converter
+
+  """
+  for (option, argument) in required:
+    if not options_dict[option]:
+      parser.error("Argument %s is required for %s" % (argument, mode))
+  for (option, argument) in forbidden:
+    if options_dict[option]:
+      parser.error("Argument %s is not allowed in %s mode" % (argument, mode))
+  for (arg1, arg2) in excluding:
+    if options_dict[arg1] and options_dict[arg2]:
+      parser.error("Arguments %s and %s exclude each other" % (arg1, arg2))
+
+
+def ParseOptions():
+  """Parses the command line options and arguments.
+
+  In case of mismatching parameters, it will show the correct usage and exit.
+
+  @rtype: tuple
+  @return: (mode, sourcefile to read from, additional options)
+
+  """
+  usage = ("%%prog {%s|%s} <source-cfg-file> [options...]" %
+           (IMPORT_MODE, EXPORT_MODE))
+  parser = optparse.OptionParser(usage=usage)
+
+  #global options
+  parser.add_option(cli.DEBUG_OPT)
+  parser.add_option(cli.VERBOSE_OPT)
+  parser.add_option("-n", "--name", dest="name", action="store",
+                    help="Name of the instance")
+  parser.add_option("--output-dir", dest="output_dir",
+                    help="Path to the output directory")
+
+  #import options
+  import_group = optparse.OptionGroup(parser, "Import options")
+  import_group.add_option(cli.BACKEND_OPT)
+  import_group.add_option(cli.DISK_OPT)
+  import_group.add_option(cli.DISK_TEMPLATE_OPT)
+  import_group.add_option(cli.HYPERVISOR_OPT)
+  import_group.add_option(cli.NET_OPT)
+  import_group.add_option(cli.NONICS_OPT)
+  import_group.add_option(cli.OS_OPT)
+  import_group.add_option(cli.OSPARAMS_OPT)
+  import_group.add_option(cli.TAG_ADD_OPT)
+  parser.add_option_group(import_group)
+
+  #export options
+  export_group = optparse.OptionGroup(parser, "Export options")
+  export_group.add_option("--compress", dest="compression",
+                          action="store_true", default=False,
+                          help="The exported disk will be compressed to tar.gz")
+  export_group.add_option("--external", dest="ext_usage",
+                          action="store_true", default=False,
+                          help="The package will be used externally (ommits the"
+                               " Ganeti-specific parts of configuration)")
+  export_group.add_option("-f", "--format", dest="disk_format",
+                          action="store",
+                          choices=("raw", "cow", "vmdk"),
+                          help="Disk format for export (one of raw/cow/vmdk)")
+  export_group.add_option("--ova", dest="ova_package",
+                          action="store_true", default=False,
+                          help="Export everything into OVA package")
+  parser.add_option_group(export_group)
+
+  options, args = parser.parse_args()
+  if len(args) != 2:
+    parser.error("Wrong number of arguments")
+  mode = args.pop(0)
+  input_path = os.path.abspath(args.pop(0))
+
+  if mode == IMPORT_MODE:
+    required = []
+    forbidden = [
+      ("compression", "--compress"),
+      ("disk_format", "--format"),
+      ("ext_usage", "--external"),
+      ("ova_package", "--ova"),
+    ]
+    excluding = [("nics", "no_nics")]
+  elif mode == EXPORT_MODE:
+    required = [("disk_format", "--format")]
+    forbidden = [
+      ("beparams", "--backend-parameters"),
+      ("disk_template", "--disk-template"),
+      ("disks", "--disk"),
+      ("hypervisor", "--hypervisor-parameters"),
+      ("nics", "--net"),
+      ("no_nics", "--no-nics"),
+      ("os", "--os-type"),
+      ("osparams", "--os-parameters"),
+      ("tags", "--tags"),
+    ]
+    excluding = []
+  else:
+    parser.error("First argument should be either '%s' or '%s'" %
+                 (IMPORT_MODE, EXPORT_MODE))
+
+  options_dict = vars(options)
+  CheckOptions(parser, options_dict, required, forbidden, excluding, mode)
+
+  return (mode, input_path, options)
+
+
+def SetupLogging(options):
+  """Setting up logging infrastructure.
+
+  @type options: optparse.Values
+  @param options: parsed command line options
+
+  """
+  formatter = logging.Formatter("%(asctime)s: %(levelname)s %(message)s")
+
+  stderr_handler = logging.StreamHandler()
+  stderr_handler.setFormatter(formatter)
+  if options.debug:
+    stderr_handler.setLevel(logging.NOTSET)
+  elif options.verbose:
+    stderr_handler.setLevel(logging.INFO)
+  else:
+    stderr_handler.setLevel(logging.WARNING)
+
+  root_logger = logging.getLogger("")
+  root_logger.setLevel(logging.NOTSET)
+  root_logger.addHandler(stderr_handler)
+
+
+def main():
+  """Main routine.
+
+  """
+  (mode, input_path, options) = ParseOptions()
+  SetupLogging(options)
+  logging.info("Chosen %s mode, reading the %s file", mode, input_path)
+  assert mode in (IMPORT_MODE, EXPORT_MODE)
+  converter = None
+  try:
+    if mode == IMPORT_MODE:
+      converter = ovf.OVFImporter(input_path, options)
+    elif mode == EXPORT_MODE:
+      converter = ovf.OVFExporter(input_path, options)
+    converter.Parse()
+    converter.Save()
+  except errors.OpPrereqError, err:
+    if converter:
+      converter.Cleanup()
+    logging.exception(err)
+    return constants.EXIT_FAILURE
+
+
+if __name__ == "__main__":
+  main()
index 8c406ce..4e09078 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 #
 
-# Copyright (C) 2010 Google Inc.
+# Copyright (C) 2010, 2012 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
@@ -29,11 +29,17 @@ This is needed before we can join the node into the cluster.
 
 import getpass
 import logging
-import paramiko
 import os.path
 import optparse
 import sys
 
+# workaround paramiko warnings
+# FIXME: use 'with warnings.catch_warnings' once we drop Python 2.4
+import warnings
+warnings.simplefilter("ignore")
+import paramiko
+warnings.resetwarnings()
+
 from ganeti import cli
 from ganeti import constants
 from ganeti import errors
@@ -247,6 +253,10 @@ def ParseOptions():
 
   (options, args) = parser.parse_args()
 
+  if not args:
+    parser.print_help()
+    sys.exit(constants.EXIT_FAILURE)
+
   return (options, args)