Merge branch 'stable-2.9' into stable-2.10
authorKlaus Aehlig <aehlig@google.com>
Wed, 30 Oct 2013 13:07:52 +0000 (14:07 +0100)
committerKlaus Aehlig <aehlig@google.com>
Wed, 30 Oct 2013 13:30:19 +0000 (14:30 +0100)
* stable-2.9
  (no changes)

* stable-2.8
  Add all dependencies for confd as test dependencies
  Add snap-server to the test-relevenat packages
  Placate warnings on ganeti.outils_unittest.py

Signed-off-by: Klaus Aehlig <aehlig@google.com>
Reviewed-by: Thomas Thrainer <thomasth@google.com>

215 files changed:
.gitignore
Makefile.am
NEWS
README
autotools/build-bash-completion
autotools/convert-constants
autotools/run-in-tempdir
configure.ac
devel/build_chroot
doc/design-2.10.rst [new file with mode: 0644]
doc/design-ceph-ganeti-support.rst [new file with mode: 0644]
doc/design-cmdlib-unittests.rst [new file with mode: 0644]
doc/design-draft.rst
doc/design-hotplug.rst [new file with mode: 0644]
doc/design-hsqueeze.rst [new file with mode: 0644]
doc/design-hugepages-support.rst [new file with mode: 0644]
doc/design-monitoring-agent.rst
doc/design-openvswitch.rst
doc/design-optables.rst [new file with mode: 0644]
doc/design-storagetypes.rst
doc/design-upgrade.rst [new file with mode: 0644]
doc/devnotes.rst
doc/examples/ganeti.cron.in
doc/hooks.rst
doc/iallocator.rst
doc/index.rst
doc/install.rst
doc/move-instance.rst
doc/security.rst
doc/virtual-cluster.rst
lib/_constants.py.in [new file with mode: 0644]
lib/backend.py
lib/bootstrap.py
lib/build/sphinx_ext.py
lib/cli.py
lib/client/gnt_cluster.py
lib/client/gnt_debug.py
lib/client/gnt_instance.py
lib/client/gnt_node.py
lib/cmdlib/backup.py
lib/cmdlib/cluster.py
lib/cmdlib/common.py
lib/cmdlib/group.py
lib/cmdlib/instance.py
lib/cmdlib/instance_migration.py
lib/cmdlib/instance_operation.py
lib/cmdlib/instance_query.py
lib/cmdlib/instance_storage.py
lib/cmdlib/instance_utils.py
lib/cmdlib/node.py
lib/cmdlib/test.py
lib/config.py
lib/constants.py
lib/errors.py
lib/ht.py
lib/hypervisor/hv_base.py
lib/hypervisor/hv_kvm.py
lib/hypervisor/hv_xen.py
lib/jqueue.py
lib/locking.py
lib/luxi.py
lib/masterd/iallocator.py
lib/masterd/instance.py
lib/mcpu.py
lib/objects.py
lib/opcodes.py [deleted file]
lib/opcodes.py.in_after [new file with mode: 0644]
lib/opcodes.py.in_before [new file with mode: 0644]
lib/opcodes_base.py [new file with mode: 0644]
lib/pathutils.py
lib/rpc.py
lib/rpc_defs.py
lib/server/masterd.py
lib/server/noded.py
lib/ssconf.py
lib/storage/base.py
lib/storage/bdev.py
lib/storage/drbd.py
lib/storage/drbd_info.py
lib/utils/__init__.py
lib/utils/storage.py
lib/utils/text.py
lib/utils/version.py [new file with mode: 0644]
man/ganeti.rst
man/gnt-cluster.rst
man/gnt-instance.rst
man/gnt-node.rst
man/hail.rst
man/hbal.rst
man/htools.rst
pylintrc
pylintrc-test [new file with mode: 0644]
qa/qa-sample.json
qa/qa_cluster.py
qa/qa_instance.py
qa/qa_monitoring.py
qa/qa_node.py
src/AutoConf.hs.in [new file with mode: 0644]
src/Ganeti/BasicTypes.hs
src/Ganeti/Confd/Server.hs
src/Ganeti/Confd/Types.hs
src/Ganeti/ConstantUtils.hs [new file with mode: 0644]
src/Ganeti/Constants.hs [new file with mode: 0644]
src/Ganeti/Cpu/LoadParser.hs [new file with mode: 0644]
src/Ganeti/Cpu/Types.hs [new file with mode: 0644]
src/Ganeti/DataCollectors/CPUload.hs [new file with mode: 0644]
src/Ganeti/DataCollectors/InstStatusTypes.hs
src/Ganeti/DataCollectors/Types.hs
src/Ganeti/HTools/Backend/IAlloc.hs
src/Ganeti/HTools/CLI.hs
src/Ganeti/HTools/Cluster.hs
src/Ganeti/HTools/ExtLoader.hs
src/Ganeti/HTools/Loader.hs
src/Ganeti/HTools/Program/Hail.hs
src/Ganeti/HTools/Program/Harep.hs
src/Ganeti/HTools/Program/Hbal.hs
src/Ganeti/HTools/Program/Hinfo.hs
src/Ganeti/HTools/Types.hs
src/Ganeti/Hs2Py/GenConstants.hs [new file with mode: 0644]
src/Ganeti/Hs2Py/GenOpCodes.hs [new file with mode: 0644]
src/Ganeti/Hs2Py/ListConstants.hs.in [new file with mode: 0644]
src/Ganeti/Hs2Py/OpDoc.hs [new file with mode: 0644]
src/Ganeti/HsConstants.hs [new file with mode: 0644]
src/Ganeti/JSON.hs
src/Ganeti/Logging.hs
src/Ganeti/Luxi.hs
src/Ganeti/Monitoring/Server.hs
src/Ganeti/Objects.hs
src/Ganeti/OpCodes.hs
src/Ganeti/OpParams.hs
src/Ganeti/Parsers.hs [new file with mode: 0644]
src/Ganeti/Path.hs
src/Ganeti/PyConstants.hs.in [moved from src/Ganeti/Constants.hs.in with 88% similarity]
src/Ganeti/PyValueInstances.hs [new file with mode: 0644]
src/Ganeti/Query/Common.hs
src/Ganeti/Query/Server.hs
src/Ganeti/Rpc.hs
src/Ganeti/Runtime.hs
src/Ganeti/Storage/Diskstats/Parser.hs
src/Ganeti/THH.hs
src/Ganeti/Types.hs
src/Ganeti/Utils.hs
src/hs2py-constants.hs [new file with mode: 0644]
src/hs2py.hs [new file with mode: 0644]
test/data/htools/hbal-dyn.data [new file with mode: 0644]
test/data/kvm_runtime.json [new file with mode: 0644]
test/data/mond-data.txt [new file with mode: 0644]
test/hs/Test/AutoConf.hs [new file with mode: 0644]
test/hs/Test/Ganeti/Constants.hs [new file with mode: 0644]
test/hs/Test/Ganeti/HTools/Cluster.hs
test/hs/Test/Ganeti/HTools/ExtLoader.hs [new file with mode: 0644]
test/hs/Test/Ganeti/HTools/Types.hs
test/hs/Test/Ganeti/Hypervisor/Xen/XmParser.hs
test/hs/Test/Ganeti/JQueue.hs
test/hs/Test/Ganeti/Luxi.hs
test/hs/Test/Ganeti/OpCodes.hs
test/hs/Test/Ganeti/TestCommon.hs
test/hs/Test/Ganeti/Types.hs
test/hs/htest.hs
test/hs/shelltests/htools-balancing.test
test/py/__init__.py [new file with mode: 0644]
test/py/cfgupgrade_unittest.py
test/py/cmdlib/__init__.py [new file with mode: 0644]
test/py/cmdlib/backup_unittest.py [new file with mode: 0644]
test/py/cmdlib/cluster_unittest.py [new file with mode: 0644]
test/py/cmdlib/cmdlib_unittest.py [new file with mode: 0755]
test/py/cmdlib/group_unittest.py [new file with mode: 0644]
test/py/cmdlib/instance_migration_unittest.py [new file with mode: 0644]
test/py/cmdlib/instance_query_unittest.py [new file with mode: 0644]
test/py/cmdlib/instance_storage_unittest.py [moved from test/py/ganeti.cmdlib.instance_storage_unittest.py with 97% similarity]
test/py/cmdlib/instance_unittest.py [new file with mode: 0644]
test/py/cmdlib/node_unittest.py [new file with mode: 0644]
test/py/cmdlib/test_unittest.py [new file with mode: 0644]
test/py/cmdlib/testsupport/__init__.py [new file with mode: 0644]
test/py/cmdlib/testsupport/cmdlib_testcase.py [new file with mode: 0644]
test/py/cmdlib/testsupport/config_mock.py [new file with mode: 0644]
test/py/cmdlib/testsupport/iallocator_mock.py [new file with mode: 0644]
test/py/cmdlib/testsupport/lock_manager_mock.py [new file with mode: 0644]
test/py/cmdlib/testsupport/netutils_mock.py [new file with mode: 0644]
test/py/cmdlib/testsupport/processor_mock.py [new file with mode: 0644]
test/py/cmdlib/testsupport/rpc_runner_mock.py [new file with mode: 0644]
test/py/cmdlib/testsupport/ssh_mock.py [new file with mode: 0644]
test/py/cmdlib/testsupport/util.py [new file with mode: 0644]
test/py/cmdlib/testsupport/utils_mock.py [new file with mode: 0644]
test/py/daemon-util_unittest.bash
test/py/docs_unittest.py
test/py/ganeti.bootstrap_unittest.py
test/py/ganeti.client.gnt_cluster_unittest.py
test/py/ganeti.cmdlib.cluster_unittest.py [deleted file]
test/py/ganeti.cmdlib.common_unittest.py [deleted file]
test/py/ganeti.cmdlib_unittest.py [deleted file]
test/py/ganeti.config_unittest.py
test/py/ganeti.constants_unittest.py
test/py/ganeti.hypervisor.hv_kvm_unittest.py
test/py/ganeti.hypervisor.hv_xen_unittest.py
test/py/ganeti.masterd.iallocator_unittest.py
test/py/ganeti.objects_unittest.py
test/py/ganeti.opcodes_unittest.py
test/py/ganeti.rapi.client_unittest.py
test/py/ganeti.rapi.rlib2_unittest.py
test/py/ganeti.rapi.testutils_unittest.py
test/py/ganeti.rpc_unittest.py
test/py/ganeti.runtime_unittest.py
test/py/ganeti.storage.drbd_unittest.py
test/py/ganeti.utils.storage_unittest.py
test/py/ganeti.utils.text_unittest.py
test/py/ganeti.utils.version_unittest.py [new file with mode: 0755]
test/py/ganeti.utils_unittest.py
test/py/mocks.py
test/py/testutils.py
tools/cfgupgrade
tools/cfgupgrade12
tools/cluster-merge
tools/move-instance
tools/sanitize-config

index fdd50d7..05905f4 100644 (file)
 /doc/examples/hooks/ipsec
 
 # lib
-/lib/_autoconf.py
+/lib/_constants.py
 /lib/_vcsversion.py
 /lib/_generated_rpc.py
+/lib/opcodes.py
 
 # man
 /man/*.[0-9]
 /src/htools
 /src/hconfd
 /src/hluxid
+/src/hs2py
+/src/hs2py-constants
 /src/ganeti-confd
 /src/ganeti-luxid
 /src/ganeti-mond
 /src/rpc-test
 
 # automatically-built Haskell files
-/src/Ganeti/Constants.hs
+/src/AutoConf.hs
 /src/Ganeti/Curl/Internal.hs
+/src/Ganeti/Hs2Py/ListConstants.hs
+/src/Ganeti/PyConstants.hs
 /src/Ganeti/Version.hs
 /test/hs/Test/Ganeti/TestImports.hs
index 908dada..a41c439 100644 (file)
@@ -40,8 +40,45 @@ CONVERT_CONSTANTS = $(top_srcdir)/autotools/convert-constants
 BUILD_RPC = $(top_srcdir)/autotools/build-rpc
 SHELL_ENV_INIT = autotools/shell-env-init
 
+# starting as of Ganeti 2.10, all files are stored in two directories,
+# with only symbolic links added at other places.
+#
+# $(versiondir) contains most of Ganeti and all architecture-dependent files
+# $(versionedsharedir) contains only architecture-independent files; all python
+# executables need to go directly to $(versionedsharedir), as all ganeti python
+# mdules are installed outside the usual python path, i.e., as private modules.
+#
+# $(defaultversiondir) and $(defaultversionedsharedir) are the corresponding
+# directories for "the currently running" version of Ganeti. We never install
+# there, but all symbolic links go there, rather than directory to $(versiondir)
+# or $(versionedsharedir). Note that all links to $(default*dir) need to be stable;
+# so, if some currently architecture-independent executable is replaced by an
+# architecture-dependent one (and hence has to go under $(versiondir)), add a link
+# under $(versionedsharedir) but do not change the external links.
+if USE_VERSION_FULL
+DIRVERSION=$(VERSION_FULL)
+else
+DIRVERSION=$(VERSION_MAJOR).$(VERSION_MINOR)
+endif
+versiondir = $(libdir)/ganeti/$(DIRVERSION)
+defaultversiondir = $(libdir)/ganeti/default
+versionedsharedir = $(prefix)/share/ganeti/$(DIRVERSION)
+defaultversionedsharedir = $(prefix)/share/ganeti/default
+
+
 # Note: these are automake-specific variables, and must be named after
 # the directory + 'dir' suffix
+pkglibdir = $(versiondir)$(libdir)/ganeti
+myexeclibdir = $(pkglibdir)
+bindir = $(versiondir)$(exec_prefix)/bin
+sbindir = $(versiondir)$(exec_prefix)/sbin
+mandir = $(versionedsharedir)$(datarootdir)/man
+pkgpythondir = $(versionedsharedir)/ganeti
+gntpythondir = $(versionedsharedir)
+pkgpython_bindir = $(versionedsharedir)
+gnt_python_sbindir = $(versionedsharedir)
+tools_pythondir = $(versionedsharedir)
+
 clientdir = $(pkgpythondir)/client
 cmdlibdir = $(pkgpythondir)/cmdlib
 hypervisordir = $(pkgpythondir)/hypervisor
@@ -57,8 +94,18 @@ utilsdir = $(pkgpythondir)/utils
 toolsdir = $(pkglibdir)/tools
 iallocatorsdir = $(pkglibdir)/iallocators
 pytoolsdir = $(pkgpythondir)/tools
-docdir = $(datadir)/doc/$(PACKAGE)
-myexeclibdir = $(pkglibdir)
+docdir = $(versiondir)$(datadir)/doc/$(PACKAGE)
+
+SYMLINK_TARGET_DIRS = \
+       $(sysconfdir)/ganeti \
+       $(libdir)/ganeti/iallocators \
+       $(libdir)/ganeti/tools \
+       $(prefix)/share/ganeti \
+       $(exec_prefix)/bin \
+       $(exec_prefix)/sbin \
+       $(datarootdir)/man/man1 \
+       $(datarootdir)/man/man7 \
+       $(datarootdir)/man/man8
 
 # Delete output file if an error occurred while building it
 .DELETE_ON_ERROR:
@@ -68,7 +115,9 @@ HS_DIRS = \
        src/Ganeti \
        src/Ganeti/Confd \
        src/Ganeti/Curl \
+       src/Ganeti/Cpu \
        src/Ganeti/DataCollectors \
+       src/Ganeti/Hs2Py \
        src/Ganeti/HTools \
        src/Ganeti/HTools/Backend \
        src/Ganeti/HTools/Program \
@@ -136,6 +185,8 @@ DIRS = \
        test/data/ovfdata \
        test/data/ovfdata/other \
        test/py \
+       test/py/cmdlib \
+       test/py/cmdlib/testsupport \
        tools
 
 ALL_APIDOC_HS_DIRS = \
@@ -187,6 +238,7 @@ CLEANFILES = \
        $(addsuffix /*.hi,$(HS_DIRS)) \
        $(addsuffix /*.o,$(HS_DIRS)) \
        $(PYTHON_BOOTSTRAP) \
+       $(gnt_python_sbin_SCRIPTS) \
        epydoc.conf \
        $(REPLACE_VARS_SED) \
        $(SHELL_ENV_INIT) \
@@ -211,18 +263,21 @@ CLEANFILES = \
        stamp-directories \
        stamp-srclinks \
        $(nodist_pkgpython_PYTHON) \
+       $(gnt_scripts) \
        $(HS_ALL_PROGS) $(HS_BUILT_SRCS) \
        $(HS_BUILT_TEST_HELPERS) \
        src/ganeti-confd \
        src/ganeti-luxid \
        src/ganeti-mond \
-       .hpc/*.mix src/*.tix test/hs/*.tix \
+       src/hs2py-constants \
+       .hpc/*.mix src/*.tix test/hs/*.tix *.tix \
        doc/hs-lint.html
 
 GENERATED_FILES = \
        $(built_base_sources) \
        $(BUILT_PYTHON_SOURCES) \
-       $(PYTHON_BOOTSTRAP)
+       $(PYTHON_BOOTSTRAP) \
+       $(gnt_python_sbin_SCRIPTS)
 
 HS_GENERATED_FILES =
 if WANT_HTOOLS
@@ -241,8 +296,9 @@ built_base_sources = \
        stamp-srclinks
 
 built_python_base_sources = \
-       lib/_autoconf.py \
-       lib/_vcsversion.py
+       lib/_constants.py \
+       lib/_vcsversion.py \
+       lib/opcodes.py
 
 BUILT_PYTHON_SOURCES = \
        $(built_python_base_sources) \
@@ -266,6 +322,12 @@ BUILT_EXAMPLES = \
 nodist_pkgpython_PYTHON = \
        $(BUILT_PYTHON_SOURCES)
 
+nodist_pkgpython_bin_SCRIPTS = \
+       $(nodist_pkglib_python_scripts)
+
+pkgpython_bin_SCRIPTS = \
+       $(pkglib_python_scripts)
+
 noinst_PYTHON = \
        lib/build/__init__.py \
        lib/build/shell_example_lexer.py \
@@ -291,7 +353,7 @@ pkgpython_PYTHON = \
        lib/mcpu.py \
        lib/netutils.py \
        lib/objects.py \
-       lib/opcodes.py \
+       lib/opcodes_base.py \
        lib/outils.py \
        lib/ovf.py \
        lib/pathutils.py \
@@ -421,6 +483,7 @@ utils_PYTHON = \
        lib/utils/retry.py \
        lib/utils/storage.py \
        lib/utils/text.py \
+       lib/utils/version.py \
        lib/utils/wrapper.py \
        lib/utils/x509.py
 
@@ -439,12 +502,14 @@ docinput = \
        doc/design-2.7.rst \
        doc/design-2.8.rst \
        doc/design-2.9.rst \
+       doc/design-2.10.rst \
        doc/design-autorepair.rst \
        doc/design-bulk-create.rst \
        doc/design-chained-jobs.rst \
        doc/design-cpu-pinning.rst \
        doc/design-device-uuid-name.rst \
        doc/design-draft.rst \
+       doc/design-hotplug.rst \
        doc/design-glusterfs-ganeti-support.rst \
        doc/design-daemons.rst \
        doc/design-htools-2.3.rst \
@@ -460,6 +525,7 @@ docinput = \
        doc/design-openvswitch.rst \
        doc/design-ovf-support.rst \
        doc/design-opportunistic-locking.rst \
+       doc/design-optables.rst \
        doc/design-partitioned.rst \
        doc/design-query-splitting.rst \
        doc/design-query2.rst \
@@ -472,6 +538,8 @@ docinput = \
        doc/design-x509-ca.rst \
        doc/design-hroller.rst \
        doc/design-storagetypes.rst \
+        doc/design-upgrade.rst \
+       doc/design-hsqueeze.rst \
        doc/devnotes.rst \
        doc/glossary.rst \
        doc/hooks.rst \
@@ -509,6 +577,7 @@ HS_COMPILE_PROGS= \
        src/ganeti-mond \
        src/hconfd \
        src/hluxid \
+       src/hs2py \
        src/rpc-test
 
 # All Haskell non-test programs to be compiled but not automatically installed
@@ -517,14 +586,20 @@ HS_PROGS = $(HS_BIN_PROGS) $(HS_MYEXECLIB_PROGS)
 HS_BIN_ROLES = harep hbal hscan hspace hinfo hcheck hroller
 HS_HTOOLS_PROGS = $(HS_BIN_ROLES) hail
 
-HS_ALL_PROGS = \
-       $(HS_PROGS) \
+# Haskell programs that cannot be disabled at configure (e.g., unlike
+# 'mon-collector')
+HS_DEFAULT_PROGS = \
+       $(HS_BIN_PROGS) \
        test/hs/hpc-htools \
        test/hs/hpc-mon-collector \
        test/hs/htest \
        $(HS_COMPILE_PROGS)
 
-HS_PROG_SRCS = $(patsubst %,%.hs,$(HS_ALL_PROGS))
+HS_ALL_PROGS = $(HS_DEFAULT_PROGS) $(HS_MYEXECLIB_PROGS)
+
+HS_PROG_SRCS = $(patsubst %,%.hs,$(HS_DEFAULT_PROGS)) \
+              src/mon-collector.hs \
+              src/hs2py-constants.hs
 HS_BUILT_TEST_HELPERS = $(HS_BIN_ROLES:%=test/hs/%) test/hs/hail
 
 HFLAGS = \
@@ -561,9 +636,14 @@ HS_LIB_SRCS = \
        src/Ganeti/Confd/Utils.hs \
        src/Ganeti/Config.hs \
        src/Ganeti/ConfigReader.hs \
+       src/Ganeti/Constants.hs \
+       src/Ganeti/ConstantUtils.hs \
+       src/Ganeti/Cpu/LoadParser.hs \
+       src/Ganeti/Cpu/Types.hs \
        src/Ganeti/Curl/Multi.hs \
        src/Ganeti/Daemon.hs \
        src/Ganeti/DataCollectors/CLI.hs \
+       src/Ganeti/DataCollectors/CPUload.hs \
        src/Ganeti/DataCollectors/Diskstats.hs \
        src/Ganeti/DataCollectors/Drbd.hs \
        src/Ganeti/DataCollectors/InstStatus.hs \
@@ -602,6 +682,10 @@ HS_LIB_SRCS = \
        src/Ganeti/Hypervisor/Xen/XmParser.hs \
        src/Ganeti/Hypervisor/Xen/Types.hs \
        src/Ganeti/Hash.hs \
+       src/Ganeti/Hs2Py/GenConstants.hs \
+       src/Ganeti/Hs2Py/GenOpCodes.hs \
+       src/Ganeti/Hs2Py/OpDoc.hs \
+       src/Ganeti/HsConstants.hs \
        src/Ganeti/JQueue.hs \
        src/Ganeti/JSON.hs \
        src/Ganeti/Jobs.hs \
@@ -613,6 +697,8 @@ HS_LIB_SRCS = \
        src/Ganeti/OpCodes.hs \
        src/Ganeti/OpParams.hs \
        src/Ganeti/Path.hs \
+       src/Ganeti/Parsers.hs \
+       src/Ganeti/PyValueInstances.hs \
        src/Ganeti/Query/Cluster.hs \
        src/Ganeti/Query/Common.hs \
        src/Ganeti/Query/Export.hs \
@@ -640,11 +726,13 @@ HS_LIB_SRCS = \
        src/Ganeti/Utils.hs
 
 HS_TEST_SRCS = \
+       test/hs/Test/AutoConf.hs \
        test/hs/Test/Ganeti/Attoparsec.hs \
        test/hs/Test/Ganeti/BasicTypes.hs \
        test/hs/Test/Ganeti/Common.hs \
        test/hs/Test/Ganeti/Confd/Types.hs \
        test/hs/Test/Ganeti/Confd/Utils.hs \
+       test/hs/Test/Ganeti/Constants.hs \
        test/hs/Test/Ganeti/Daemon.hs \
        test/hs/Test/Ganeti/Errors.hs \
        test/hs/Test/Ganeti/HTools/Backend/Simu.hs \
@@ -652,6 +740,7 @@ HS_TEST_SRCS = \
        test/hs/Test/Ganeti/HTools/CLI.hs \
        test/hs/Test/Ganeti/HTools/Cluster.hs \
        test/hs/Test/Ganeti/HTools/Container.hs \
+       test/hs/Test/Ganeti/HTools/ExtLoader.hs \
        test/hs/Test/Ganeti/HTools/Graph.hs \
        test/hs/Test/Ganeti/HTools/Instance.hs \
        test/hs/Test/Ganeti/HTools/Loader.hs \
@@ -688,12 +777,17 @@ HS_LIBTEST_SRCS = $(HS_LIB_SRCS) $(HS_TEST_SRCS)
 
 HS_BUILT_SRCS = \
        test/hs/Test/Ganeti/TestImports.hs \
-       src/Ganeti/Constants.hs \
+       src/AutoConf.hs \
+       src/Ganeti/Hs2Py/ListConstants.hs \
+       src/Ganeti/PyConstants.hs \
        src/Ganeti/Curl/Internal.hs \
        src/Ganeti/Version.hs
 HS_BUILT_SRCS_IN = \
        $(patsubst %,%.in,$(filter-out src/Ganeti/Curl/Internal.hs,$(HS_BUILT_SRCS))) \
-       src/Ganeti/Curl/Internal.hsc
+       src/Ganeti/Curl/Internal.hsc \
+       lib/_constants.py.in \
+       lib/opcodes.py.in_after \
+       lib/opcodes.py.in_before
 
 HS_LIBTESTBUILT_SRCS = $(HS_LIBTEST_SRCS) $(HS_BUILT_SRCS)
 
@@ -704,15 +798,15 @@ doc/man-html/index.html: ENABLE_MANPAGES = 1
 doc/man-html/index.html: doc/manpages-enabled.rst $(mandocrst)
 
 # Note: we use here an order-only prerequisite, as the contents of
-# _autoconf.py are not actually influencing the html build output: it
+# _constants.py are not actually influencing the html build output: it
 # has to exist in order for the sphinx module to be loaded
 # successfully, but we certainly don't want the docs to be rebuilt if
 # it changes
 doc/html/index.html doc/man-html/index.html: $(docinput) doc/conf.py \
        configure.ac $(RUN_IN_TEMPDIR) lib/build/sphinx_ext.py \
-       lib/build/shell_example_lexer.py lib/opcodes.py lib/ht.py \
+       lib/build/shell_example_lexer.py lib/ht.py \
        doc/css/style.css lib/rapi/connector.py lib/rapi/rlib2.py \
-       | $(BUILT_PYTHON_SOURCES)
+       autotools/sphinx-wrapper | $(BUILT_PYTHON_SOURCES)
        @test -n "$(SPHINX)" || \
            { echo 'sphinx-build' not found during configure; exit 1; }
 if !MANPAGES_IN_DOC
@@ -821,15 +915,21 @@ gnt_scripts = \
        scripts/gnt-os \
        scripts/gnt-storage
 
+gnt_scripts_basenames = \
+       $(patsubst scripts/%,%,$(patsubst daemons/%,%,$(gnt_scripts) $(gnt_python_sbin_SCRIPTS)))
+
+gnt_python_sbin_SCRIPTS = \
+       $(PYTHON_BOOTSTRAP_SBIN)
+
+gntpython_SCRIPTS = $(gnt_scripts)
+
 PYTHON_BOOTSTRAP_SBIN = \
        daemons/ganeti-masterd \
        daemons/ganeti-noded \
        daemons/ganeti-rapi \
-       daemons/ganeti-watcher \
-       $(gnt_scripts)
+       daemons/ganeti-watcher
 
 PYTHON_BOOTSTRAP = \
-       $(PYTHON_BOOTSTRAP_SBIN) \
        tools/burnin \
        tools/ensure-dirs \
        tools/node-cleanup \
@@ -868,6 +968,23 @@ install-exec-hook:
        done
 endif
 
+# This target cannot be merged with the '$(HS_ALL_PROGS)' target
+# because 'hs2py-constants' cannot depend on 'Ganeti.Constants'.  And
+# the reason for this is because 'hs2py-constants' needs to generate
+# Python code, and 'Ganeti.Constants' is generated by Python.
+src/hs2py-constants: src/hs2py-constants.hs src/AutoConf.hs \
+                    src/Ganeti/BasicTypes.hs src/Ganeti/ConstantUtils.hs \
+                    src/Ganeti/JSON.hs src/Ganeti/THH.hs \
+                    src/Ganeti/Hs2Py/GenConstants.hs \
+                    src/Ganeti/Hs2Py/ListConstants.hs \
+                    src/Ganeti/HsConstants.hs \
+                    src/Ganeti/PyValueInstances.hs \
+                  | stamp-srclinks
+       $(GHC) --make \
+         $(HFLAGS) \
+         -osuf $(notdir $@).o -hisuf $(notdir $@).hi \
+         $(HEXTRA) $(HEXTRA_INT) src/hs2py-constants.hs
+
 $(HS_ALL_PROGS): %: %.hs $(HS_LIBTESTBUILT_SRCS) Makefile
        @if [ "$(notdir $@)" = "test" ] && [ "$(HS_NODEV)" ]; then \
          echo "Error: cannot run unittests without the development" \
@@ -920,9 +1037,13 @@ dist_sbin_SCRIPTS = \
        tools/ganeti-listrunner
 
 nodist_sbin_SCRIPTS = \
-       $(PYTHON_BOOTSTRAP_SBIN) \
        daemons/ganeti-cleaner
 
+# strip path prefixes off the sbin scripts
+all_sbin_scripts = \
+       $(patsubst tools/%,%,$(patsubst daemons/%,%,$(patsubst scripts/%,%,\
+       $(patsubst src/%,%,$(dist_sbin_SCRIPTS) $(nodist_sbin_SCRIPTS)))))
+
 if ENABLE_CONFD
 src/ganeti-confd: src/hconfd
        cp -f $< $@
@@ -951,20 +1072,26 @@ python_scripts = \
        tools/sanitize-config
 
 dist_tools_SCRIPTS = \
-       $(python_scripts) \
-       tools/burnin \
        tools/kvm-console-wrapper \
        tools/master-ip-setup \
        tools/xen-console-wrapper
 
-nodist_tools_python_scripts = \
+dist_tools_python_SCRIPTS = \
+       $(python_scripts) \
+       tools/burnin
+
+nodist_tools_python_SCRIPTS = \
        tools/node-cleanup
 
+tools_python_basenames = $(patsubst tools/%,%,\
+       $(dist_tools_python_SCRIPTS) $(nodist_tools_python_SCRIPTS))
+
 nodist_tools_SCRIPTS = \
-       $(nodist_tools_python_scripts) \
        tools/users-setup \
        tools/vcluster-setup
 
+tools_basenames = $(patsubst tools/%,%,$(nodist_tools_SCRIPTS) $(dist_tools_SCRIPTS))
+
 pkglib_python_scripts = \
        daemons/import-export \
        tools/check-cert-expired
@@ -974,22 +1101,27 @@ nodist_pkglib_python_scripts = \
        tools/node-daemon-setup \
        tools/prepare-node-join
 
+pkglib_python_basenames = \
+       $(patsubst daemons/%,%,$(patsubst tools/%,%,\
+       $(pkglib_python_scripts) $(nodist_pkglib_python_scripts)))
+
 myexeclib_SCRIPTS = \
        daemons/daemon-util \
        tools/kvm-ifup \
        tools/vif-ganeti \
        tools/net-common \
-       $(pkglib_python_scripts) \
        $(HS_MYEXECLIB_PROGS)
 
-nodist_myexeclib_SCRIPTS = \
-       $(nodist_pkglib_python_scripts)
+# compute the basenames of the myexeclib_scripts
+myexeclib_scripts_basenames = \
+       $(patsubst tools/%,%,$(patsubst daemons/%,%,$(patsubst src/%,%,$(myexeclib_SCRIPTS))))
 
 EXTRA_DIST = \
        NEWS \
        UPGRADE \
        epydoc.conf.in \
        pylintrc \
+       pylintrc-test \
        autotools/build-bash-completion \
        autotools/build-rpc \
        autotools/check-header \
@@ -1029,11 +1161,9 @@ EXTRA_DIST = \
        doc/users/groupmemberships.in \
        doc/users/groups.in \
        doc/users/users.in \
-       test/py/lockperf.py \
-       test/py/testutils.py \
-       test/py/mocks.py \
        $(dist_TESTS) \
        $(TEST_FILES) \
+       $(python_test_support) \
        man/footer.rst \
        $(manrst) \
        $(maninput) \
@@ -1091,6 +1221,10 @@ maninput = \
        $(patsubst %.html,%.html.in,$(manhtml)) \
        $(mangen)
 
+manfullpath = $(patsubst man/%.1,man1/%.1,\
+       $(patsubst man/%.7,man7/%.7,\
+       $(patsubst man/%.8,man8/%.8,$(man_MANS))))
+
 TEST_FILES = \
        test/autotools/autotools-check-news.test \
        test/data/htools/clean-nonzero-score.data \
@@ -1106,6 +1240,7 @@ TEST_FILES = \
        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-dyn.data \
        test/data/htools/hbal-excl-tags.data \
        test/data/htools/hbal-split-insts.data \
        test/data/htools/hspace-tiered-dualspec-exclusive.data \
@@ -1183,6 +1318,7 @@ TEST_FILES = \
        test/data/kvm_0.9.1_help_boot_test.txt \
        test/data/kvm_1.0_help.txt \
        test/data/kvm_1.1.2_help.txt \
+       test/data/kvm_runtime.json \
        test/data/lvs_lv.txt \
        test/data/NEWS_OK.txt \
        test/data/NEWS_previous_unreleased.txt \
@@ -1242,6 +1378,16 @@ TEST_FILES = \
 
 python_tests = \
        doc/examples/rapi_testutils.py \
+       test/py/cmdlib/backup_unittest.py \
+       test/py/cmdlib/cluster_unittest.py \
+       test/py/cmdlib/cmdlib_unittest.py \
+       test/py/cmdlib/group_unittest.py \
+       test/py/cmdlib/instance_unittest.py \
+       test/py/cmdlib/instance_migration_unittest.py \
+       test/py/cmdlib/instance_query_unittest.py \
+       test/py/cmdlib/instance_storage_unittest.py \
+       test/py/cmdlib/node_unittest.py \
+       test/py/cmdlib/test_unittest.py \
        test/py/cfgupgrade_unittest.py \
        test/py/docs_unittest.py \
        test/py/ganeti.asyncnotifier_unittest.py \
@@ -1252,9 +1398,6 @@ python_tests = \
        test/py/ganeti.client.gnt_cluster_unittest.py \
        test/py/ganeti.client.gnt_instance_unittest.py \
        test/py/ganeti.client.gnt_job_unittest.py \
-       test/py/ganeti.cmdlib_unittest.py \
-       test/py/ganeti.cmdlib.cluster_unittest.py \
-       test/py/ganeti.cmdlib.instance_storage_unittest.py \
        test/py/ganeti.compat_unittest.py \
        test/py/ganeti.confd.client_unittest.py \
        test/py/ganeti.config_unittest.py \
@@ -1318,6 +1461,7 @@ python_tests = \
        test/py/ganeti.utils.retry_unittest.py \
        test/py/ganeti.utils.storage_unittest.py \
        test/py/ganeti.utils.text_unittest.py \
+       test/py/ganeti.utils.version_unittest.py \
        test/py/ganeti.utils.wrapper_unittest.py \
        test/py/ganeti.utils.x509_unittest.py \
        test/py/ganeti.utils_unittest.py \
@@ -1327,6 +1471,24 @@ python_tests = \
        test/py/qa.qa_config_unittest.py \
        test/py/tempfile_fork_unittest.py
 
+python_test_support = \
+       test/py/__init__.py \
+       test/py/lockperf.py \
+       test/py/testutils.py \
+       test/py/mocks.py \
+       test/py/cmdlib/__init__.py \
+       test/py/cmdlib/testsupport/__init__.py \
+       test/py/cmdlib/testsupport/cmdlib_testcase.py \
+       test/py/cmdlib/testsupport/config_mock.py \
+       test/py/cmdlib/testsupport/iallocator_mock.py \
+       test/py/cmdlib/testsupport/lock_manager_mock.py \
+       test/py/cmdlib/testsupport/netutils_mock.py \
+       test/py/cmdlib/testsupport/processor_mock.py \
+       test/py/cmdlib/testsupport/rpc_runner_mock.py \
+       test/py/cmdlib/testsupport/ssh_mock.py \
+       test/py/cmdlib/testsupport/utils_mock.py \
+       test/py/cmdlib/testsupport/util.py
+
 haskell_tests = test/hs/htest
 
 dist_TESTS = \
@@ -1357,7 +1519,7 @@ TESTS = $(dist_TESTS) $(nodist_TESTS)
 
 # Environment for all tests
 PLAIN_TESTS_ENVIRONMENT = \
-       PYTHONPATH=. \
+       PYTHONPATH=.:./test/py \
        TOP_SRCDIR=$(abs_top_srcdir) TOP_BUILDDIR=$(abs_top_builddir) \
        PYTHON=$(PYTHON) FAKEROOT=$(FAKEROOT_PATH) \
        $(RUN_IN_TEMPDIR)
@@ -1391,6 +1553,7 @@ all_python_code = \
 
 if PY_UNIT
 all_python_code += $(python_tests)
+all_python_code += $(python_test_support)
 endif
 
 srclink_files = \
@@ -1424,6 +1587,7 @@ lint_python_code = \
        $(CHECK_IMPORTS) \
        $(CHECK_HEADER) \
        $(DOCPP) \
+       $(gnt_python_sbin_SCRIPTS) \
        $(PYTHON_BOOTSTRAP)
 
 standalone_python_modules = \
@@ -1440,7 +1604,9 @@ pep8_python_code = \
        $(CHECK_HEADER) \
        $(DOCPP) \
        $(PYTHON_BOOTSTRAP) \
-       qa
+       $(gnt_python_sbin_SCRIPTS) \
+       qa \
+       $(python_test_support)
 
 test/py/daemon-util_unittest.bash: daemons/daemon-util
 
@@ -1571,8 +1737,25 @@ src/Ganeti/Version.hs: src/Ganeti/Version.hs.in \
        VCSVER=`cat $(abs_top_srcdir)/vcs-version`; \
        sed -e "s/%ver%/$$VCSVER/" < $< > $@
 
-src/Ganeti/Constants.hs: src/Ganeti/Constants.hs.in \
-       lib/constants.py lib/_autoconf.py lib/luxi.py lib/errors.py \
+src/Ganeti/Hs2Py/ListConstants.hs: src/Ganeti/Hs2Py/ListConstants.hs.in \
+                                  src/Ganeti/HsConstants.hs \
+                                | stamp-directories
+       @echo Generating $@
+       @set -e; \
+## Extract constant names from 'HsConstants.hs' by extracting the left
+## side of all lines containing an equal sign (i.e., '=') and
+## prepending the apostrophe sign (i.e., "'").
+##
+## For example, the constant
+##   adminstDown = ...
+## becomes
+##   'adminstDown
+       NAMES=$$(sed -e "/^--/ d" $(abs_top_srcdir)/src/Ganeti/HsConstants.hs |\
+                sed -n -e "/=/ s/\(.*\) =.*/    '\1:/g p"); \
+       m4 -DPY_CONSTANT_NAMES="$$NAMES" $(abs_top_srcdir)/$< > $@
+
+src/Ganeti/PyConstants.hs: src/Ganeti/PyConstants.hs.in \
+       lib/constants.py lib/luxi.py lib/errors.py \
        lib/jstore.py $(RUN_IN_TEMPDIR) \
        $(CONVERT_CONSTANTS) $(built_base_sources) \
        | lib/_vcsversion.py
@@ -1594,86 +1777,79 @@ test/hs/Test/Ganeti/TestImports.hs: test/hs/Test/Ganeti/TestImports.hs.in \
          done ; \
        } > $@
 
-lib/_autoconf.py: Makefile | stamp-directories
-       set -e; \
-       { echo '# This file is automatically generated, do not edit!'; \
-         echo '#'; \
-         echo ''; \
-         echo '"""Build-time configuration for Ganeti.'; \
-         echo '';\
-         echo 'This file is autogenerated by the build process.'; \
-         echo 'For any changes you need to re-run ./configure (and'; \
-         echo 'not edit by hand).'; \
-         echo ''; \
-         echo '"""'; \
-         echo ''; \
-         echo '# pylint: disable=C0301,C0324'; \
-         echo '# because this is autogenerated, we do not want'; \
-         echo '# style warnings' ; \
-         echo ''; \
-         echo "PACKAGE_VERSION = '$(PACKAGE_VERSION)'"; \
-         echo "VERSION_MAJOR = '$(VERSION_MAJOR)'"; \
-         echo "VERSION_MINOR = '$(VERSION_MINOR)'"; \
-         echo "VERSION_REVISION = '$(VERSION_REVISION)'"; \
-         echo "VERSION_SUFFIX = '$(VERSION_SUFFIX)'"; \
-         echo "VERSION_FULL = '$(VERSION_FULL)'"; \
-         echo "LOCALSTATEDIR = '$(localstatedir)'"; \
-         echo "SYSCONFDIR = '$(sysconfdir)'"; \
-         echo "SSH_CONFIG_DIR = '$(SSH_CONFIG_DIR)'"; \
-         echo "SSH_LOGIN_USER = '$(SSH_LOGIN_USER)'"; \
-         echo "SSH_CONSOLE_USER = '$(SSH_CONSOLE_USER)'"; \
-         echo "EXPORT_DIR = '$(EXPORT_DIR)'"; \
-         echo "OS_SEARCH_PATH = [$(OS_SEARCH_PATH)]"; \
-         echo "ES_SEARCH_PATH = [$(ES_SEARCH_PATH)]"; \
-         echo "XEN_BOOTLOADER = '$(XEN_BOOTLOADER)'"; \
-         echo "XEN_CONFIG_DIR = '$(XEN_CONFIG_DIR)'"; \
-         echo "XEN_KERNEL = '$(XEN_KERNEL)'"; \
-         echo "XEN_INITRD = '$(XEN_INITRD)'"; \
-         echo "KVM_KERNEL = '$(KVM_KERNEL)'"; \
-         echo "IALLOCATOR_SEARCH_PATH = [$(IALLOCATOR_SEARCH_PATH)]"; \
-         echo "KVM_PATH = '$(KVM_PATH)'"; \
-         echo "IP_PATH = '$(IP_PATH)'"; \
-         echo "SOCAT_PATH = '$(SOCAT)'"; \
-         echo "SOCAT_USE_ESCAPE = $(SOCAT_USE_ESCAPE)"; \
-         echo "SOCAT_USE_COMPRESS = $(SOCAT_USE_COMPRESS)"; \
-         echo "LVM_STRIPECOUNT = $(LVM_STRIPECOUNT)"; \
-         echo "TOOLSDIR = '$(toolsdir)'"; \
-         echo "GNT_SCRIPTS = [$(foreach i,$(notdir $(gnt_scripts)),'$(i)',)]"; \
-         echo "HTOOLS_PROGS = [$(foreach i,$(HS_HTOOLS_PROGS),'$(i)',)]"; \
-         echo "PKGLIBDIR = '$(pkglibdir)'"; \
-         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)'"; \
-         echo "MASTERD_USER = '$(MASTERD_USER)'"; \
-         echo "MASTERD_GROUP = '$(MASTERD_GROUP)'"; \
-         echo "RAPI_USER = '$(RAPI_USER)'"; \
-         echo "RAPI_GROUP = '$(RAPI_GROUP)'"; \
-         echo "CONFD_USER = '$(CONFD_USER)'"; \
-         echo "CONFD_GROUP = '$(CONFD_GROUP)'"; \
-         echo "LUXID_USER = '$(LUXID_USER)'"; \
-         echo "LUXID_GROUP = '$(LUXID_GROUP)'"; \
-         echo "NODED_USER = '$(NODED_USER)'"; \
-         echo "NODED_GROUP = '$(NODED_GROUP)'"; \
-         echo "MOND_USER = '$(MOND_USER)'"; \
-         echo "MOND_GROUP = '$(MOND_GROUP)'"; \
-         echo "DISK_SEPARATOR = '$(DISK_SEPARATOR)'"; \
-         echo "QEMUIMG_PATH = '$(QEMUIMG_PATH)'"; \
-         echo "HTOOLS = True"; \
-         echo "ENABLE_CONFD = $(ENABLE_CONFD)"; \
-         echo "XEN_CMD = '$(XEN_CMD)'"; \
-         echo "ENABLE_SPLIT_QUERY = $(ENABLE_SPLIT_QUERY)"; \
-         echo "ENABLE_RESTRICTED_COMMANDS = $(ENABLE_RESTRICTED_COMMANDS)"; \
-         echo "ENABLE_MOND = $(ENABLE_MOND)"; \
-## Write dictionary with man page name as the key and the section number as the
-## value
-         echo "MAN_PAGES = {"; \
-         for i in $(notdir $(man_MANS)); do \
-           echo "$$i" | sed -re 's/^(.*)\.([0-9]+)$$/  "\1": \2,/g'; \
-         done; \
-         echo "}"; \
-       } > $@
+lib/_constants.py: Makefile lib/_constants.py.in src/hs2py-constants \
+                | stamp-directories
+       cat $(abs_top_srcdir)/lib/_constants.py.in > $@
+       src/hs2py-constants >> $@
+
+lib/constants.py: lib/_constants.py
+
+src/AutoConf.hs: Makefile src/AutoConf.hs.in | stamp-directories
+       @echo "m4 ... >" $@
+       @m4 -DPACKAGE_VERSION="$(PACKAGE_VERSION)" \
+           -DVERSION_MAJOR="$(VERSION_MAJOR)" \
+           -DVERSION_MINOR="$(VERSION_MINOR)" \
+           -DVERSION_REVISION="$(VERSION_REVISION)" \
+           -DVERSION_SUFFIX="$(VERSION_SUFFIX)" \
+           -DVERSION_FULL="$(VERSION_FULL)" \
+           -DDIRVERSION="$(DIRVERSION)" \
+           -DLOCALSTATEDIR="$(localstatedir)" \
+           -DSYSCONFDIR="$(sysconfdir)" \
+           -DSSH_CONFIG_DIR="$(SSH_CONFIG_DIR)" \
+           -DSSH_LOGIN_USER="$(SSH_LOGIN_USER)" \
+           -DSSH_CONSOLE_USER="$(SSH_CONSOLE_USER)" \
+           -DEXPORT_DIR="$(EXPORT_DIR)" \
+           -DOS_SEARCH_PATH="\"$(OS_SEARCH_PATH)\"" \
+           -DES_SEARCH_PATH="\"$(ES_SEARCH_PATH)\"" \
+           -DXEN_BOOTLOADER="$(XEN_BOOTLOADER)" \
+           -DXEN_CONFIG_DIR="$(XEN_CONFIG_DIR)" \
+           -DXEN_KERNEL="$(XEN_KERNEL)" \
+           -DXEN_INITRD="$(XEN_INITRD)" \
+           -DKVM_KERNEL="$(KVM_KERNEL)" \
+           -DSHARED_FILE_STORAGE_DIR="$(SHARED_FILE_STORAGE_DIR)" \
+           -DIALLOCATOR_SEARCH_PATH="\"$(IALLOCATOR_SEARCH_PATH)\"" \
+           -DKVM_PATH="$(KVM_PATH)" \
+           -DIP_PATH="$(IP_PATH)" \
+           -DSOCAT_PATH="$(SOCAT)" \
+           -DSOCAT_USE_ESCAPE="$(SOCAT_USE_ESCAPE)" \
+           -DSOCAT_USE_COMPRESS="$(SOCAT_USE_COMPRESS)" \
+           -DLVM_STRIPECOUNT="$(LVM_STRIPECOUNT)" \
+           -DTOOLSDIR="$(libdir)/ganeti/tools" \
+           -DGNT_SCRIPTS="$(foreach i,$(notdir $(gnt_scripts)),\"$(i)\":)" \
+           -DHS_HTOOLS_PROGS="$(foreach i,$(HS_HTOOLS_PROGS),\"$(i)\":)" \
+           -DPKGLIBDIR="$(libdir)/ganeti" \
+           -DSHAREDIR="$(prefix)/share/ganeti" \
+           -DVERSIONEDSHAREDIR="$(versionedsharedir)" \
+           -DDRBD_BARRIERS="$(DRBD_BARRIERS)" \
+           -DDRBD_NO_META_FLUSH="$(DRBD_NO_META_FLUSH)" \
+           -DSYSLOG_USAGE="$(SYSLOG_USAGE)" \
+           -DDAEMONS_GROUP="$(DAEMONS_GROUP)" \
+           -DADMIN_GROUP="$(ADMIN_GROUP)" \
+           -DMASTERD_USER="$(MASTERD_USER)" \
+           -DMASTERD_GROUP="$(MASTERD_GROUP)" \
+           -DRAPI_USER="$(RAPI_USER)" \
+           -DRAPI_GROUP="$(RAPI_GROUP)" \
+           -DCONFD_USER="$(CONFD_USER)" \
+           -DCONFD_GROUP="$(CONFD_GROUP)" \
+           -DLUXID_USER="$(LUXID_USER)" \
+           -DLUXID_GROUP="$(LUXID_GROUP)" \
+           -DNODED_USER="$(NODED_USER)" \
+           -DNODED_GROUP="$(NODED_GROUP)" \
+           -DMOND_USER="$(MOND_USER)" \
+           -DMOND_GROUP="$(MOND_GROUP)" \
+           -DDISK_SEPARATOR="$(DISK_SEPARATOR)" \
+           -DQEMUIMG_PATH="$(QEMUIMG_PATH)" \
+           -DHTOOLS="True" \
+           -DENABLE_CONFD="$(ENABLE_CONFD)" \
+           -DXEN_CMD="$(XEN_CMD)" \
+           -DENABLE_SPLIT_QUERY="$(ENABLE_SPLIT_QUERY)" \
+           -DENABLE_RESTRICTED_COMMANDS="$(ENABLE_RESTRICTED_COMMANDS)" \
+           -DENABLE_MOND="$(ENABLE_MOND)" \
+           -DHAS_GNU_LN="$(HAS_GNU_LN)" \
+           -DMAN_PAGES="$$(for i in $(notdir $(man_MANS)); do \
+                           echo -n "$$i" | sed -re 's/^(.*)\.([0-9]+)$$/("\1",\2):/g'; \
+                           done)" \
+       $(abs_top_srcdir)/src/AutoConf.hs.in > $@
 
 lib/_vcsversion.py: Makefile vcs-version | stamp-directories
        set -e; \
@@ -1696,6 +1872,13 @@ lib/_vcsversion.py: Makefile vcs-version | stamp-directories
          echo "VCS_VERSION = '$$VCSVER'"; \
        } > $@
 
+lib/opcodes.py: Makefile src/hs2py src/Ganeti/PyConstants.hs \
+               lib/opcodes.py.in_before lib/opcodes.py.in_after \
+               | stamp-directories
+       cat $(abs_top_srcdir)/lib/opcodes.py.in_before > $@
+       src/hs2py >> $@
+       cat $(abs_top_srcdir)/lib/opcodes.py.in_after >> $@
+
 lib/_generated_rpc.py: lib/rpc_defs.py $(BUILD_RPC)
        PYTHONPATH=. $(RUN_IN_TEMPDIR) $(CURDIR)/$(BUILD_RPC) lib/rpc_defs.py > $@
 
@@ -1705,7 +1888,7 @@ $(SHELL_ENV_INIT): Makefile stamp-directories
          echo 'readonly LOCALSTATEDIR=$${LOCALSTATEDIR:-$${GANETI_ROOTDIR:-}$(localstatedir)}'; \
          echo 'readonly SYSCONFDIR=$${SYSCONFDIR:-$${GANETI_ROOTDIR:-}$(sysconfdir)}'; \
          echo; \
-         echo 'readonly PKGLIBDIR=$(pkglibdir)'; \
+         echo 'readonly PKGLIBDIR=$(libdir)/ganeti'; \
          echo 'readonly LOG_DIR="$$LOCALSTATEDIR/log/ganeti"'; \
          echo 'readonly RUN_DIR="$$LOCALSTATEDIR/run/ganeti"'; \
          echo 'readonly DATA_DIR="$$LOCALSTATEDIR/lib/ganeti"'; \
@@ -1720,8 +1903,8 @@ $(REPLACE_VARS_SED): $(SHELL_ENV_INIT) Makefile stamp-directories
        { echo 's#@''PREFIX@#$(prefix)#g'; \
          echo 's#@''SYSCONFDIR@#$(sysconfdir)#g'; \
          echo 's#@''LOCALSTATEDIR@#$(localstatedir)#g'; \
-         echo 's#@''BINDIR@#$(bindir)#g'; \
-         echo 's#@''SBINDIR@#$(sbindir)#g'; \
+         echo 's#@''BINDIR@#$(execprefix)/bin#g'; \
+         echo 's#@''SBINDIR@#$(execprefix)/sbin#g'; \
          echo 's#@''LIBDIR@#$(libdir)#g'; \
          echo 's#@''GANETI_VERSION@#$(PACKAGE_VERSION)#g'; \
          echo 's#@''CUSTOM_XEN_BOOTLOADER@#$(XEN_BOOTLOADER)#g'; \
@@ -1730,7 +1913,7 @@ $(REPLACE_VARS_SED): $(SHELL_ENV_INIT) Makefile stamp-directories
          echo 's#@''CUSTOM_IALLOCATOR_SEARCH_PATH@#$(IALLOCATOR_SEARCH_PATH)#g'; \
          echo 's#@''CUSTOM_EXPORT_DIR@#$(EXPORT_DIR)#g'; \
          echo 's#@''RPL_SSH_INITD_SCRIPT@#$(SSH_INITD_SCRIPT)#g'; \
-         echo 's#@''PKGLIBDIR@#$(pkglibdir)#g'; \
+         echo 's#@''PKGLIBDIR@#$(libdir)/ganeti#g'; \
          echo 's#@''GNTMASTERUSER@#$(MASTERD_USER)#g'; \
          echo 's#@''GNTRAPIUSER@#$(RAPI_USER)#g'; \
          echo 's#@''GNTCONFDUSER@#$(CONFD_USER)#g'; \
@@ -1766,7 +1949,7 @@ tools/prepare-node-join: MODULE = ganeti.tools.prepare_node_join
 tools/node-cleanup: MODULE = ganeti.tools.node_cleanup
 $(HS_BUILT_TEST_HELPERS): TESTROLE = $(patsubst test/hs/%,%,$@)
 
-$(PYTHON_BOOTSTRAP): Makefile | stamp-directories
+$(PYTHON_BOOTSTRAP) $(gnt_scripts) $(gnt_python_sbin_SCRIPTS): Makefile | stamp-directories
        test -n "$(MODULE)" || { echo Missing module; exit 1; }
        set -e; \
        { echo '#!/usr/bin/python'; \
@@ -1844,12 +2027,15 @@ check-dirs: $(GENERATED_FILES)
          test -z "$$error"; \
        }
 
+.PHONY: check-news
+check-news:
+       RELEASE=$(PACKAGE_VERSION) $(CHECK_NEWS) < $(top_srcdir)/NEWS
+
 .PHONY: check-local
-check-local: check-dirs $(GENERATED_FILES)
+check-local: check-dirs check-news $(GENERATED_FILES)
        $(CHECK_PYTHON_CODE) $(check_python_code)
        PYTHONPATH=. $(CHECK_HEADER) $(check_python_code)
        $(CHECK_VERSION) $(VERSION) $(top_srcdir)/NEWS
-       RELEASE=$(PACKAGE_VERSION) $(CHECK_NEWS) < $(top_srcdir)/NEWS
        PYTHONPATH=. $(RUN_IN_TEMPDIR) $(CURDIR)/$(CHECK_IMPORTS) . $(standalone_python_modules)
        error= ; \
        if [ "x`echo $(VERSION_SUFFIX)|grep 'alpha'`" == "x" ]; then \
@@ -1931,7 +2117,7 @@ PEP8_IGNORE = E111,E121,E123,E125,E127,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
+LINT_TARGETS = pylint pylint-qa pylint-test
 if HAS_PEP8
 LINT_TARGETS += pep8
 endif
@@ -1953,6 +2139,12 @@ pylint-qa: $(GENERATED_FILES)
        cd $(top_srcdir)/qa && \
          PYTHONPATH=$(abs_top_srcdir) $(PYLINT) $(LINT_OPTS) \
          --rcfile  ../pylintrc $(patsubst qa/%.py,%,$(qa_scripts))
+# FIXME: lint all test code, not just the newly added test support
+pylint-test: $(GENERATED_FILES)
+       @test -n "$(PYLINT)" || { echo 'pylint' not found during configure; exit 1; }
+       cd $(top_srcdir) && \
+               PYTHONPATH=.:./test/py $(PYLINT) $(LINT_OPTS) \
+               --rcfile=pylintrc-test  $(python_test_support)
 
 .PHONY: pep8
 pep8: $(GENERATED_FILES)
@@ -2041,6 +2233,42 @@ install-exec-local:
        @mkdir_p@ "$(DESTDIR)${localstatedir}/lib/ganeti" \
          "$(DESTDIR)${localstatedir}/log/ganeti" \
          "$(DESTDIR)${localstatedir}/run/ganeti"
+       for dir in $(SYMLINK_TARGET_DIRS); do \
+         @mkdir_p@  $(DESTDIR)$$dir; \
+       done
+       $(LN_S) -f $(sysconfdir)/ganeti/lib $(DESTDIR)$(defaultversiondir)
+       $(LN_S) -f $(sysconfdir)/ganeti/share $(DESTDIR)$(defaultversionedsharedir)
+       for prog in $(HS_BIN_ROLES); do \
+         $(LN_S) -f $(defaultversiondir)$(exec_prefix)/bin/$$prog $(DESTDIR)$(exec_prefix)/bin/$$prog; \
+       done
+       $(LN_S) -f $(defaultversiondir)$(libdir)/ganeti/iallocators/hail $(DESTDIR)$(libdir)/ganeti/iallocators/hail
+       for prog in $(all_sbin_scripts); do \
+         $(LN_S) -f $(defaultversiondir)$(exec_prefix)/sbin/$$prog $(DESTDIR)$(exec_prefix)/sbin/$$prog; \
+       done
+       for prog in $(gnt_scripts_basenames); do \
+         $(LN_S) -f $(defaultversionedsharedir)/$$prog $(DESTDIR)$(exec_prefix)/sbin/$$prog; \
+       done
+       for prog in $(pkglib_python_basenames); do \
+         $(LN_S) -f $(defaultversionedsharedir)/$$prog $(DESTDIR)$(libdir)/ganeti/$$prog; \
+       done
+       for prog in $(tools_python_basenames); do \
+         $(LN_S) -f $(defaultversionedsharedir)/$$prog $(DESTDIR)$(libdir)/ganeti/tools/$$prog; \
+       done
+       for prog in $(tools_basenames); do \
+         $(LN_S) -f $(defaultversiondir)/$(libdir)/ganeti/tools/$$prog $(DESTDIR)$(libdir)/ganeti/tools/$$prog; \
+       done
+       if ! test -n '$(ENABLE_MANPAGES)'; then \
+         for man in $(manfullpath); do \
+           $(LN_S) -f $(defaultversionedsharedir)$(datarootdir)/man/$$man $(DESTDIR)$(datarootdir)/man/$$man; \
+         done; \
+       fi
+       for prog in $(myexeclib_scripts_basenames); do \
+         $(LN_S) -f $(defaultversiondir)$(libdir)/ganeti/$$prog $(DESTDIR)$(libdir)/ganeti/$$prog; \
+       done
+if INSTALL_SYMLINKS
+       $(LN_S) -f $(versionedsharedir) $(DESTDIR)$(sysconfdir)/ganeti/share
+       $(LN_S) -f $(versiondir) $(DESTDIR)$(sysconfdir)/ganeti/lib
+endif
 
 .PHONY: apidoc
 if WANT_HSAPIDOC
@@ -2136,7 +2364,7 @@ hs-coverage: $(haskell_tests) test/hs/hpc-htools test/hs/hpc-mon-collector
        hpc sum --union $(HPCEXCL) \
          htest.tix hpc-htools.tix hpc-mon-collector.tix > coverage-hs.tix
        hpc markup --destdir=$(COVERAGE_HS_DIR) coverage-hs.tix
-       hpc report coverage-hs.tix
+       hpc report coverage-hs.tix | tee $(COVERAGE_HS_DIR)/report.txt
        $(LN_S) -f hpc_index.html $(COVERAGE_HS_DIR)/index.html
 
 # Special "kind-of-QA" target for htools, needs special setup (all
diff --git a/NEWS b/NEWS
index 934b5ae..42a295e 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -2,6 +2,74 @@ News
 ====
 
 
+Version 2.10.0 alpha1
+---------------------
+
+*(unreleased)*
+
+Incompatible/important changes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+- Adding disks with 'gnt-instance modify' now waits for the disks to sync per
+  default. Specify --no-wait-for-sync to override this behavior.
+- The Ganeti python code now adheres to a private-module layout. In particular,
+  the module 'ganeti' is no longer in the python search path.
+- On instance allocation, the iallocator now considers non-LVM storage
+  properly. In particular, actual file storage space information is used
+  when allocating space for a file/sharedfile instance.
+- When disabling disk templates cluster-wide, the cluster now first
+  checks whether there are instances still using those templates.
+- 'gnt-node list-storage' now also reports storage information about
+  file-based storage types.
+
+New features
+~~~~~~~~~~~~
+
+- KVM hypervisors can now access RBD storage directly without having to
+  go through a block device.
+- A new command 'gnt-cluster upgrade' was added that automates the upgrade
+  procedure between two Ganeti versions that are both 2.10 or higher.
+- The move-instance command can now change disk templates when moving
+  instances, and does not require any node placement options to be
+  specified if the destination cluster has a default iallocator.
+- Users can now change the soundhw and cpuid settings for XEN hypervisors.
+- Hail and hbal now have the (optional) capability of accessing average CPU
+  load information through the monitoring deamon, and to use it to dynamically
+  adapt the allocation of instances.
+- Hotplug support. Introduce new option '--hotplug' to ``gnt-instance modify``
+  so that disk and NIC modifications take effect without the need of actual
+  reboot. There are a couple of constrains currently for this feature:
+
+   - only KVM hypervisor (versions >= 1.0) supports it,
+   - one can not (yet) hotplug a disk using userspace access mode for RBD
+   - in case of a downgrade instances should suffer a reboot in order to
+     be migratable (due to core change of runtime files)
+
+Misc changes
+~~~~~~~~~~~~
+
+- A new test framework for logical units was introduced and the test
+  coverage for logical units was improved significantly.
+- Opcodes are entirely generated from Haskell using the tool 'hs2py' and
+  the module 'src/Ganeti/OpCodes.hs'.
+- Constants are also generated from Haskell using the tool
+  'hs2py-constants' and the module 'src/Ganeti/HsConstants.hs', with the
+  exception of socket related constants, which require changing the
+  cluster configuration file, and HVS related constants, because they
+  are part of a port of instance queries to Haskell.  As a result, these
+  changes will be part of the next release of Ganeti.
+
+New dependencies
+~~~~~~~~~~~~~~~~
+
+The following new dependencies have been added/updated.
+
+Python
+
+- The version requirements for ``python-mock`` have increased to at least
+  version 1.0.1. It is still used for testing only.
+
+
 Version 2.9.0 rc4
 -----------------
 
diff --git a/README b/README
index 4266b70..6ef2668 100644 (file)
--- a/README
+++ b/README
@@ -1,5 +1,5 @@
-Ganeti 2.9
-==========
+Ganeti 2.10
+===========
 
 For installation instructions, read the INSTALL and the doc/install.rst
 files.
index 63def12..731e93d 100755 (executable)
@@ -33,6 +33,10 @@ import itertools
 import optparse
 from cStringIO import StringIO
 
+# _constants shouldn't be imported from anywhere except constants.py, but we're
+# making an exception here because this script is only used at build time.
+from ganeti import _constants
+
 from ganeti import constants
 from ganeti import cli
 from ganeti import utils
@@ -41,10 +45,6 @@ from ganeti import pathutils
 
 from ganeti.tools import burnin
 
-# _autoconf shouldn't be imported from anywhere except constants.py, but we're
-# 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]+$")
@@ -832,7 +832,7 @@ def main():
   WritePreamble(sw, debug)
 
   # gnt-* scripts
-  for scriptname in _autoconf.GNT_SCRIPTS:
+  for scriptname in _constants.GNT_SCRIPTS:
     filename = "scripts/%s" % scriptname
 
     WriteCompletion(sw, scriptname, GetFunctionName(scriptname), debug,
@@ -849,17 +849,17 @@ def main():
                          debug=not options.compact)
 
   # htools, if enabled
-  if _autoconf.HTOOLS:
-    for script in _autoconf.HTOOLS_PROGS:
+  if _constants.HTOOLS:
+    for script in _constants.HTOOLS_PROGS:
       WriteHaskellCompletion(sw, script, htools=True, debug=debug)
 
   # ganeti-confd, if enabled
-  if _autoconf.ENABLE_CONFD:
+  if _constants.ENABLE_CONFD:
     WriteHaskellCompletion(sw, "src/ganeti-confd", htools=False,
                            debug=debug)
 
   # mon-collector, if monitoring is enabled
-  if _autoconf.ENABLE_MOND:
+  if _constants.ENABLE_MOND:
     WriteHaskellCmdCompletion(sw, "src/mon-collector", debug=debug)
 
   # Reset extglob to original value
index 0cb07bb..421d3d4 100755 (executable)
 import re
 import types
 
-from ganeti import _autoconf
+from ganeti import _constants
 from ganeti import compat
 from ganeti import constants
 from ganeti import errors
 from ganeti import luxi
-from ganeti import opcodes
 from ganeti import qlang
 from ganeti import jstore
 
@@ -288,7 +287,9 @@ def Convert(module, prefix):
   """
   lines = [""]
 
-  all_items = dict((name, getattr(module, name)) for name in dir(module))
+  all_items = dict((name, getattr(module, name))
+                   for name in dir(module)
+                   if name not in dir(_constants))
 
   for name in sorted(all_items.keys()):
     value = all_items[name]
@@ -300,24 +301,12 @@ def Convert(module, prefix):
   return "\n".join(lines)
 
 
-def ConvertMisc():
-  """Convert some extra computed-values to Haskell.
-
-  """
-  lines = [""]
-  lines.extend(ConvertVariable("opcodes", "OP_IDS",
-                               opcodes.OP_MAPPING.keys(), {}))
-  return "\n".join(lines)
-
-
 def main():
   print Convert(constants, "")
   print Convert(luxi, "luxi")
   print Convert(qlang, "qlang")
-  print Convert(_autoconf, "autoconf")
   print Convert(errors, "errors")
   print Convert(jstore, "jstore")
-  print ConvertMisc()
 
 
 if __name__ == "__main__":
index 3c1a79b..080abdb 100755 (executable)
@@ -28,7 +28,7 @@ mv $tmpdir/lib $tmpdir/ganeti
 ln -T -s $tmpdir/ganeti $tmpdir/lib
 
 mkdir -p $tmpdir/src $tmpdir/test/hs
-for hfile in htools ganeti-confd mon-collector; do
+for hfile in htools ganeti-confd mon-collector hs2py; do
   if [ -e src/$hfile ]; then
     ln -s $PWD/src/$hfile $tmpdir/src/
   fi
index 8aa43d3..7cedc7f 100644 (file)
@@ -1,8 +1,8 @@
 # Configure script for Ganeti
 m4_define([gnt_version_major], [2])
-m4_define([gnt_version_minor], [9])
+m4_define([gnt_version_minor], [10])
 m4_define([gnt_version_revision], [0])
-m4_define([gnt_version_suffix], [~rc3])
+m4_define([gnt_version_suffix], [~alpha1])
 m4_define([gnt_version_full],
           m4_format([%d.%d.%d%s],
                     gnt_version_major, gnt_version_minor,
@@ -20,6 +20,38 @@ AC_SUBST([VERSION_REVISION], gnt_version_revision)
 AC_SUBST([VERSION_SUFFIX], gnt_version_suffix)
 AC_SUBST([VERSION_FULL], gnt_version_full)
 
+# --enable-versionfull
+AC_ARG_ENABLE([versionfull],
+  [AS_HELP_STRING([--enable-versionfull],
+                  m4_normalize([use the full version string rather
+                  than major.minor for version directories]))],
+  [[if test "$enableval" != no; then
+      USE_VERSION_FULL=yes
+    else
+      USER_VERSION_FULL=no
+    fi
+  ]],
+  [USE_VERSION_FULL=no
+  ])
+AC_SUBST(USE_VERSION_FULL, $USE_VERSION_FULL)
+AM_CONDITIONAL([USE_VERSION_FULL], [test "$USE_VERSION_FULL" = yes])
+
+# --enable-symlinks
+AC_ARG_ENABLE([symlinks],
+  [AS_HELP_STRING([--enable-symlinks],
+                  m4_normalize([also install version-dependent symlinks under
+                  $sysconfdir (default: enabled)]))],
+  [[if test "$enableval" != no; then
+      INSTALL_SYMLINKS=yes
+    else
+      INSTALL_SYMLINKS=no
+    fi
+  ]],
+  [INSTALL_SYMLINKS=yes
+  ])
+AC_SUBST(INSTALL_SYMLINKS, $INSTALL_SYMLINKS)
+AM_CONDITIONAL([INSTALL_SYMLINKS], [test "$INSTALL_SYMLINKS" = yes])
+
 # --with-ssh-initscript=...
 AC_ARG_WITH([ssh-initscript],
   [AS_HELP_STRING([--with-ssh-initscript=SCRIPT],
@@ -59,37 +91,34 @@ AC_ARG_WITH([xen-config-dir],
 AC_SUBST(XEN_CONFIG_DIR, $xen_config_dir)
 
 # --with-os-search-path=...
-# do a bit of black sed magic to for quoting of the strings in the list
 AC_ARG_WITH([os-search-path],
   [AS_HELP_STRING([--with-os-search-path=LIST],
     [comma separated list of directories to]
     [ search for OS images (default is /srv/ganeti/os)]
   )],
-  [os_search_path=`echo -n "$withval" | sed -e "s/\([[^,]]*\)/'\1'/g"`],
-  [os_search_path="'/srv/ganeti/os'"])
+  [os_search_path="$withval"],
+  [os_search_path="/srv/ganeti/os"])
 AC_SUBST(OS_SEARCH_PATH, $os_search_path)
 
 # --with-extstorage-search-path=...
-# same black sed magic for quoting of the strings in the list
 AC_ARG_WITH([extstorage-search-path],
   [AS_HELP_STRING([--with-extstorage-search-path=LIST],
     [comma separated list of directories to]
     [ search for External Storage Providers]
     [ (default is /srv/ganeti/extstorage)]
   )],
-  [es_search_path=`echo -n "$withval" | sed -e "s/\([[^,]]*\)/'\1'/g"`],
-  [es_search_path="'/srv/ganeti/extstorage'"])
+  [es_search_path="$withval"],
+  [es_search_path="/srv/ganeti/extstorage"])
 AC_SUBST(ES_SEARCH_PATH, $es_search_path)
 
 # --with-iallocator-search-path=...
-# do a bit of black sed magic to for quoting of the strings in the list
 AC_ARG_WITH([iallocator-search-path],
   [AS_HELP_STRING([--with-iallocator-search-path=LIST],
     [comma separated list of directories to]
     [ search for instance allocators (default is $libdir/ganeti/iallocators)]
   )],
-  [iallocator_search_path=`echo -n "$withval" | sed -e "s/\([[^,]]*\)/'\1'/g"`],
-  [iallocator_search_path="'$libdir/$PACKAGE_NAME/iallocators'"])
+  [iallocator_search_path="$withval"],
+  [iallocator_search_path="$libdir/$PACKAGE_NAME/iallocators"])
 AC_SUBST(IALLOCATOR_SEARCH_PATH, $iallocator_search_path)
 
 # --with-xen-bootloader=...
@@ -296,6 +325,7 @@ then
 fi
 AC_SUBST(SYSLOG_USAGE, $SYSLOG)
 
+# --enable-restricted-commands[=no/yes]
 AC_ARG_ENABLE([restricted-commands],
   [AS_HELP_STRING([--enable-restricted-commands],
                   m4_normalize([enable restricted commands in the node daemon
@@ -323,6 +353,14 @@ AC_SUBST(DISK_SEPARATOR, $disk_separator)
 AC_PROG_INSTALL
 AC_PROG_LN_S
 
+# check if ln is the GNU version of ln (and hence supports -T)
+if ln --version 2> /dev/null | head -1 | grep -q GNU
+then
+  AC_SUBST(HAS_GNU_LN, True)
+else
+  AC_SUBST(HAS_GNU_LN, False)
+fi
+
 # Check for the ip command
 AC_ARG_VAR(IP_PATH, [ip path])
 AC_PATH_PROG(IP_PATH, [ip], [])
index a462f8d..8196019 100755 (executable)
@@ -111,126 +111,166 @@ debootstrap --arch $ARCH $DIST_RELEASE $CHDIR
 
 APT_INSTALL="apt-get install -y --no-install-recommends"
 
-echo "deb http://backports.debian.org/debian-backports" \
-     "$DIST_RELEASE-backports main contrib non-free" \
-     > $CHDIR/etc/apt/sources.list.d/backports.list
+if [ DIST_RELEASE = squeeze ]
+then
+  echo "deb http://backports.debian.org/debian-backports" \
+       "$DIST_RELEASE-backports main contrib non-free" \
+       > $CHDIR/etc/apt/sources.list.d/backports.list
+fi
 
 #Install all the packages
 in_chroot -- \
   apt-get update
 
+case $DIST_RELEASE in
+
+  squeeze)
+
+    # do not install libghc6-network-dev, since it's too old, and just
+    # confuses the dependencies
+    in_chroot -- \
+      $APT_INSTALL \
+        autoconf automake \
+        ghc cabal-install \
+        libghc6-curl-dev \
+        libghc6-parallel-dev \
+        libghc6-text-dev \
+        libghc6-vector-dev \
+        libpcre3-dev \
+        hlint hscolour pandoc \
+        graphviz qemu-utils \
+        python-docutils \
+        python-simplejson \
+        python-pyparsing \
+        python-pyinotify \
+        python-pycurl \
+        python-ipaddr \
+        python-yaml \
+        python-paramiko
+
+    in_chroot -- \
+      $APT_INSTALL python-setuptools python-dev build-essential
+
+    in_chroot -- \
+      easy_install \
+        logilab-astng==0.24.1 \
+        logilab-common==0.58.3 \
+        mock==1.0.1 \
+        pylint==0.26.0
+
+    in_chroot -- \
+      easy_install \
+        sphinx==1.1.3 \
+        pep8==1.3.3 \
+        coverage==3.4 \
+        bitarray==0.8.0
+
+    in_chroot -- \
+      cabal update
+
+    in_chroot -- \
+      cabal install --global \
+        network==2.3 \
+        regex-pcre==0.94.4 \
+        hinotify==0.3.2 \
+        hslogger==1.1.4 \
+        quickcheck==2.5.1.1 \
+        attoparsec==0.10.1.1 \
+        crypto==4.2.4 \
+        MonadCatchIO-transformers==0.2.2.0 \
+        mtl==2.0.1.0 \
+        hashable==1.1.2.0 \
+        case-insensitive==0.3 \
+        parsec==3.0.1 \
+        snap-server==0.8.1 \
+        json==0.4.4
+
+    in_chroot -- \
+      cabal install --global \
+        hunit==1.2.5.2 \
+        happy==1.18.10 \
+        hlint==1.8.43 \
+        hscolour==1.20.3 \
+        temporary==1.1.2.3 \
+        test-framework==0.6.1 \
+        test-framework-hunit==0.2.7 \
+        test-framework-quickcheck2==0.2.12.3
+
+    in_chroot -- \
+      cabal install --global cabal-file-th
+
+    in_chroot -- \
+      cabal install --global shelltestrunner
+
+    #Install selected packages from backports
+    in_chroot -- \
+      $APT_INSTALL -t squeeze-backports \
+        git \
+        git-email \
+        vim
+
+;;
+
+  wheezy)
+
+    in_chroot -- \
+      $APT_INSTALL \
+      autoconf automake ghc ghc-haddock libghc-network-dev \
+      libghc-test-framework{,-hunit,-quickcheck2}-dev \
+      libghc-json-dev libghc-curl-dev libghc-hinotify-dev \
+      libghc-parallel-dev libghc-utf8-string-dev \
+      libghc-hslogger-dev libghc-crypto-dev \
+      libghc-regex-pcre-dev libghc-attoparsec-dev \
+      libghc-vector-dev libghc-temporary-dev \
+      libghc-snap-server-dev libpcre3 libpcre3-dev hscolour hlint pandoc \
+      python-setuptools python-sphinx python-epydoc graphviz python-pyparsing \
+      python-simplejson python-pycurl python-paramiko \
+      python-bitarray python-ipaddr python-yaml qemu-utils python-coverage pep8 \
+      shelltestrunner python-dev pylint openssh-client vim git git-email
+
+      easy_install pyinotify==0.9.4
+
+;;
+
+  *)
+
+    in_chroot -- \
+      $APT_INSTALL \
+      autoconf automake ghc ghc-haddock libghc-network-dev \
+      libghc-test-framework{,-hunit,-quickcheck2}-dev \
+      libghc-json-dev libghc-curl-dev libghc-hinotify-dev \
+      libghc-parallel-dev libghc-utf8-string-dev \
+      libghc-hslogger-dev libghc-crypto-dev \
+      libghc-regex-pcre-dev libghc-attoparsec-dev \
+      libghc-vector-dev libghc-temporary-dev \
+      libghc-snap-server-dev libpcre3 libpcre3-dev hscolour hlint pandoc \
+      python-setuptools python-sphinx python-epydoc graphviz python-pyparsing \
+      python-simplejson python-pyinotify python-pycurl python-paramiko \
+      python-bitarray python-ipaddr python-yaml qemu-utils python-coverage pep8 \
+      shelltestrunner python-dev pylint openssh-client vim git git-email
+
+;;
+esac
 
-# do not install libghc6-network-dev, since it's too old, and just
-# confuses the dependencies
-in_chroot -- \
-  $APT_INSTALL \
-    autoconf automake \
-    ghc cabal-install \
-    libghc6-curl-dev \
-    libghc6-parallel-dev \
-    libghc6-text-dev \
-    libghc6-vector-dev \
-    libpcre3-dev \
-    hlint hscolour pandoc \
-    graphviz socat qemu-utils \
-    python-docutils \
-    python-simplejson \
-    python-pyparsing \
-    python-pyinotify \
-    python-pycurl \
-    python-ipaddr \
-    python-yaml \
-    python-paramiko
-
-in_chroot -- \
-  $APT_INSTALL python-setuptools python-dev build-essential
-
-in_chroot -- \
-  easy_install \
-    logilab-astng==0.24.1 \
-    logilab-common==0.58.3 \
-    mock==1.0.1 \
-    pylint==0.26.0
-
-in_chroot -- \
-  easy_install \
-    sphinx==1.1.3 \
-    pep8==1.3.3 \
-    coverage==3.4 \
-    bitarray==0.8.0
-
-in_chroot -- \
-  cabal update
-
-in_chroot -- \
-  cabal install --global \
-    network==2.3 \
-    regex-pcre==0.94.2 \
-    hinotify==0.3.2 \
-    hslogger==1.1.4 \
-    quickcheck==2.5.1.1 \
-    attoparsec==0.10.1.1 \
-    crypto==4.2.4 \
-    MonadCatchIO-transformers==0.2.2.0 \
-    mtl==2.0.1.0 \
-    hashable==1.1.2.0 \
-    case-insensitive==0.3 \
-    parsec==3.0.1 \
-    network==2.3 \
-    snap-server==0.8.1 \
-    json==0.4.4
-
-in_chroot -- \
-  cabal install --global \
-    hunit==1.2.5.2 \
-    happy==1.18.10 \
-    hlint==1.8.43 \
-    hscolour==1.20.3 \
-    temporary==1.1.2.3 \
-    test-framework==0.6.1 \
-    test-framework-hunit==0.2.7 \
-    test-framework-quickcheck2==0.2.12.3
-
-in_chroot -- \
-  cabal install --global cabal-file-th
-
-in_chroot -- \
-  cabal install --global shelltestrunner
+echo "en_US.UTF-8 UTF-8" >> $CHDIR/etc/locale.gen
 
-#Install selected packages from backports
 in_chroot -- \
-  $APT_INSTALL -t squeeze-backports \
-    git \
-    git-email \
-    vim
-
-in_chroot -- \
-  $APT_INSTALL sudo fakeroot rsync locales less
-
-echo "en_US.UTF-8 UTF-8" >> $CHDIR/etc/locale.gen
+  $APT_INSTALL sudo fakeroot rsync locales less socat
 
 in_chroot -- \
   locale-gen
 
 in_chroot -- \
   $APT_INSTALL lvm2 ssh bridge-utils iproute iputils-arping \
-               ndisc6 python python-pyopenssl openssl \
-               python-mock \
-               socat fping
-
-in_chroot -- \
-  $APT_INSTALL qemu-utils
+               ndisc6 python-openssl openssl \
+               python-mock fping qemu-utils
 
 in_chroot -- \
   easy_install affinity
 
-#Python development tools
 in_chroot -- \
-  $APT_INSTALL python-epydoc
-
-#Tools for creating debian packages
-in_chroot -- \
-  $APT_INSTALL debhelper quilt
+  $APT_INSTALL \
+  python-epydoc debhelper quilt
 
 # extra debian packages
 
@@ -260,5 +300,4 @@ rm -rf $TEMP_DATA_DIR
 echo "Chroot created. In order to run it:"
 echo " * Copy the file $FINAL_CHROOT_CONF to $CONF_DIR/$FINAL_CHROOT_CONF"
 echo " * Copy the file $COMP_FILEPATH to $CHROOT_DIR/$COMP_FILENAME"
-
 echo "Then run \"schroot -c $CHROOTNAME\""
diff --git a/doc/design-2.10.rst b/doc/design-2.10.rst
new file mode 100644 (file)
index 0000000..38caad7
--- /dev/null
@@ -0,0 +1,16 @@
+==================
+Ganeti 2.10 design
+==================
+
+The following design documents have been implemented in Ganeti 2.10.
+
+- :doc:`design-cmdlib-unittests`
+- :doc:`design-hotplug`
+- :doc:`design-openvswitch`
+- :doc:`design-storagetypes`
+- :doc:`design-upgrade`
+
+The following designs have been partially implemented in Ganeti 2.10.
+
+- :doc:`design-ceph-ganeti-support`
+- :doc:`design-query-splitting`
diff --git a/doc/design-ceph-ganeti-support.rst b/doc/design-ceph-ganeti-support.rst
new file mode 100644 (file)
index 0000000..96d648f
--- /dev/null
@@ -0,0 +1,185 @@
+============================
+RADOS/Ceph support in Ganeti
+============================
+
+.. contents:: :depth: 4
+
+Objective
+=========
+
+The project aims to improve Ceph RBD support in Ganeti. It can be
+primarily divided into following tasks.
+
+- Use Qemu/KVM RBD driver to provide instances with direct RBD
+  support.
+- Allow Ceph RBDs' configuration through Ganeti.
+- Write a data collector to monitor Ceph nodes.
+
+Background
+==========
+
+Ceph RBD
+--------
+
+Ceph is a distributed storage system which provides data access as
+files, objects and blocks. As part of this project, we're interested in
+integrating ceph's block device (RBD) directly with Qemu/KVM.
+
+Primary components/daemons of Ceph.
+- Monitor - Serve as authentication point for clients.
+- Metadata - Store all the filesystem metadata (Not configured here as
+they are not required for RBD)
+- OSD - Object storage devices. One daemon for each drive/location.
+
+RBD support in Ganeti
+---------------------
+
+Currently, Ganeti supports RBD volumes on a pre-configured Ceph cluster.
+This is enabled through RBD disk templates. These templates allow RBD
+volume's access through RBD Linux driver. The volumes are mapped to host
+as local block devices which are then attached to the instances. This
+method incurs an additional overhead. We plan to resolve it by using
+Qemu's RBD driver to enable direct access to RBD volumes for KVM
+instances.
+
+Also, Ganeti currently uses RBD volumes on a pre-configured ceph cluster.
+Allowing configuration of ceph nodes through Ganeti will be a good
+addition to its prime features.
+
+
+Qemu/KVM Direct RBD Integration
+===============================
+
+A new disk param ``access`` is introduced. It's added at
+cluster/node-group level to simplify prototype implementation.
+It will specify the access method either as ``userspace`` or
+``kernelspace``. It's accessible to StartInstance() in hv_kvm.py. The
+device path, ``rbd:<pool>/<vol_name>``, is generated by RADOSBlockDevice
+and is added to the params dictionary as ``kvm_dev_path``.
+
+This approach ensures that no disk template specific changes are
+required in hv_kvm.py allowing easy integration of other distributed
+storage systems (like Gluster).
+
+Note that the RBD volume is mapped as a local block device as before.
+The local mapping won't be used during instance operation in the
+``userspace`` access mode, but can be used by administrators and OS
+scripts.
+
+Updated commands
+----------------
+::
+  $ gnt-instance info
+
+``access:userspace/kernelspace`` will be added to Disks category. This
+output applies to KVM based instances only.
+
+Ceph configuration on Ganeti nodes
+==================================
+
+This document proposes configuration of distributed storage
+pool (Ceph or Gluster) through ganeti. Currently, this design document
+focuses on configuring a Ceph cluster. A prerequisite of this setup
+would be installation of ceph packages on all the concerned nodes.
+
+At Ganeti Cluster init, the user will set distributed-storage specific
+options which will be stored at cluster level. The Storage cluster
+will be initialized using ``gnt-storage``. For the prototype, only a
+single storage pool/node-group is configured.
+
+Following steps take place when a node-group is initialized as a storage
+cluster.
+
+  - Check for an existing ceph cluster through /etc/ceph/ceph.conf file
+    on each node.
+  - Fetch cluster configuration parameters and create a distributed
+    storage object accordingly.
+  - Issue an 'init distributed storage' RPC to group nodes (if any).
+  - On each node, ``ceph`` cli tool will run appropriate services.
+  - Mark nodes as well as the node-group as distributed-storage-enabled.
+
+The storage cluster will operate at a node-group level. The ceph
+cluster will be initiated using gnt-storage. A new sub-command
+``init-distributed-storage`` will be added to it.
+
+The configuration of the nodes will be handled through an init function
+called by the node daemons running on the respective nodes. A new RPC is
+introduced to handle the calls.
+
+A new object will be created to send the storage parameters to the node
+- storage_type, devices, node_role (mon/osd) etc.
+
+A new node can be directly assigned to the storage enabled node-group.
+During the 'gnt-node add' process, required ceph daemons will be started
+and node will be added to the ceph cluster.
+
+Only an offline node can be assigned to storage enabled node-group.
+``gnt-node --readd`` needs to be performed to issue RPCs for spawning
+appropriate services on the newly assigned node.
+
+Updated Commands
+----------------
+
+Following are the affected commands.::
+
+  $ gnt-cluster init -S ceph:disk=/dev/sdb,option=value...
+
+During cluster initialization, ceph specific options are provided which
+apply at cluster-level.::
+
+  $ gnt-cluster modify -S ceph:option=value2...
+
+For now, cluster modification will be allowed when there is no
+initialized storage cluster.::
+
+  $ gnt-storage init-distributed-storage -s{--storage-type} ceph \
+    <node-group>
+
+Ensure that no other node-group is configured as distributed storage
+cluster and configure ceph on the specified node-group. If there is no
+node in the node-group, it'll only be marked as distributed storage
+enabled and no action will be taken.::
+
+  $ gnt-group assign-nodes <group> <node>
+
+It ensures that the node is offline if the node-group specified is
+distributed storage capable. Ceph configuration on the newly assigned
+node is not performed at this step.::
+
+  $ gnt-node --offline
+
+If the node is part of storage node-group, an offline call will stop/remove
+ceph daemons.::
+
+  $ gnt-node add --readd
+
+If the node is now part of the storage node-group, issue init
+distributed storage RPC to the respective node. This step is required
+after assigning a node to the storage enabled node-group::
+
+  $ gnt-node remove
+
+A warning will be issued stating that the node is part of distributed
+storage, mark it offline before removal.
+
+Data collector for Ceph
+-----------------------
+
+TBD
+
+Future Work
+-----------
+
+Due to the loopback bug in ceph, one may run into daemon hang issues
+while performing writes to a RBD volumes through block device mapping.
+This bug is applicable only when the RBD volume is stored on the OSD
+running on the local node. In order to mitigate this issue, we can
+create storage pools on different nodegroups and access RBD
+volumes on different pools.
+http://tracker.ceph.com/issues/3076
+
+.. vim: set textwidth=72 :
+.. Local Variables:
+.. mode: rst
+.. fill-column: 72
+.. End:
diff --git a/doc/design-cmdlib-unittests.rst b/doc/design-cmdlib-unittests.rst
new file mode 100644 (file)
index 0000000..29efec8
--- /dev/null
@@ -0,0 +1,177 @@
+=====================================
+Unit tests for cmdlib / LogicalUnit's
+=====================================
+
+.. contents:: :depth: 4
+
+This is a design document describing unit tests for the cmdlib module.
+Other modules are deliberately omitted, as LU's contain the most complex
+logic and are only sparingly tested.
+
+Current state and shortcomings
+==============================
+
+The current test coverage of the cmdlib module is at only ~14%. Given
+the complexity of the code this is clearly too little.
+
+The reasons for this low coverage are numerous. There are organisational
+reasons, like no strict requirements for unit tests for each feature.
+But there are also design and technical reasons, which this design
+document wants to address. First, it's not clear which parts of LU's
+should be tested by unit tests, i.e. the test boundaries are not clearly
+defined. And secondly, it's too hard to actually write unit tests for
+LU's. There exists no good framework or set of tools to write easy to
+understand and concise tests.
+
+Proposed changes
+================
+
+This design document consists of two parts. Initially, the test
+boundaries for cmdlib are laid out, and considerations about writing
+unit tests are given. Then the test framework is described, together
+with a rough overview of the individual parts and how they are meant
+to be used.
+
+Test boundaries
+---------------
+
+For the cmdlib module, every LogicalUnit is seen as a unit for testing.
+Unit tests for LU's may only execute the LU but make sure that no side
+effect (like filesystem access, network access or the like) takes
+place. Smaller test units (like individual methods) are sensible and
+will be supported by the test framework. However, they are not the main
+scope of this document.
+
+LU's require the following environment to be provided by the test code
+in order to be executed:
+
+An input opcode
+  LU's get all the user provided input and parameters from the opcode.
+The command processor
+  Used to get the execution context id and to output logging  messages.
+  It also drives the execution of LU's by calling the appropriate
+  methods in the right order.
+The Ganeti context
+  Provides node-management methods and contains
+
+   * The configuration. This gives access to the cluster configuration.
+   * The Ganeti Lock Manager. Manages locks during the execution.
+
+The RPC runner
+  Used to communicate with node daemons on other nodes and to perform
+  operations on them.
+
+The IAllocator runner
+  Calls the IAllocator with a given request.
+
+All of those components have to be replaced/adapted by the test
+framework.
+
+The goal of unit tests at the LU level is to exercise every possible
+code path in the LU at least once. Shared methods which are used by
+multiple LU's should be made testable by themselves and explicit unit
+tests should be written for them.
+
+Ultimately, the code coverage for the cmdlib module should be higher
+than 90%. As Python is a dynamic language, a portion of those tests
+only exists to exercise the code without actually asserting for
+anything in the test. They merely make sure that no type errors exist
+and that potential typos etc. are caught at unit test time.
+
+Test framework
+--------------
+
+The test framework will it make possible to write short and concise
+tests for LU's. In the simplest case, only an opcode has to be provided
+by the test. The framework will then use default values, like an almost
+empty configuration with only the master node and no instances.
+
+All aspects of the test environment will be configurable by individual
+tests.
+
+MCPU mocking
+************
+
+The MCPU drives the execution of LU's. It has to perform its usual
+sequence of actions, but additionally it has to provide easy access to
+the log output of LU's. It will contain utility assertion methods on the
+output.
+
+The mock will be a sub-class of ``mcpu.Processor`` which overrides
+portions of it in order to support the additional functionality. The
+advantage of being a sub-class of the original processor is the
+automatic compatibility with the code running in real clusters.
+
+Configuration mocking
+*********************
+
+Per default, the mocked configuration will contain only the master node,
+no instances and default parameters. However, convenience methods for
+the following use cases will be provided:
+
+ - "Shortcut" methods to add objects to the configuration.
+ - Helper methods to quickly create standard nodes/instances/etc.
+ - Pre-populated default configurations for standard use-cases (i.e.
+   cluster with three nodes, five instances, etc.).
+ - Convenience assertion methods for checking the configuration.
+
+Lock mocking
+************
+
+Initially, the mocked lock manager always grants all locks. It performs
+the following tasks:
+
+ - It keeps track of requested/released locks.
+ - Provides utility assertion methods for checking locks (current and
+   already released ones).
+
+In the future, this component might be extended to prevent locks from
+being granted. This could eventually be used to test optimistic locking.
+
+RPC mocking
+***********
+
+No actual RPC can be made during unit tests. Therefore, those calls have
+to be replaced and their results mocked. As this will entail a large
+portion of work when writing tests, mocking RPC's will be made as easy as
+possible. This entails:
+
+ - Easy construction of RPC results.
+ - Easy mocking of RPC calls (also multiple ones of the same type during
+   one LU execution).
+ - Asserting for RPC calls (including arguments, affected nodes, etc.).
+
+IAllocator mocking
+******************
+
+Calls (also multiple ones during the execution of a LU) to the
+IAllocator interface have to be mocked. The framework will provide,
+similarly to the RPC mocking, provide means to specify the mocked result
+and to assert on the IAllocator requests.
+
+Future work
+===========
+
+With unit tests for cmdlib in place, further unit testing for other
+modules can and should be added. The test boundaries therefore should be
+aligned with the boundaries from cmdlib.
+
+The mocked locking module can be extended to allow testing of optimistic
+locking in LU's. In this case, on all requested locks are actually
+granted to the LU, so it has to adapt for this situation correctly.
+
+A higher test coverage for LU's will increase confidence in our code and
+tests. Refactorings will be easier to make as more problems are caught
+during tests.
+
+After a baseline of unit tests is established for cmdlib, efficient
+testing guidelines could be put in place. For example, new code could be
+required to not lower the test coverage in cmdlib. Additionally, every
+bug fix could be required to include a test which triggered the bug
+before the fix is created.
+
+.. vim: set textwidth=72 :
+.. Local Variables:
+.. mode: rst
+.. fill-column: 72
+.. End:
index d69cd2f..dfe4f7c 100644 (file)
@@ -2,7 +2,7 @@
 Design document drafts
 ======================
 
-.. Last updated for Ganeti 2.9
+.. Last updated for Ganeti 2.10
 
 .. toctree::
    :maxdepth: 2
@@ -12,11 +12,13 @@ Design document drafts
    design-impexp2.rst
    design-resource-model.rst
    design-query-splitting.rst
-   design-storagetypes.rst
    design-internal-shutdown.rst
    design-glusterfs-ganeti-support.rst
-   design-openvswitch.rst
+   design-hugepages-support.rst
+   design-optables.rst
+   design-ceph-ganeti-support.rst
    design-daemons.rst
+   design-hsqueeze.rst
 
 .. vim: set textwidth=72 :
 .. Local Variables:
diff --git a/doc/design-hotplug.rst b/doc/design-hotplug.rst
new file mode 100644 (file)
index 0000000..1a52229
--- /dev/null
@@ -0,0 +1,252 @@
+=======
+Hotplug
+=======
+
+.. contents:: :depth: 4
+
+This is a design document detailing the implementation of device
+hotplugging in Ganeti. The logic used is hypervisor agnostic but still
+the initial implementation will target the KVM hypervisor. The
+implementation adds ``python-fdsend`` as a new dependency. In case
+it is not installed hotplug will not be possible and the user will
+be notified with a warning.
+
+
+Current state and shortcomings
+==============================
+
+Currently, Ganeti supports addition/removal/modification of devices
+(NICs, Disks) but the actual modification takes place only after
+rebooting the instance. To this end an instance cannot change network,
+get a new disk etc. without a hard reboot.
+
+Until now, in case of KVM hypervisor, code does not name devices nor
+places them in specific PCI slots. Devices are appended in the KVM
+command and Ganeti lets KVM decide where to place them. This means that
+there is a possibility a device that resides in PCI slot 5, after a
+reboot (due to another device removal) to be moved to another PCI slot
+and probably get renamed too (due to udev rules, etc.).
+
+In order for a migration to succeed, the process on the target node
+should be started with exactly the same machine version, CPU
+architecture and PCI configuration with the running process. During
+instance creation/startup ganeti creates a KVM runtime file with all the
+necessary information to generate the KVM command. This runtime file is
+used during instance migration to start a new identical KVM process. The
+current format includes the fixed part of the final KVM command, a list
+of NICs', and hvparams dict. It does not favor easy manipulations
+concerning disks, because they are encapsulated in the fixed KVM
+command.
+
+
+Proposed changes
+================
+
+For the case of the KVM hypervisor, QEMU exposes 32 PCI slots to the
+instance. Disks and NICs occupy some of these slots. Recent versions of
+QEMU have introduced monitor commands that allow addition/removal of PCI
+devices. Devices are referenced based on their name or position on the
+virtual PCI bus. To be able to use these commands, we need to be able to
+assign each device a unique name.
+
+To keep track where each device is plugged into, we add the
+``pci`` slot to Disk and NIC objects, but we save it only in runtime
+files, since it is hypervisor specific info. This is added for easy
+object manipulation and is ensured not to be written back to the config.
+
+We propose to make use of QEMU 1.0 monitor commands so that
+modifications to devices take effect instantly without the need for hard
+reboot. The only change exposed to the end-user will be the addition of
+a ``--hotplug`` option to the ``gnt-instance modify`` command.
+
+Upon hotplugging the PCI configuration of an instance is changed.
+Runtime files should be updated correspondingly. Currently this is
+impossible in case of disk hotplug because disks are included in command
+line entry of the runtime file, contrary to NICs that are correctly
+treated separately. We change the format of runtime files, we remove
+disks from the fixed KVM command and create new entry containing them
+only. KVM options concerning disk are generated during
+``_ExecuteKVMCommand()``, just like NICs.
+
+Design decisions
+================
+
+Which should be each device ID? Currently KVM does not support arbitrary
+IDs for devices; supported are only names starting with a letter, max 32
+chars length, and only including '.' '_' '-' special chars.
+For debugging purposes and in order to be more informative, device will be
+named after: <device type>-<part of uuid>-pci-<slot>.
+
+Who decides where to hotplug each device? As long as this is a
+hypervisor specific matter, there is no point for the master node to
+decide such a thing. Master node just has to request noded to hotplug a
+device. To this end, hypervisor specific code should parse the current
+PCI configuration (i.e. ``info pci`` QEMU monitor command), find the first
+available slot and hotplug the device. Having noded to decide where to
+hotplug a device we ensure that no error will occur due to duplicate
+slot assignment (if masterd keeps track of PCI reservations and noded
+fails to return the PCI slot that the device was plugged into then next
+hotplug will fail).
+
+Where should we keep track of devices' PCI slots? As already mentioned,
+we must keep track of devices PCI slots to successfully migrate
+instances. First option is to save this info to config data, which would
+allow us to place each device at the same PCI slot after reboot. This
+would require to make the hypervisor return the PCI slot chosen for each
+device, and storing this information to config data. Additionally the
+whole instance configuration should be returned with PCI slots filled
+after instance start and each instance should keep track of current PCI
+reservations. We decide not to go towards this direction in order to
+keep it simple and do not add hypervisor specific info to configuration
+data (``pci_reservations`` at instance level and ``pci`` at device
+level). For the aforementioned reason, we decide to store this info only
+in KVM runtime files.
+
+Where to place the devices upon instance startup? QEMU has by default 4
+pre-occupied PCI slots. So, hypervisor can use the remaining ones for
+disks and NICs. Currently, PCI configuration is not preserved after
+reboot.  Each time an instance starts, KVM assigns PCI slots to devices
+based on their ordering in Ganeti configuration, i.e. the second disk
+will be placed after the first, the third NIC after the second, etc.
+Since we decided that there is no need to keep track of devices PCI
+slots, there is no need to change current functionality.
+
+How to deal with existing instances? Hotplug depends on runtime file
+manipulation. It stores there pci info and every device the kvm process is
+currently using. Existing files have no pci info in devices and have block
+devices encapsulated inside kvm_cmd entry. Thus hotplugging of existing devices
+will not be possible. Still migration and hotplugging of new devices will
+succeed. The workaround will happen upon loading kvm runtime: if we detect old
+style format we will add an empty list for block devices and upon saving kvm
+runtime we will include this empty list as well. Switching entirely to new
+format will happen upon instance reboot.
+
+
+Configuration changes
+---------------------
+
+The ``NIC`` and ``Disk`` objects get one extra slot: ``pci``. It refers to
+PCI slot that the device gets plugged into.
+
+In order to be able to live migrate successfully, runtime files should
+be updated every time a live modification (hotplug) takes place. To this
+end we change the format of runtime files. The KVM options referring to
+instance's disks are no longer recorded as part of the KVM command line.
+Disks are treated separately, just as we treat NICs right now. We insert
+and remove entries to reflect the current PCI configuration.
+
+
+Backend changes
+---------------
+
+Introduce one new RPC call:
+
+- hotplug_device(DEVICE_TYPE, ACTION, device, ...)
+
+where DEVICE_TYPE can be either NIC or Disk, and ACTION either REMOVE or ADD.
+
+Hypervisor changes
+------------------
+
+We implement hotplug on top of the KVM hypervisor. We take advantage of
+QEMU 1.0 monitor commands (``device_add``, ``device_del``,
+``drive_add``, ``drive_del``, ``netdev_add``,`` netdev_del``). QEMU
+refers to devices based on their id. We use ``uuid`` to name them
+properly. If a device is about to be hotplugged we parse the output of
+``info pci`` and find the occupied PCI slots. We choose the first
+available and the whole device object is appended to the corresponding
+entry in the runtime file.
+
+Concerning NIC handling, we build on the top of the existing logic
+(first create a tap with _OpenTap() and then pass its file descriptor to
+the KVM process). To this end we need to pass access rights to the
+corresponding file descriptor over the monitor socket (UNIX domain
+socket). The open file is passed as a socket-level control message
+(SCM), using the ``fdsend`` python library.
+
+
+User interface
+--------------
+
+The new ``--hotplug`` option to gnt-instance modify is introduced, which
+forces live modifications.
+
+
+Enabling hotplug
+++++++++++++++++
+
+Hotplug will be optional during gnt-instance modify.  For existing
+instance, after installing a version that supports hotplugging we
+have the restriction that hotplug will not be supported for existing
+devices. The reason is that old runtime files lack of:
+
+1. Device pci configuration info.
+
+2. Separate block device entry.
+
+Hotplug will be supported only for KVM in the first implementation. For
+all other hypervisors, backend will raise an Exception case hotplug is
+requested.
+
+
+NIC Hotplug
++++++++++++
+
+The user can add/modify/remove NICs either with hotplugging or not. If a
+NIC is to be added a tap is created first and configured properly with
+kvm-vif-bridge script. Then the instance gets a new network interface.
+Since there is no QEMU monitor command to modify a NIC, we modify a NIC
+by temporary removing the existing one and adding a new with the new
+configuration. When removing a NIC the corresponding tap gets removed as
+well.
+
+::
+
+ gnt-instance modify --net add --hotplug test
+ gnt-instance modify --net 1:mac=aa:00:00:55:44:33 --hotplug test
+ gnt-instance modify --net 1:remove --hotplug test
+
+
+Disk Hotplug
+++++++++++++
+
+The user can add and remove disks with hotplugging or not. QEMU monitor
+supports resizing of disks, however the initial implementation will
+support only disk addition/deletion.
+
+::
+
+ gnt-instance modify --disk add:size=1G --hotplug test
+ gnt-instance modify --net 1:remove --hotplug test
+
+
+Dealing with chroot and uid pool
+--------------------------------
+
+The design so far covers all issues that arise without addressing the
+case where the kvm process will not run with root privileges.
+Specifically:
+
+- in case of chroot, the kvm process cannot see the newly created device
+
+- in case of uid pool security model, the kvm process is not allowed
+  to access the device
+
+For NIC hotplug we address this problem by using the ``getfd`` monitor
+command and passing the file descriptor to the kvm process over the
+monitor socket using SCM_RIGHTS. For disk hotplug and in case of uid
+pool we can let the hypervisor code temporarily ``chown()`` the  device
+before the actual hotplug. Still this is insufficient in case of chroot.
+In this case, we need to ``mknod()`` the device inside the chroot. Both
+workarounds can be avoided, if we make use of the ``add-fd`` qemu
+monitor command, that was introduced in version 1.3. This command is the
+equivalent of NICs' `get-fd`` for disks and will allow disk hotplug in
+every case. So, if the qemu monitor does not support the ``add-fd``
+command, we will not allow disk hotplug for chroot and uid security
+model and notify the user with the corresponding warning.
+
+.. vim: set textwidth=72 :
+.. Local Variables:
+.. mode: rst
+.. fill-column: 72
+.. End:
diff --git a/doc/design-hsqueeze.rst b/doc/design-hsqueeze.rst
new file mode 100644 (file)
index 0000000..d182490
--- /dev/null
@@ -0,0 +1,132 @@
+=============
+HSqueeze tool
+=============
+
+.. contents:: :depth: 4
+
+This is a design document detailing the node-freeing scheduler, HSqueeze.
+
+
+Current state and shortcomings
+==============================
+
+Externally-mirrored instances can be moved between nodes at low
+cost. Therefore, it is attractive to free up nodes and power them down
+at times of low usage, even for small periods of time, like nights or
+weekends.
+
+Currently, the best way to find out a suitable set of nodes to shut down
+is to use the property of our balancedness metric to move instances
+away from drained nodes. So, one would manually drain more and more
+nodes and see, if `hbal` could find a solution freeing up all those
+drained nodes.
+
+
+Proposed changes
+================
+
+We propose the addition of a new htool command-line tool, called
+`hsqueeze`, that aims at keeping resource usage at a constant high
+level by evacuating and powering down nodes, or powering up nodes and
+rebalancing, as appropriate. By default, only externally-mirrored
+instances are moved, but options are provided to additionally take
+DRBD instances (which can be moved without downtimes), or even all
+instances into consideration.
+
+Tagging of standy nodes
+-----------------------
+
+Powering down nodes that are technically healthy effectively creates a
+new node state: nodes on standby. To avoid further state
+proliferation, and as this information is only used by `hsqueeze`,
+this information is recorded in node tags. `hsqueeze` will assume
+that offline nodes having a tag with prefix `htools:standby:` can
+easily be powered on at any time.
+
+Minimum available resources
+---------------------------
+
+To keep the squeezed cluster functional, a minimal amount of resources
+will be left available on every node. While the precise amount will
+be specifiable via command-line options, a sensible default is chosen,
+like enough resource to start an additional instance at standard
+allocation on each node. If the available resources fall below this
+limit, `hsqueeze` will, in fact, try to power on more nodes, till
+enough resources are available, or all standy nodes are online.
+
+To avoid flapping behavior, a second, higher, amount of reserve
+resources can be specified, and `hsqueeze` will only power down nodes,
+if after the power down this higher amount of reserve resources is
+still available.
+
+Computation of the set to free up
+---------------------------------
+
+To determine which nodes can be powered down, `hsqueeze` basically
+follows the same algorithm as the manual process. It greedily goes
+through all non-master nodes and tries if the algorithm used by `hbal`
+would find a solution (with the appropriate move restriction) that
+frees up the extended set of nodes to be drained, while keeping enough
+resources free. Being based on the algorithm used by `hbal`, all
+restrictions respected by `hbal`, in particular memory reservation
+for N+1 redundancy, are also respected by `hsqueeze`.
+The order in which the nodes are tried is choosen by a
+suitable heuristics, like trying the nodes in order of increasing
+number of instances; the hope is that this reduces the number of
+instances that actually have to be moved.
+
+If the amount of free resources has fallen below the lower limit,
+`hsqueeze` will determine the set of nodes to power up in a similar
+way; it will hypothetically add more and more of the standby
+nodes (in some suitable order) till the algorithm used by `hbal` will
+finally balance the cluster in a way that enough resources are available,
+or all standy nodes are online.
+
+
+Instance moves and execution
+----------------------------
+
+Once the final set of nodes to power down is determined, the instance
+moves are determined by the algorithm used by `hbal`. If
+requested by the `-X` option, the nodes freed up are drained, and the
+instance moves are executed in the same way as `hbal` does. Finally,
+those of the freed-up nodes that do not already have a
+`htools:standby:` tag are tagged as `htools:standby:auto`, all free-up
+nodes are marked as offline and powered down via the
+:doc:`design-oob`.
+
+Similarly, if it is determined that nodes need to be added, then first
+the nodes are powered up via the :doc:`design-oob`, then they're marked
+as online and finally,
+the cluster is balanced in the same way, as `hbal` would do. For the
+newly powered up nodes, the `htools:standby:auto` tag, if present, is
+removed, but no other tags are removed (including other
+`htools:standby:` tags).
+
+
+Design choices
+==============
+
+The proposed algorithm builds on top of the already present balancing
+algorithm, instead of greedily packing nodes as full as possible. The
+reason is, that in the end, a balanced cluster is needed anyway;
+therefore, basing on the balancing algorithm reduces the number of
+instance moves. Additionally, the final configuration will also
+benefit from all improvements to the balancing algorithm, like taking
+dynamic CPU data into account.
+
+We decided to have a separate program instead of adding an option to
+`hbal` to keep the interfaces, especially that of `hbal`, cleaner. It is
+not unlikely that, over time, additional `hsqueeze`-specific options
+might be added, specifying, e.g., which nodes to prefer for
+shutdown. With the approach of the `htools` of having a single binary
+showing different behaviors, having an additional program also does not
+introduce significant additional cost.
+
+We decided to have a whole prefix instead of a single tag reserved
+for marking standby nodes (we consider all tags starting with
+`htools:standby:` as serving only this purpose). This is not only in
+accordance with the tag
+reservations for other tools, but it also allows for further extension
+(like specifying priorities on which nodes to power up first) without
+changing name spaces.
diff --git a/doc/design-hugepages-support.rst b/doc/design-hugepages-support.rst
new file mode 100644 (file)
index 0000000..62c4bce
--- /dev/null
@@ -0,0 +1,95 @@
+===============================
+Huge Pages Support for Ganeti
+===============================
+This is a design document about implementing support for huge pages in
+Ganeti. (Please note that Ganeti works with Transparent Huge Pages i.e.
+THP and any reference in this document to Huge Pages refers to explicit
+Huge Pages).
+
+Current State and Shortcomings:
+-------------------------------
+The Linux kernel allows using pages of larger size by setting aside a
+portion of the memory. Using larger page size may enhance the
+performance of applications that require a lot of memory by improving
+page hits. To use huge pages, memory has to be reserved beforehand. This
+portion of memory is subtracted from free memory and is considered as in
+use. Currently Ganeti cannot take proper advantage of huge pages. On a
+node, if huge pages are reserved and are available to fulfill the VM
+request, Ganeti fails to recognize huge pages and considers the memory
+reserved for huge pages as used memory.  This leads to failure of
+launching VMs on a node where memory is available in the form of huge
+pages rather than normal pages.
+
+Proposed Changes:
+-----------------
+The following components will be changed in order for Ganeti to take
+advantage of Huge Pages.
+
+Hypervisor Parameters:
+----------------------
+Currently, It is possible to set or modify huge pages mount point at
+cluster level via the hypervisor parameter ``mem_path`` as::
+
+       $ gnt-cluster init \
+       >--enabled-hypervisors=kvm -nic-parameters link=br100 \
+       > -H kvm:mem_path=/mount/point/for/hugepages
+
+This hypervisor parameter is inherited by all the instances as
+default although it can be overriden at the instance level.
+
+The following changes will be made to the inheritence behaviour.
+
+-  The hypervisor parameter   ``mem_path`` and all other hypervisor
+   parameters will be made available at the node group level (in
+   addition to the cluster level), so that users can set defaults for
+   the node group::
+
+       $ gnt-group add/modify\
+       > -H hv:parameter=value
+
+   This changes the hypervisor inheritence level as::
+
+     cluster -> group -> OS -> instance
+
+-  Furthermore, the hypervisor parameter ``mem_path`` will be changeable
+   only at the cluster or node group level and users must not be able to
+   override this at OS or instance level. The following command must
+   produce an error message that ``mem_path`` may only be set at either
+   the cluster or the node group level::
+
+       $ gnt-instance add -H kvm:mem_path=/mount/point/for/hugepages
+
+Memory Pools:
+-------------
+Memory management of Ganeti will be improved by creating separate pools
+for memory used by the node itself, memory used by the hypervisor and
+the memory reserved for huge pages as:
+- mtotal/xen (Xen memory)
+- mfree/xen (Xen unused memory)
+- mtotal/hp (Memory reserved for Huge Pages)
+- mfree/hp (Memory available from unused huge pages)
+- mpgsize/hp (Size of a huge page)
+
+mfree and mtotal will be changed to mean "the total and free memory for
+the default method in this cluster/nodegroup". Note that the default
+method depends both on the default hypervisor and its parameters.
+
+iAllocator Changes:
+-------------------
+If huge pages are set as default for a cluster of node group, then
+iAllocator must consider the huge pages memory on the nodes, as a
+parameter when trying to find the best node for the VM.
+Note that the iallocator will also be changed to use the correct
+parameter depending on the cluster/group.
+
+hbal Changes:
+-------------
+The cluster balancer (hbal) will be changed to use the default  memory
+pool and  recognize memory reserved for huge pages when trying to
+rebalance the cluster.
+
+.. vim: set textwidth=72 :
+.. Local Variables:
+.. mode: rst
+.. fill-column: 72
+.. End:
index 9546abd..9c4871b 100644 (file)
@@ -49,6 +49,8 @@ The monitoring agent system will report on the following basic information:
 - Node OS CPU load average report
 - Information from a plugin system
 
+.. _monitoring-agent-format-of-the-report:
+
 Format of the report
 --------------------
 
@@ -814,6 +816,13 @@ the data collector will be saved in an appropriate map, associating each
 value to the corresponding collector, using the collector's name as the
 key of the map. This map will be stored in mond's memory.
 
+The collectors are divided in two categories:
+
+- stateless collectors, collectors who have immediate access to the
+  reported information
+- stateful collectors, collectors whose report is based on data collected
+  in a previous time window
+
 For example: the collection function of the CPU load collector will
 collect a CPU load value and save it in the map mentioned above. The
 collection function will be called by the collector thread every t
index 68a720e..555ad82 100644 (file)
@@ -21,10 +21,10 @@ The shortcomings of this approach are:
 
 Proposed changes
 ----------------
-1. Implement functions into gnt-network to manage Open vSwitch through Ganeti gnt-network
-   should be able to create, modify and delete vSwitches. The resulting configuration shall
-   automatically be done on all members of the node group. Connecting Ethernet devices to
-   vSwitches should be managed through this interface as well.
+1. Implement functions into gnt-cluster and gnt-node to manage Open vSwitch through Ganeti.
+   It should be possible to create, modify and delete vSwitches. The resulting configuration
+   shall automatically be done on all members of the node group, if possible. Connecting Ethernet 
+   devices to vSwitches should be managed through this interface as well.
 
 2. Implement VLAN-capabilities: Instances shall have additional information for every NIC: VLAN-ID
    and port type. These are used to determine their type of connection to Open vSwitch. This will
@@ -39,6 +39,29 @@ Proposed changes
    bandwidth and maximum burst. This helps to balance the bandwidth needs between the VMs and to
    ensure fair sharing of the bandwidth.
 
+Automatic configuration of OpenvSwitches
+++++++++++++++++++++++++++++++++++++++++
+Ideally, the OpenvSwitch configuration should be done automatically.
+
+This needs to be done on node level, since each node can be individual and a setting on cluster / node group
+level would be too global is thus not wanted. 
+
+The task that each node needs to do is:
+  ``ovs-vsctl addbr <switchname>`` with <switchname> defaulting to constants.DEFAULT_OVS
+  ``ovs-vsctl add-port <switchname> <ethernet device>`` optional: connection to the outside
+
+This will give us 2 parameters, that are needed for the OpenvSwitch Setup:
+  switchname: Which will default to constants.DEFAULT_OVS when not given
+  ethernet device: Which will default to None when not given, might be more than one (NIC bonding)
+
+These parameters should be set at node level for individuality, _but_ can have defined defaults on cluster
+and node group level, which can be inherited and thus allow a cluster or node group wide configuration.
+If a node is setup without parameters, it should use the settings from the parent node group or cluster. If none
+are given there, defaults should be used.
+
+As a first step, this will be implemented for using 1 ethernet device only. Functions for nic bonding will be added
+later on.
+
 Configuration changes for VLANs
 +++++++++++++++++++++++++++++++
 nicparams shall be extended by a value "vlan" that will store the VLAN information for each NIC.
@@ -55,10 +78,12 @@ Support for KVM will be added in the future.
 Example:
 switch1 will connect the VM to the default VLAN of the switch1.
 switch1.3 means that the VM is connected to an access port of VLAN 3.
-switch1.2:10:20 means that the VM is connected to a trunk port on switch1, carrying VLANs 2, 10 and 20.
+switch1.2:10:20 means that the VM is connected to a hybrid port on switch1, carrying VLANs 2 untagged and 
+VLANs 10 and 20 tagged.
+switch1:44:55 means that the VM is connected to a trunk port on switch1, carrying VLANS 44 and 55
 
-This configuration string is split at the dot and stored in nicparams[constants.NIC_LINK] and
-nicparams[constants.NIC_VLAN] respectively.
+This configuration string is split at the dot or colon respectively and stored in nicparams[constants.NIC_LINK] 
+and nicparams[constants.NIC_VLAN] respectively. Dot or colon are stored as well in nicparams[constants.NIC_VLAN].
 
 For Xen hypervisors, this information can be concatenated again and stored in the vif config as
 the bridge parameter and will be fully compatible with vif-openvswitch as of Xen 4.3.
@@ -66,12 +91,13 @@ the bridge parameter and will be fully compatible with vif-openvswitch as of Xen
 Users of older Xen versions should be able to grab vif-openvswitch from the Xen repo and use it
 (tested in 4.2).
 
-The differentiation between access port and trunk port is given by the number of VLANs that are
-specified.
-
 gnt-instance modify shall be able to add or remove single VLANs from the vlan string without users needing
 to specify the complete new string.
 
+NIC bonding
++++++++++++
+To be done
+
 Configuration changes for QoS
 +++++++++++++++++++++++++++++
 Instances shall be extended with configuration options for
diff --git a/doc/design-optables.rst b/doc/design-optables.rst
new file mode 100644 (file)
index 0000000..db24690
--- /dev/null
@@ -0,0 +1,191 @@
+==========================================
+Filtering of jobs for the Ganeti job queue
+==========================================
+
+.. contents:: :depth: 4
+
+This is a design document detailing the semantics of the fine-grained control
+of jobs in Ganeti. For the implementation there will be a separate
+design document that also describes the vision for the Ganeti daemon
+structure.
+
+
+Current state and shortcomings
+==============================
+
+Control of the Ganeti job queue is quite limited. There is a single
+status bit, the "drained flag". If set, no new jobs are accepted to
+the queue. This is too coarse for some use cases.
+
+- The queue might be required to be drained for several reasons,
+  initiated by different persons or automatic programs. Each one
+  should be able to indicate that his reasons for draining are over
+  without affecting the others.
+
+- There is no support for partial drains. For example, one might want
+  to allow all jobs belonging to a manual (or externally coordinated)
+  maintenance, while disallowing all other jobs.
+
+- There is no support for blocking jobs by their op-codes, e.g.,
+  disallowing all jobs that bring new instances to a cluster. This might
+  be part of a maintenance preparation.
+
+- There is no support for a soft version of draining, where all
+  jobs currently in the queue are finished, while new jobs entering
+  the queue are delayed until the drain is over.
+
+
+Proposed changes
+================
+
+We propose to add filters on the job queue. These will be part of the
+configuration and as such are persisted with it. Conceptionally, the
+filters are always processed when a job enters the queue and while it
+is still in the queue. Of course, in the implementation, reevaluation
+is only carried out, if something could make the result change, e.g.,
+a new job is entered to the queue, or the filter rules are changed.
+There is no distinction between filter processing when a job is about
+to enter the queue and while it is in the queue, as this can be
+expressed by the filter rules themselves (see predicates below).
+
+Format of a Filter rule
+-----------------------
+
+Filter rules are given by the following data.
+
+- A UUID. This ensures that there can be different filter rules
+  that otherwise have all parameters equal. In this way, multiple
+  drains for different reasons are possible. The UUID is used to
+  address the filter rule, in particular for deletion.
+
+  If no UUID is provided at rule addition, Ganeti will create one.
+
+- The watermark. This is the highest job id ever used, as valid in
+  the moment when the filter was added. This data will be added
+  automatically upon addition of the filter.
+
+- A priority. This is a non-negative integer. Filters are processed
+  in order of increasing priority until a rule applies. While there
+  is a well-defined order in which rules of the same priority are
+  evaluated (increasing watermark, then the uuid, are taken as tie
+  breakers), it is not recommended to have rules of the same priority
+  that overlap and have different actions associated.
+
+- A list of predicates. The rule fires, if all of them hold true
+  for the job.
+
+- An action. For the time being, one of the following, but more
+  actions might be added in the future (in particular, future
+  implementations might add an action making filtering continue with
+  a different filter chain).
+
+  - ACCEPT. The job will be accepted; no further filter rules
+    are applied.
+  - PAUSE. The job will be accepted to the queue and remain there;
+    however, it is not executed. If an opcode is currently running,
+    it continues, but the next opcode will not be started. For a paused
+    job all locks it might have acquired will be released as soon as
+    possible, at the latest when the currently running opcode has
+    finished. The job queue will take care of this.
+  - REJECT. The job is rejected. If it is already in the queue,
+    it will be marked as cancelled.
+  - CONTINUE. The filtering continues processing with the next
+    rule. Such a rule will never have any direct or indirect effect,
+    but it can serve as documentation for a "normally present, but
+    currently disabled" rule.
+
+- A reason trail, in the same format as reason trails for opcodes. 
+  This allows to find out, which maintenance (or other reason) caused
+  the addition of this filter rule.
+
+Predicates available for the filter rules
+-----------------------------------------
+
+A predicate is a list, with the first element being the name of the
+predicate and the rest being parameters suitable for that predicate.
+In most cases, the name of the predicate will be a field of a job,
+and there will be a single parameter, which is a boolean expression
+(``filter``) in the sense
+of the Ganeti query language. However, no assumption should be made
+that all predicates are of this shape. More predicates may be added
+in the future.
+
+- ``jobid``. Only parameter is a boolean expression. For this expression,
+  there is only one field available, ``id``, which represents the id the job to be
+  filtered. In all value positions, the string ``watermark`` will be
+  replaced by the value of the watermark.
+
+- ``opcode``. Only parameter is boolean expresion. For this expression, ``OP_ID``
+  and all other fields present in the opcode are available. This predicate
+  will hold true, if the expression is true for at least one opcode in
+  the job.
+
+- ``reason``. Only parameter is a boolean expression. For this expression, the three
+  fields ``source``, ``reason``, ``timestamp`` of reason trail entries
+  are available. This predicate is true, if one of the entries of one
+  of the opcodes in this job satisfies the expression.
+
+
+Examples
+========
+
+Draining the queue.
+::
+
+   {'priority': 0,
+    'predicates': [['jobid', ['>', 'id', 'watermark']]],
+    'action': 'REJECT'}
+
+Soft draining could be achieved by replacing ``REJECT`` by ``PAUSE`` in the
+above example.
+
+Pausing all new jobs not belonging to a specific maintenance.
+::
+
+   {'priority': 1,
+    'predicates': [['jobid', ['>', 'id', 'watermark']],
+                   ['reason', ['!', ['=~', 'reason', 'maintenance pink bunny']]]],
+    'action': 'PAUSE'}
+
+Canceling all queued instance creations and disallowing new such jobs.
+::
+
+  {'priority': 1,
+   'predicates': [['opcode', ['=', 'OP_ID', 'OP_INSTANCE_CREATE']]],
+   'action': 'REJECT'}
+
+
+
+Interface
+=========
+
+Since queue control is intended to be used by external maintenance-handling
+tools as well, the primary interface for manipulating queue filters is the
+:doc:`rapi`. For convenience, a command-line interface will be added as well.
+
+The following resources will be added.
+
+- /2/filters/
+
+  - GET returns the list of all currently set filters
+
+  - POST adds a new filter
+
+- /2/filters/[uuid]
+
+  - GET returns the description of the specified filter
+
+  - DELETE removes the specified filter
+
+  - PUT replaces the specified filter rule, or creates it,
+    if it doesn't exist already.
+
+Security considerations
+=======================
+
+Filtering of jobs is not a security feature. It merely serves the purpose
+of coordinating efforts and avoiding accidental conflicting
+jobs. Everybody with appropriate credentials can modify the filter
+rules, not just the originator of a rule. To avoid accidental
+lock-out, requests modifying the queue are executed directly and not
+going through the queue themselves.
index ed4721f..c6b48f1 100644 (file)
@@ -52,8 +52,8 @@ template is not enabled by the cluster.
 The ipolicy also contains a list of enabled disk templates. Since the cluster-
 wide enabled disk templates should be a stronger constraint, the list of
 enabled disk templates in the ipolicy should be a subset of those. In case the
-user tries to create an inconsistent situation here, gnt-cluster should emit
-a warning.
+user tries to create an inconsistent situation here, gnt-cluster should
+display this as an error.
 
 We consider the first disk template in the list to be the default template for
 instance creation and storage reporting. This will remove the need to specify
@@ -71,6 +71,9 @@ group name on cluster initialization and can only be disabled by explicitly
 using the option ``--no-lvm-storage``. This will be replaced by adding/removing
 ``drbd`` and ``plain`` from the set of enabled disk templates.
 
+The option ``--no-drbd-storage`` is also subsumed by dis/enabling the
+disk template ``drbd`` on the cluster.
+
 Up till now, file storage and shared file storage could be dis/enabled at
 ``./configure`` time. This will also be replaced by adding/removing the
 respective disk templates from the set of enabled disk templates.
@@ -89,7 +92,6 @@ based on the current configuration of the cluster. We propose the following
 update logic to be implemented in the online update of the config in
 the ``Cluster`` class in ``objects.py``:
 - If a ``volume_group_name`` is existing, then enable ``drbd`` and ``plain``.
-(TODO: can we narrow that down further?)
 - If ``file`` or ``sharedfile`` was enabled at configure time, add the
 respective disk template to the list of enabled disk templates.
 - For disk templates ``diskless``, ``blockdev``, ``ext``, and ``rbd``, we
@@ -139,18 +141,19 @@ type is currently only used to enable the user to mark it as (un)allocatable.
 unit that is of type ``lvm-pv`` directly, therefore it is not included in the
 mapping.
 
-The storage reporting for file storage will report space on the file storage
-dir, which is currently limited to one directory. In the future, if we'll have
-support for more directories, or for per-nodegroup directories this can be
-changed.
+The storage reporting for file and sharedfile storage will report space
+on the file storage dir, which is currently limited to one directory.
+In the future, if we'll have support for more directories, or for per-nodegroup
+directories this can be changed.
 
-For now, we will implement only the storage reporting for non-shared storage,
-that is disk templates ``file``, ``lvm``, and ``drbd``. For disk template
-``diskless``, there is obviously nothing to report about. When implementing
-storage reporting for file, we can also use it for ``sharedfile``, since it
-uses the same file system mechanisms to determine the free space. In the
-future, we can optimize storage reporting for shared storage by not querying
-all nodes that use a common shared file for the same space information.
+For now, we will implement only the storage reporting for lvm-based and
+file-based storage, that is disk templates ``file``, ``sharedfile``, ``lvm``,
+and ``drbd``. For disk template ``diskless``, there is obviously nothing to
+report about. When implementing storage reporting for file, we can also use
+it for ``sharedfile``, since it uses the same file system mechanisms to
+determine the free space. In the future, we can optimize storage reporting
+for shared storage by not querying all nodes that use a common shared file
+for the same space information.
 
 In the future, we extend storage reporting for shared storage types like
 ``rados`` and ``ext``. Note that it will not make sense to query each node for
@@ -221,6 +224,20 @@ one of the disk templates. There can be more than one storage pool based on the
 same disk template, therefore we will then start referencing the storage pool
 name instead of the disk template.
 
+Note: As of version 2.10, ``gnt-node list`` only reports storage space
+information for the default disk template, as supporting more options
+turned out to be not feasible without storage pools.
+
+Besides in ``gnt-node list``, storage space information is also
+displayed in ``gnt-node list-storage``. This will also adapt to the
+extended storage reporting capabilities. The user can specify a storage
+type using ``--storage-type``. If he requests storage information about
+a storage type which does not support space reporting, a warning is
+emitted. If no storage type is specified explicitely, ``gnt-node
+list-storage`` will try to report storage on the storage type of the
+default disk template. If the default disk template's storage type does
+not support space reporting, an error message is emitted.
+
 ``gnt-cluster info`` will report which disk templates are enabled, i.e.
 which ones are supported according to the cluster configuration. Example
 output::
@@ -245,6 +262,13 @@ made. Note that for DRBD nowadays we ignore the case when vg and metavg
 are different, and we only consider the main volume group. Fixing this is
 outside the scope of this design.
 
+Although the iallocator protocol itself does not need change, the
+invocation of the iallocator needs quite some adaption. So far, it
+always requested LVM storage information no matter if that was the
+disk template to be considered for the allocation. For instance
+allocation, this is the disk template of the instance.
+TODO: consider other allocator requests.
+
 With this design, we ensure forward-compatibility with respect to storage
 pools. For now, we'll report space for all available disk templates that
 are based on non-shared storage types, in the future, for all available
diff --git a/doc/design-upgrade.rst b/doc/design-upgrade.rst
new file mode 100644 (file)
index 0000000..15a88ef
--- /dev/null
@@ -0,0 +1,294 @@
+========================================
+Automatized Upgrade Procedure for Ganeti
+========================================
+
+.. contents:: :depth: 4
+
+This is a design document detailing the proposed changes to the
+upgrade process, in order to allow it to be more automatic.
+
+
+Current state and shortcomings
+==============================
+
+Ganeti requires to run the same version of Ganeti to be run on all
+nodes of a cluster and this requirement is unlikely to go away in the
+foreseeable future. Also, the configuration may change between minor
+versions (and in the past has proven to do so). This requires a quite
+involved manual upgrade process of draining the queue, stopping
+ganeti, changing the binaries, upgrading the configuration, starting
+ganeti, distributing the configuration, and undraining the queue.
+
+
+Proposed changes
+================
+
+While we will not remove the requirement of the same Ganeti
+version running on all nodes, the transition from one version
+to the other will be made more automatic. It will be possible
+to install new binaries ahead of time, and the actual switch
+between versions will be a single command.
+
+While changing the file layout anyway, we install the python
+code, which is architecture independent, under ``${prefix}/share``,
+in a way that properly separates the Ganeti libraries of the
+various versions. 
+
+Path changes to allow multiple versions installed
+-------------------------------------------------
+
+Currently, Ganeti installs to ``${PREFIX}/bin``, ``${PREFIX}/sbin``,
+and so on, as well as to ``${pythondir}/ganeti``.
+
+These paths will be changed in the following way.
+
+- The python package will be installed to
+  ``${PREFIX}/share/ganeti/${VERSION}/ganeti``.
+  Here ${VERSION} is, depending on configure options, either the full qualified
+  version number, consisting of major, minor, revision, and suffix, or it is
+  just a major.minor pair. All python executables will be installed under
+  ``${PREFIX}/share/ganeti/${VERSION}`` so that they see their respective
+  Ganeti library. ``${PREFIX}/share/ganeti/default`` is a symbolic link to
+  ``${sysconfdir}/ganeti/share`` which, in turn, is a symbolic link to
+  ``${PREFIX}/share/ganeti/${VERSION}``. For all python executatables (like
+  ``gnt-cluster``, ``gnt-node``, etc) symbolic links going through
+  ``${PREFIX}/share/ganeti/default`` are added under ``${PREFIX}/sbin``.
+
+- All other files will be installed to the corresponding path under
+  ``${libdir}/ganeti/${VERSION}`` instead of under ``${PREFIX}``
+  directly, where ``${libdir}`` defaults to ``${PREFIX}/lib``.
+  ``${libdir}/ganeti/default`` will be a symlink to ``${sysconfdir}/ganeti/lib``
+  which, in turn, is a symlink to ``${libdir}/ganeti/${VERSION}``.
+  Symbolic links to the files installed under ``${libdir}/ganeti/${VERSION}``
+  will be added under ``${PREFIX}/bin``, ``${PREFIX}/sbin``, and so on. These
+  symbolic links will go through ``${libdir}/ganeti/default`` so that the
+  version can easily be changed by updating the symbolic link in
+  ``${sysconfdir}``.
+
+The set of links for ganeti binaries might change between the versions.
+However, as the file structure under ``${libdir}/ganeti/${VERSION}`` reflects
+that of ``/``, two links of differnt versions will never conflict. Similarly,
+the symbolic links for the python executables will never conflict, as they
+always point to a file with the same basename directly under
+``${PREFIX}/share/ganeti/default``. Therefore, each version will make sure that
+enough symbolic links are present in ``${PREFIX}/bin``, ``${PREFIX}/sbin`` and
+so on, even though some might be dangling, if a differnt version of ganeti is
+currently active.
+
+The extra indirection through ``${sysconfdir}`` allows installations that choose
+to have ``${sysconfdir}`` and ``${localstatedir}`` outside ``${PREFIX}`` to
+mount ``${PREFIX}`` read-only. The latter is important for systems that choose
+``/usr`` as ``${PREFIX}`` and are following the Filesystem Hierarchy Standard.
+For example, choosing ``/usr`` as ``${PREFIX}`` and ``/etc`` as ``${sysconfdir}``,
+the layout for version 2.10 will look as follows.
+::
+
+   /
+   |
+   +-- etc
+   |   |
+   |   +-- ganeti 
+   |         |
+   |         +-- lib -> /usr/lib/ganeti/2.10
+   |         |
+   |         +-- share  -> /usr/share/ganeti/2.10
+   +-- usr
+        |
+        +-- bin
+        |   |
+        |   +-- harep -> /usr/lib/ganeti/default/usr/bin/harep
+        |   |
+        |   ...  
+        |
+        +-- sbin
+        |   |
+        |   +-- gnt-cluster -> /usr/share/ganeti/default/gnt-cluster
+        |   |
+        |   ...  
+        |
+        +-- ...
+        |
+        +-- lib
+        |   |
+        |   +-- ganeti
+        |       |
+        |       +-- default -> /etc/ganeti/lib
+        |       |
+        |       +-- 2.10
+        |           |
+        |           +-- usr
+        |               |
+        |               +-- bin
+        |               |    |
+        |               |    +-- htools
+        |               |    |
+        |               |    +-- harep -> htools
+        |               |    |
+        |               |    ...
+        |               ...
+        |
+        +-- share
+             |
+             +-- ganeti
+                 |
+                 +-- default -> /etc/ganeti/share
+                 |
+                 +-- 2.10
+                     |
+                     + -- gnt-cluster
+                     |
+                     + -- gnt-node
+                     |
+                     + -- ...
+                     |
+                     + -- ganeti
+                          |
+                          +-- backend.py
+                          |
+                          +-- ...
+                          |
+                          +-- cmdlib
+                          |   |
+                          |   ...
+                          ...
+
+
+
+gnt-cluster upgrade
+-------------------
+
+The actual upgrade process will be done by a new command ``upgrade`` to
+``gnt-cluster``. If called with the option ``--to`` which take precisely
+one argument, the version to
+upgrade (or downgrade) to, given as full string with major, minor, suffix,
+and suffix. To be compatible with current configuration upgrade and downgrade
+procedures, the new version must be of the same major version and
+either an equal or higher minor version, or precisely the previous
+minor version.
+
+When executed, ``gnt-cluster upgrade --to=<version>`` will perform the
+following actions.
+
+- It verifies that the version to change to is installed on all nodes
+  of the cluster that are not marked as offline. If this is not the
+  case it aborts with an error. This initial testing is an
+  optimization to allow for early feedback.
+
+- An intent-to-upgrade file is created that contains the current
+  version of ganeti, the version to change to, and the process ID of
+  the ``gnt-cluster upgrade`` process. The latter is not used automatically,
+  but allows manual detection if the upgrade process died
+  unintentionally. The intend-to-upgrade file is persisted to disk
+  before continuing.
+
+- The Ganeti job queue is drained, and the executable waits till there
+  are no more jobs in the queue. Once :doc:`design-optables` is
+  implemented, for upgrades, and only for upgrades, all jobs are paused
+  instead (in the sense that the currently running opcode continues,
+  but the next opcode is not started) and it is continued once all
+  jobs are fully paused.
+
+- All ganeti daemons on the master node are stopped.
+
+- It is verified again that all nodes at this moment not marked as
+  offline have the new version installed. If this is not the case,
+  then all changes so far (stopping ganeti daemons and draining the
+  queue) are undone and failure is reported. This second verification
+  is necessary, as the set of online nodes might have changed during
+  the draining period.
+
+- All ganeti daemons on all remaining (non-offline) nodes are stopped.
+
+- A backup of all Ganeti-related status information is created for
+  manual rollbacks. While the normal way of rolling back after an
+  upgrade should be calling ``gnt-clsuter upgrade`` from the newer version
+  with the older version as argument, a full backup provides an
+  additional safety net, especially for jump-upgrades (skipping
+  intermediate minor versions).
+
+- If the action is a downgrade to the previous minor version, the
+  configuration is downgraded now, using ``cfgupgrade --downgrade``.
+
+- The ``${sysconfdir}/ganeti/lib`` and ``${sysconfdir}/ganeti/share``
+  symbolic links are updated.
+
+- If the action is an upgrade to a higher minor version, the configuration
+  is upgraded now, using ``cfgupgrade``.
+
+- All daemons are started on all nodes.
+
+- ``ensure-dirs --full-run`` is run on all nodes.
+
+- ``gnt-cluster redist-conf`` is run on the master node. 
+
+- All daemons are restarted on all nodes.
+
+- The Ganeti job queue is undrained.
+
+- The intent-to-upgrade file is removed.
+
+- ``gnt-cluster verify`` is run and the result reported.
+
+
+Considerations on unintended reboots of the master node
+=======================================================
+During the upgrade procedure, the only ganeti process still running is
+the one instance of ``gnt-cluster upgrade``. This process is also responsible
+for eventually removing the queue drain. Therefore, we have to provide
+means to resume this process, if it dies unintentionally. The process
+itself will handle SIGTERM gracefully by either undoing all changes
+done so far, or by ignoring the signal all together and continuing to
+the end; the choice between these behaviors depends on whether change
+of the configuration has already started (in which case it goes
+through to the end), or not (in which case the actions done so far are
+rolled back).
+
+To achieve this, ``gnt-cluster upgrade`` will support a ``--resume``
+option. It is recommended
+to have ``gnt-cluster upgrade --resume`` as an at-reboot task in the crontab.
+The ``gnt-cluster upgrade --resume`` comand first verifies that
+it is running on the master node, using the same requirement as for
+starting the master daemon, i.e., confirmed by a majority of all
+nodes. If it is not the master node, it will remove any possibly
+existing intend-to-upgrade file and exit. If it is running on the
+master node, it will check for the existence of an intend-to-upgrade
+file. If no such file is found, it will simply exit. If found, it will
+resume at the appropriate stage.
+
+- If the configuration file still is at the initial version,
+  ``gnt-cluster upgrade`` is resumed at the step immediately following the
+  writing of the intend-to-upgrade file. It should be noted that
+  all steps before changing the configuration are idempotent, so
+  redoing them does not do any harm.
+
+- If the configuration is already at the new version, all daemons on
+  all nodes are stopped (as they might have been started again due
+  to a reboot) and then it is resumed at the step immediately
+  following the configuration change. All actions following the
+  configuration change can be repeated without bringing the cluster
+  into a worse state.
+
+
+Caveats
+=======
+
+Since ``gnt-cluster upgrade`` drains the queue and undrains it later, so any
+information about a previous drain gets lost. This problem will
+disappear, once :doc:`design-optables` is implemented, as then the
+undrain will then be restricted to filters by gnt-upgrade.
+
+
+Requirement of opcode backwards compatibility
+==============================================
+
+Since for upgrades we only pause jobs and do not fully drain the
+queue, we need to be able to transform the job queue into a queue for
+the new version. The way this is achieved is by keeping the
+serialization format backwards compatible. This is in line with
+current practice that opcodes do not change between versions, and at
+most new fields are added. Whenever we add a new field to an opcode,
+we will make sure that the deserialization function will provide a
+default value if the field is not present.
+
+
index 98b8d23..bdd0ebe 100644 (file)
@@ -17,6 +17,7 @@ Most dependencies from :doc:`install-quick`, including ``qemu-img``
 - `python-sphinx <http://sphinx.pocoo.org/>`_
   (tested with version 1.1.3)
 - `python-mock <http://www.voidspace.org.uk/python/mock/>`_
+  (tested with version 1.0.1)
 - `graphviz <http://www.graphviz.org/>`_
 - the `en_US.UTF-8` locale must be enabled on the system
 - `pylint <http://www.logilab.org/857>`_ and its associated
@@ -48,13 +49,14 @@ Installation of all dependencies listed here::
 
      $ apt-get install python-setuptools automake git fakeroot
      $ apt-get install pandoc python-epydoc graphviz
-     $ apt-get install python-yaml python-mock
+     $ apt-get install python-yaml
      $ cd / && sudo easy_install \
                sphinx \
                logilab-astng==0.24.1 \
                logilab-common==0.58.3 \
                pylint==0.26.0 \
                pep8==1.3.3 \
+               mock==1.0.1 \
                coverage
 
 For Haskell development, again all things from the quick install
@@ -212,6 +214,10 @@ for example::
 
   $ make hs-shell-{balancing,basic}
 
+Checking for the correct style of the NEWS file is also possible, by running::
+
+  $ make check-news
+
 Packaging notes
 ===============
 
index 1112b21..4ba2d34 100644 (file)
@@ -1,5 +1,8 @@
 PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin
 
+# On reboot, continue a Ganeti upgrade, if one was in progress
+@reboot root @SBINDIR@/gnt-cluster upgrade --resume
+
 # Restart failed instances (every 5 minutes)
 */5 * * * * root [ -x @SBINDIR@/ganeti-watcher ] && @SBINDIR@/ganeti-watcher
 
index a60e4ac..3a2846a 100644 (file)
@@ -1,7 +1,7 @@
 Ganeti customisation using hooks
 ================================
 
-Documents Ganeti version 2.9
+Documents Ganeti version 2.10
 
 .. contents::
 
index f62f549..456d5ee 100644 (file)
@@ -1,7 +1,7 @@
 Ganeti automatic instance allocation
 ====================================
 
-Documents Ganeti version 2.9
+Documents Ganeti version 2.10
 
 .. contents::
 
@@ -325,6 +325,16 @@ As for ``node-evacuate``, it needs the following request arguments:
   instances
     a list of request dicts
 
+MonD data
++++++++++
+
+Additional information is available from mond. Mond's data collectors
+provide information that can help an allocator script make better
+decisions when allocating a new instance. Mond's information may also be
+accessible from a mock file mainly for testing purposes. The file will
+be in JSON format and will present an array of :ref:`report objects
+<monitoring-agent-format-of-the-report>`.
+
 Response message
 ~~~~~~~~~~~~~~~~
 
index 48ae338..7672fa4 100644 (file)
@@ -94,6 +94,7 @@ Implemented designs
    design-2.7.rst
    design-2.8.rst
    design-2.9.rst
+   design-2.10.rst
 
 Draft designs
 -------------
@@ -110,9 +111,11 @@ Draft designs
    design-autorepair.rst
    design-bulk-create.rst
    design-chained-jobs.rst
+   design-cmdlib-unittests.rst
    design-cpu-pinning.rst
    design-device-uuid-name.rst
    design-hroller.rst
+   design-hotplug.rst
    design-linuxha.rst
    design-lu-generated-jobs.rst
    design-monitoring-agent.rst
@@ -120,6 +123,7 @@ Draft designs
    design-network.rst
    design-node-add.rst
    design-oob.rst
+   design-openvswitch.rst
    design-opportunistic-locking.rst
    design-ovf-support.rst
    design-partitioned
@@ -127,6 +131,8 @@ Draft designs
    design-reason-trail.rst
    design-restricted-commands.rst
    design-shared-storage.rst
+   design-storagetypes.rst
+   design-upgrade.rst
    design-virtual-clusters.rst
    devnotes.rst
    glossary.rst
index 04ea7c0..2b4551a 100644 (file)
@@ -367,6 +367,31 @@ above. We recommend using the latest version of ``ceph-common``.
 
       $ apt-get install ceph-common
 
+KVM userspace access
+~~~~~~~~~~~~~~~~~~~~
+
+If your cluster uses a sufficiently new version of KVM (you will need at
+least QEMU 0.14 with RBD support compiled in), you can take advantage of
+KVM's native support for ceph in order to have better performance and
+avoid potential deadlocks_ in low memory scenarios.
+
+.. _deadlocks: http://tracker.ceph.com/issues/3076
+
+To initialize a cluster with support for this feature, use a command
+such as::
+
+  $ gnt-cluster init \
+      --enabled-disk-templates rbd \
+      --ipolicy-disk-templates rbd \
+      --enabled-hypervisors=kvm \
+      -D rbd:access=userspace
+
+(You may want to enable more templates than just ``rbd``.)
+
+You can also change this setting on a live cluster by giving the same
+switches to ``gnt-cluster modify``, or change those settings at the node
+group level with ``gnt-group modify``.
+
 Configuration file
 ~~~~~~~~~~~~~~~~~~
 
index a7f06b7..a6317d6 100644 (file)
@@ -85,6 +85,8 @@ destination-related options default to the source value (e.g. setting
   When moving a single instance: Primary node on destination cluster.
 ``--dest-secondary-node``
   When moving a single instance: Secondary node on destination cluster.
+``--dest-disk-template``
+  Disk template to use after the move. Can be used to change disk templates.
 ``--iallocator``
   Iallocator for creating instance on destination cluster.
 ``--hypervisor-parameters``/``--backend-parameters``/``--os-parameters``/``--net``
index 76a4309..81969e9 100644 (file)
@@ -1,7 +1,7 @@
 Security in Ganeti
 ==================
 
-Documents Ganeti version 2.9
+Documents Ganeti version 2.10
 
 Ganeti was developed to run on internal, trusted systems. As such, the
 security model is all-or-nothing.
index 41e4dba..6b11c03 100644 (file)
@@ -1,7 +1,7 @@
 Virtual cluster support
 =======================
 
-Documents Ganeti version 2.9
+Documents Ganeti version 2.10
 
 .. contents::
 
diff --git a/lib/_constants.py.in b/lib/_constants.py.in
new file mode 100644 (file)
index 0000000..49f444c
--- /dev/null
@@ -0,0 +1,14 @@
+# This file is automatically generated, do not edit!
+#
+
+"""Automatically generated constants for Python
+
+Note that this file is autogenerated with @lib/_constants.py.in@ as a
+header.
+
+"""
+
+# pylint: disable=C0301,C0324
+# because this is autogenerated, we do not want
+# style warnings
+
index 7cc4521..a8e1f01 100644 (file)
 
 """
 
-# pylint: disable=E1103
+# pylint: disable=E1103,C0302
 
 # E1103: %s %r has no %r member (but some types could not be
 # inferred), because the _TryOSFromDisk returns either (True, os_obj)
 # or (False, "string") which confuses pylint
 
+# C0302: This module has become too big and should be split up
+
 
 import os
 import os.path
@@ -1609,6 +1611,28 @@ def _RemoveBlockDevLinks(instance_name, disks):
         logging.exception("Can't remove symlink '%s'", link_name)
 
 
+def _CalculateDeviceURI(instance, disk, device):
+  """Get the URI for the device.
+
+  @type instance: L{objects.Instance}
+  @param instance: the instance which disk belongs to
+  @type disk: L{objects.Disk}
+  @param disk: the target disk object
+  @type device: L{bdev.BlockDev}
+  @param device: the corresponding BlockDevice
+  @rtype: string
+  @return: the device uri if any else None
+
+  """
+  access_mode = disk.params.get(constants.LDP_ACCESS,
+                                constants.DISK_KERNELSPACE)
+  if access_mode == constants.DISK_USERSPACE:
+    # This can raise errors.BlockDeviceError
+    return device.GetUserspaceAccessUri(instance.hypervisor)
+  else:
+    return None
+
+
 def _GatherAndLinkBlockDevs(instance):
   """Set up an instance's block device(s).
 
@@ -1616,9 +1640,9 @@ def _GatherAndLinkBlockDevs(instance):
   devices must be already assembled.
 
   @type instance: L{objects.Instance}
-  @param instance: the instance whose disks we shoul assemble
+  @param instance: the instance whose disks we should assemble
   @rtype: list
-  @return: list of (disk_object, device_path)
+  @return: list of (disk_object, link_name, drive_uri)
 
   """
   block_devices = []
@@ -1633,8 +1657,9 @@ def _GatherAndLinkBlockDevs(instance):
     except OSError, e:
       raise errors.BlockDeviceError("Cannot create block device symlink: %s" %
                                     e.strerror)
+    uri = _CalculateDeviceURI(instance, disk, device)
 
-    block_devices.append((disk, link_name))
+    block_devices.append((disk, link_name, uri))
 
   return block_devices
 
@@ -1941,6 +1966,43 @@ def GetMigrationStatus(instance):
     _Fail("Failed to get migration status: %s", err, exc=True)
 
 
+def HotplugDevice(instance, action, dev_type, device, extra, seq):
+  """Hotplug a device
+
+  Hotplug is currently supported only for KVM Hypervisor.
+  @type instance: L{objects.Instance}
+  @param instance: the instance to which we hotplug a device
+  @type action: string
+  @param action: the hotplug action to perform
+  @type dev_type: string
+  @param dev_type: the device type to hotplug
+  @type device: either L{objects.NIC} or L{objects.Disk}
+  @param device: the device object to hotplug
+  @type extra: string
+  @param extra: extra info used by hotplug code (e.g. disk link)
+  @type seq: int
+  @param seq: the index of the device from master perspective
+  @raise RPCFail: in case instance does not have KVM hypervisor
+
+  """
+  hyper = hypervisor.GetHypervisor(instance.hypervisor)
+  try:
+    hyper.VerifyHotplugSupport(instance, action, dev_type)
+  except errors.HotplugError, err:
+    _Fail("Hotplug is not supported: %s", err)
+
+  if action == constants.HOTPLUG_ACTION_ADD:
+    fn = hyper.HotAddDevice
+  elif action == constants.HOTPLUG_ACTION_REMOVE:
+    fn = hyper.HotDelDevice
+  elif action == constants.HOTPLUG_ACTION_MODIFY:
+    fn = hyper.HotModDevice
+  else:
+    assert action in constants.HOTPLUG_ALL_ACTIONS
+
+  return fn(instance, dev_type, device, extra, seq)
+
+
 def BlockdevCreate(disk, size, owner, on_primary, info, excl_stor):
   """Creates a block device for an instance.
 
@@ -2116,10 +2178,18 @@ def BlockdevRemove(disk):
     rdev = None
   if rdev is not None:
     r_path = rdev.dev_path
-    try:
-      rdev.Remove()
-    except errors.BlockDeviceError, err:
-      msgs.append(str(err))
+
+    def _TryRemove():
+      try:
+        rdev.Remove()
+        return []
+      except errors.BlockDeviceError, err:
+        return [str(err)]
+
+    msgs.extend(utils.SimpleRetry([], _TryRemove,
+                                  constants.DISK_REMOVE_RETRY_INTERVAL,
+                                  constants.DISK_REMOVE_RETRY_TIMEOUT))
+
     if not msgs:
       DevCacheManager.RemoveCache(r_path)
 
@@ -2193,23 +2263,28 @@ def BlockdevAssemble(disk, owner, as_primary, idx):
   This is a wrapper over _RecursiveAssembleBD.
 
   @rtype: str or boolean
-  @return: a C{/dev/...} path for primary nodes, and
-      C{True} for secondary nodes
+  @return: a tuple with the C{/dev/...} path and the created symlink
+      for primary nodes, and (C{True}, C{True}) for secondary nodes
 
   """
   try:
     result = _RecursiveAssembleBD(disk, owner, as_primary)
     if isinstance(result, BlockDev):
       # pylint: disable=E1103
-      result = result.dev_path
+      dev_path = result.dev_path
+      link_name = None
       if as_primary:
-        _SymlinkBlockDev(owner, result, idx)
+        link_name = _SymlinkBlockDev(owner, dev_path, idx)
+    elif result:
+      return result, result
+    else:
+      _Fail("Unexpected result from _RecursiveAssembleBD")
   except errors.BlockDeviceError, err:
     _Fail("Error while assembling disk: %s", err, exc=True)
   except OSError, err:
     _Fail("Error while symlinking disk: %s", err, exc=True)
 
-  return result
+  return dev_path, link_name
 
 
 def BlockdevShutdown(disk):
@@ -2849,7 +2924,7 @@ def OSEnvironment(instance, inst_os, debug=0):
       result["DISK_%d_BACKEND_TYPE" % idx] = "block"
     elif disk.dev_type in [constants.DT_FILE, constants.DT_SHARED_FILE]:
       result["DISK_%d_BACKEND_TYPE" % idx] = \
-        "file:%s" % disk.physical_id[0]
+        "file:%s" % disk.logical_id[0]
 
   # NICs
   for idx, nic in enumerate(instance.nics):
@@ -3074,7 +3149,7 @@ def FinalizeExport(instance, snap_disks):
       config.set(constants.INISECT_INS, "disk%d_ivname" % disk_count,
                  ("%s" % disk.iv_name))
       config.set(constants.INISECT_INS, "disk%d_dump" % disk_count,
-                 ("%s" % disk.physical_id[1]))
+                 ("%s" % disk.logical_id[1]))
       config.set(constants.INISECT_INS, "disk%d_size" % disk_count,
                  ("%d" % disk.size))
 
@@ -3157,11 +3232,9 @@ def BlockdevRename(devlist):
   """Rename a list of block devices.
 
   @type devlist: list of tuples
-  @param devlist: list of tuples of the form  (disk,
-      new_logical_id, new_physical_id); disk is an
-      L{objects.Disk} object describing the current disk,
-      and new logical_id/physical_id is the name we
-      rename it to
+  @param devlist: list of tuples of the form  (disk, new_unique_id); disk is
+      an L{objects.Disk} object describing the current disk, and new
+      unique_id is the name we rename it to
   @rtype: boolean
   @return: True if all renames succeeded, False otherwise
 
@@ -3632,8 +3705,6 @@ def _GetImportExportIoCommand(instance, mode, ieio, ieargs):
 
     assert isinstance(disk_index, (int, long))
 
-    real_disk = _OpenRealBD(disk)
-
     inst_os = OSFromDisk(instance.os)
     env = OSEnvironment(instance, inst_os)
 
@@ -3643,6 +3714,7 @@ def _GetImportExportIoCommand(instance, mode, ieio, ieargs):
       script = inst_os.import_script
 
     elif mode == constants.IEM_EXPORT:
+      real_disk = _OpenRealBD(disk)
       env["EXPORT_DEVICE"] = real_disk.dev_path
       env["EXPORT_INDEX"] = str(disk_index)
       script = inst_os.export_script
@@ -3870,35 +3942,31 @@ def CleanupImportExport(name):
   shutil.rmtree(status_dir, ignore_errors=True)
 
 
-def _SetPhysicalId(target_node_uuid, nodes_ip, disks):
-  """Sets the correct physical ID on all passed disks.
-
-  """
-  for cf in disks:
-    cf.SetPhysicalID(target_node_uuid, nodes_ip)
+def _FindDisks(disks):
+  """Finds attached L{BlockDev}s for the given disks.
 
+  @type disks: list of L{objects.Disk}
+  @param disks: the disk objects we need to find
 
-def _FindDisks(target_node_uuid, nodes_ip, disks):
-  """Sets the physical ID on disks and returns the block devices.
+  @return: list of L{BlockDev} objects or C{None} if a given disk
+           was not found or was no attached.
 
   """
-  _SetPhysicalId(target_node_uuid, nodes_ip, disks)
-
   bdevs = []
 
-  for cf in disks:
-    rd = _RecursiveFindBD(cf)
+  for disk in disks:
+    rd = _RecursiveFindBD(disk)
     if rd is None:
-      _Fail("Can't find device %s", cf)
+      _Fail("Can't find device %s", disk)
     bdevs.append(rd)
   return bdevs
 
 
-def DrbdDisconnectNet(target_node_uuid, nodes_ip, disks):
+def DrbdDisconnectNet(disks):
   """Disconnects the network on a list of drbd devices.
 
   """
-  bdevs = _FindDisks(target_node_uuid, nodes_ip, disks)
+  bdevs = _FindDisks(disks)
 
   # disconnect disks
   for rd in bdevs:
@@ -3909,12 +3977,11 @@ def DrbdDisconnectNet(target_node_uuid, nodes_ip, disks):
             err, exc=True)
 
 
-def DrbdAttachNet(target_node_uuid, nodes_ip, disks, instance_name,
-                  multimaster):
+def DrbdAttachNet(disks, instance_name, multimaster):
   """Attaches the network on a list of drbd devices.
 
   """
-  bdevs = _FindDisks(target_node_uuid, nodes_ip, disks)
+  bdevs = _FindDisks(disks)
 
   if multimaster:
     for idx, rd in enumerate(bdevs):
@@ -3972,7 +4039,7 @@ def DrbdAttachNet(target_node_uuid, nodes_ip, disks, instance_name,
         _Fail("Can't change to primary mode: %s", err)
 
 
-def DrbdWaitSync(target_node_uuid, nodes_ip, disks):
+def DrbdWaitSync(disks):
   """Wait until DRBDs have synchronized.
 
   """
@@ -3982,7 +4049,7 @@ def DrbdWaitSync(target_node_uuid, nodes_ip, disks):
       raise utils.RetryAgain()
     return stats
 
-  bdevs = _FindDisks(target_node_uuid, nodes_ip, disks)
+  bdevs = _FindDisks(disks)
 
   min_resync = 100
   alldone = True
@@ -4002,11 +4069,10 @@ def DrbdWaitSync(target_node_uuid, nodes_ip, disks):
   return (alldone, min_resync)
 
 
-def DrbdNeedsActivation(target_node_uuid, nodes_ip, disks):
+def DrbdNeedsActivation(disks):
   """Checks which of the passed disks needs activation and returns their UUIDs.
 
   """
-  _SetPhysicalId(target_node_uuid, nodes_ip, disks)
   faulty_disks = []
 
   for disk in disks:
@@ -4259,6 +4325,33 @@ def SetWatcherPause(until, _filename=pathutils.WATCHER_PAUSEFILE):
     utils.WriteFile(_filename, data="%d\n" % (until, ), mode=0644)
 
 
+def ConfigureOVS(ovs_name, ovs_link):
+  """Creates a OpenvSwitch on the node.
+
+  This function sets up a OpenvSwitch on the node with given name nad
+  connects it via a given eth device.
+
+  @type ovs_name: string
+  @param ovs_name: Name of the OpenvSwitch to create.
+  @type ovs_link: None or string
+  @param ovs_link: Ethernet device for outside connection (can be missing)
+
+  """
+  # Initialize the OpenvSwitch
+  result = utils.RunCmd(["ovs-vsctl", "add-br", ovs_name])
+  if result.failed:
+    _Fail("Failed to create openvswitch. Script return value: %s, output: '%s'"
+          % (result.exit_code, result.output), log=True)
+
+  # And connect it to a physical interface, if given
+  if ovs_link:
+    result = utils.RunCmd(["ovs-vsctl", "add-port", ovs_name, ovs_link])
+    if result.failed:
+      _Fail("Failed to connect openvswitch to  interface %s. Script return"
+            " value: %s, output: '%s'" % (ovs_link, result.exit_code,
+            result.output), log=True)
+
+
 class HooksRunner(object):
   """Hook runner.
 
index 1b862c5..eacd8f2 100644 (file)
@@ -472,6 +472,32 @@ def _RestrictIpolicyToEnabledDiskTemplates(ipolicy, enabled_disk_templates):
   ipolicy[constants.IPOLICY_DTS] = restricted_disk_templates
 
 
+def _InitCheckDrbdHelper(drbd_helper, drbd_enabled):
+  """Checks the DRBD usermode helper.
+
+  @type drbd_helper: string
+  @param drbd_helper: name of the DRBD usermode helper that the system should
+    use
+
+  """
+  if not drbd_enabled:
+    return
+
+  if drbd_helper is not None:
+    try:
+      curr_helper = drbd.DRBD8.GetUsermodeHelper()
+    except errors.BlockDeviceError, err:
+      raise errors.OpPrereqError("Error while checking drbd helper"
+                                 " (disable drbd with --enabled-disk-templates"
+                                 " if you are not using drbd): %s" % str(err),
+                                 errors.ECODE_ENVIRON)
+    if drbd_helper != curr_helper:
+      raise errors.OpPrereqError("Error: requiring %s as drbd helper but %s"
+                                 " is the current helper" % (drbd_helper,
+                                                             curr_helper),
+                                 errors.ECODE_INVAL)
+
+
 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,
@@ -569,19 +595,8 @@ def InitCluster(cluster_name, mac_prefix, # pylint: disable=R0913, R0914
     if vgstatus:
       raise errors.OpPrereqError("Error: %s" % vgstatus, errors.ECODE_INVAL)
 
-  if drbd_helper is not None:
-    try:
-      curr_helper = drbd.DRBD8.GetUsermodeHelper()
-    except errors.BlockDeviceError, err:
-      raise errors.OpPrereqError("Error while checking drbd helper"
-                                 " (specify --no-drbd-storage if you are not"
-                                 " using drbd): %s" % str(err),
-                                 errors.ECODE_ENVIRON)
-    if drbd_helper != curr_helper:
-      raise errors.OpPrereqError("Error: requiring %s as drbd helper but %s"
-                                 " is the current helper" % (drbd_helper,
-                                                             curr_helper),
-                                 errors.ECODE_INVAL)
+  drbd_enabled = constants.DT_DRBD8 in enabled_disk_templates
+  _InitCheckDrbdHelper(drbd_helper, drbd_enabled)
 
   logging.debug("Stopping daemons (if any are running)")
   result = utils.RunCmd([pathutils.DAEMON_UTIL, "stop-all"])
@@ -599,11 +614,14 @@ def InitCluster(cluster_name, mac_prefix, # pylint: disable=R0913, R0914
     raise errors.OpPrereqError("Invalid mac prefix given '%s'" % mac_prefix,
                                errors.ECODE_INVAL)
 
-  result = utils.RunCmd(["ip", "link", "show", "dev", master_netdev])
-  if result.failed:
-    raise errors.OpPrereqError("Invalid master netdev given (%s): '%s'" %
-                               (master_netdev,
-                                result.output.strip()), errors.ECODE_INVAL)
+  if not nicparams.get('mode', None) == "openvswitch":
+    # Do not do this check if mode=openvswitch, since the openvswitch is not
+    # created yet
+    result = utils.RunCmd(["ip", "link", "show", "dev", master_netdev])
+    if result.failed:
+      raise errors.OpPrereqError("Invalid master netdev given (%s): '%s'" %
+                                 (master_netdev,
+                                  result.output.strip()), errors.ECODE_INVAL)
 
   dirs = [(pathutils.RUN_DIR, constants.RUN_DIRS_MODE)]
   utils.EnsureDirs(dirs)
index 55e67cb..f9c71d7 100644 (file)
@@ -46,17 +46,18 @@ except (AttributeError, ValueError, KeyError), err:
   # Normally the "manpage" role is registered by sphinx/roles.py
   raise Exception("Can't find reST role named 'manpage': %s" % err)
 
+from ganeti import _constants
 from ganeti import constants
 from ganeti import compat
 from ganeti import errors
 from ganeti import utils
 from ganeti import opcodes
+from ganeti import opcodes_base
 from ganeti import ht
 from ganeti import rapi
 from ganeti import luxi
 from ganeti import objects
 from ganeti import http
-from ganeti import _autoconf
 
 import ganeti.rapi.rlib2 # pylint: disable=W0611
 import ganeti.rapi.connector # pylint: disable=W0611
@@ -83,7 +84,7 @@ def _GetCommonParamNames():
   names = set(map(compat.fst, opcodes.OpCode.OP_PARAMS))
 
   # The "depends" attribute should be listed
-  names.remove(opcodes.DEPEND_ATTR)
+  names.remove(opcodes_base.DEPEND_ATTR)
 
   return names
 
@@ -164,8 +165,8 @@ def _BuildOpcodeParams(op_id, include, exclude, alias):
     if include is not None and name not in include:
       continue
 
-    has_default = default is not ht.NoDefault
-    has_test = not (test is None or test is ht.NoType)
+    has_default = default is not None or default is not ht.NoDefault
+    has_test = test is not None
 
     buf = StringIO()
     buf.write("``%s``" % (rapi_name,))
@@ -222,7 +223,10 @@ class OpcodeParams(s_compat.Directive):
     alias = self.options.get("alias", {})
 
     path = op_id
-    include_text = "\n".join(_BuildOpcodeParams(op_id, include, exclude, alias))
+    include_text = "\n\n".join(_BuildOpcodeParams(op_id,
+                                                  include,
+                                                  exclude,
+                                                  alias))
 
     # Inject into state machine
     include_lines = docutils.statemachine.string2lines(include_text, _TAB_WIDTH,
@@ -381,7 +385,7 @@ class _ManPageXRefRole(sphinx.roles.XRefRole):
     name = m.group("name")
     section = int(m.group("section"))
 
-    wanted_section = _autoconf.MAN_PAGES.get(name, None)
+    wanted_section = _constants.MAN_PAGES.get(name, None)
 
     if not (wanted_section is None or wanted_section == section):
       raise ReSTError("Referenced man page '%s' has section number %s, but the"
index d1ac10d..2b35665 100644 (file)
@@ -95,6 +95,7 @@ __all__ = [
   "GLOBAL_FILEDIR_OPT",
   "HID_OS_OPT",
   "GLOBAL_SHARED_FILEDIR_OPT",
+  "HOTPLUG_OPT",
   "HVLIST_OPT",
   "HVOPTS_OPT",
   "HYPERVISOR_OPT",
@@ -135,7 +136,6 @@ __all__ = [
   "NODEGROUP_OPT",
   "NODE_PARAMS_OPT",
   "NODE_POWERED_OPT",
-  "NODRBD_STORAGE_OPT",
   "NOHDR_OPT",
   "NOIPCHECK_OPT",
   "NO_INSTALL_OPT",
@@ -234,6 +234,7 @@ __all__ = [
   "ParseTimespec",
   "RunWhileClusterStopped",
   "SubmitOpCode",
+  "SubmitOpCodeToDrainedQueue",
   "SubmitOrSend",
   "UsesRPC",
   # Formatting functions
@@ -464,7 +465,7 @@ def _ExtractTagsObject(opts, args):
     raise errors.ProgrammerError("tag_type not passed to _ExtractTagsObject")
   kind = opts.tag_type
   if kind == constants.TAG_CLUSTER:
-    retval = kind, None
+    retval = kind, ""
   elif kind in (constants.TAG_NODEGROUP,
                 constants.TAG_NODE,
                 constants.TAG_NETWORK,
@@ -1467,10 +1468,6 @@ DRBD_HELPER_OPT = cli_option("--drbd-usermode-helper", dest="drbd_helper",
                              action="store", default=None,
                              help="Specifies usermode helper for DRBD")
 
-NODRBD_STORAGE_OPT = cli_option("--no-drbd-storage", dest="drbd_storage",
-                                action="store_false", default=True,
-                                help="Disable support for DRBD")
-
 PRIMARY_IP_VERSION_OPT = \
     cli_option("--primary-ip-version", default=constants.IP4_VERSION,
                action="store", dest="primary_ip_version",
@@ -1645,6 +1642,10 @@ INCLUDEDEFAULTS_OPT = cli_option("--include-defaults", dest="include_defaults",
                                  default=False, action="store_true",
                                  help="Include default values")
 
+HOTPLUG_OPT = cli_option("--hotplug", dest="hotplug",
+                         action="store_true", default=False,
+                         help="Hotplug supported devices (NICs and Disks)")
+
 #: Options provided by all commands
 COMMON_OPTS = [DEBUG_OPT, REASON_OPT]
 
@@ -2279,6 +2280,16 @@ def SubmitOpCode(op, cl=None, feedback_fn=None, opts=None, reporter=None):
   return op_results[0]
 
 
+def SubmitOpCodeToDrainedQueue(op):
+  """Forcefully insert a job in the queue, even if it is drained.
+
+  """
+  cl = GetClient()
+  job_id = cl.SubmitJobToDrainedQueue([op])
+  op_results = PollJob(job_id, cl=cl)
+  return op_results[0]
+
+
 def SubmitOrSend(op, opts, cl=None, feedback_fn=None):
   """Wrapper around SubmitOpCode or SendJob.
 
index f6ef9a6..c28936b 100644 (file)
@@ -27,7 +27,7 @@
 # C0103: Invalid name gnt-cluster
 
 from cStringIO import StringIO
-import os.path
+import os
 import time
 import OpenSSL
 import itertools
@@ -43,7 +43,10 @@ from ganeti import objects
 from ganeti import uidpool
 from ganeti import compat
 from ganeti import netutils
+from ganeti import ssconf
 from ganeti import pathutils
+from ganeti import serializer
+from ganeti import qlang
 
 
 ON_OPT = cli_option("--on", default=False,
@@ -58,6 +61,18 @@ FORCE_FAILOVER = cli_option("--yes-do-it", dest="yes_do_it",
                             help="Override interactive check for --no-voting",
                             default=False, action="store_true")
 
+FORCE_DISTRIBUTION = cli_option("--yes-do-it", dest="yes_do_it",
+                                help="Unconditionally distribute the"
+                                " configuration, even if the queue"
+                                " is drained",
+                                default=False, action="store_true")
+
+TO_OPT = cli_option("--to", default=None, type="string",
+                    help="The Ganeti version to upgrade to")
+
+RESUME_OPT = cli_option("--resume", default=False, action="store_true",
+                        help="Resume any pending Ganeti upgrades")
+
 _EPO_PING_INTERVAL = 30 # 30 seconds between pings
 _EPO_PING_TIMEOUT = 1 # 1 second
 _EPO_REACHABLE_TIMEOUT = 15 * 60 # 15 minutes
@@ -72,10 +87,63 @@ def _CheckNoLvmStorageOptDeprecated(opts):
              " to disable lvm-based storage cluster-wide, use the option"
              " --enabled-disk-templates to disable all of these lvm-base disk "
              "  templates: %s" %
-             utils.CommaJoin(utils.GetLvmDiskTemplates()))
+             utils.CommaJoin(constants.DTS_LVM))
     return 1
 
 
+def _InitEnabledDiskTemplates(opts):
+  """Initialize the list of enabled disk templates.
+
+  """
+  if opts.enabled_disk_templates:
+    return opts.enabled_disk_templates.split(",")
+  else:
+    return constants.DEFAULT_ENABLED_DISK_TEMPLATES
+
+
+def _InitVgName(opts, enabled_disk_templates):
+  """Initialize the volume group name.
+
+  @type enabled_disk_templates: list of strings
+  @param enabled_disk_templates: cluster-wide enabled disk templates
+
+  """
+  vg_name = None
+  if opts.vg_name is not None:
+    vg_name = opts.vg_name
+    if vg_name:
+      if not utils.IsLvmEnabled(enabled_disk_templates):
+        ToStdout("You specified a volume group with --vg-name, but you did not"
+                 " enable any disk template that uses lvm.")
+    elif utils.IsLvmEnabled(enabled_disk_templates):
+      raise errors.OpPrereqError(
+          "LVM disk templates are enabled, but vg name not set.")
+  elif utils.IsLvmEnabled(enabled_disk_templates):
+    vg_name = constants.DEFAULT_VG
+  return vg_name
+
+
+def _InitDrbdHelper(opts, enabled_disk_templates):
+  """Initialize the DRBD usermode helper.
+
+  """
+  drbd_enabled = constants.DT_DRBD8 in enabled_disk_templates
+
+  if not drbd_enabled and opts.drbd_helper is not None:
+    ToStdout("Note: You specified a DRBD usermode helper, while DRBD storage"
+             " is not enabled.")
+
+  if drbd_enabled:
+    if opts.drbd_helper is None:
+      return constants.DEFAULT_DRBD_HELPER
+    if opts.drbd_helper == '':
+      raise errors.OpPrereqError(
+          "Unsetting the drbd usermode helper while enabling DRBD is not"
+          " allowed.")
+
+  return opts.drbd_helper
+
+
 @UsesRPC
 def InitCluster(opts, args):
   """Initialize the cluster.
@@ -90,38 +158,26 @@ def InitCluster(opts, args):
   """
   if _CheckNoLvmStorageOptDeprecated(opts):
     return 1
-  enabled_disk_templates = opts.enabled_disk_templates
-  if enabled_disk_templates:
-    enabled_disk_templates = enabled_disk_templates.split(",")
-  else:
-    enabled_disk_templates = constants.DEFAULT_ENABLED_DISK_TEMPLATES
 
-  vg_name = None
-  if opts.vg_name is not None:
-    vg_name = opts.vg_name
-    if vg_name:
-      if not utils.IsLvmEnabled(enabled_disk_templates):
-        ToStdout("You specified a volume group with --vg-name, but you did not"
-                 " enable any disk template that uses lvm.")
-    else:
-      if utils.IsLvmEnabled(enabled_disk_templates):
-        ToStderr("LVM disk templates are enabled, but vg name not set.")
-        return 1
-  else:
-    if utils.IsLvmEnabled(enabled_disk_templates):
-      vg_name = constants.DEFAULT_VG
+  enabled_disk_templates = _InitEnabledDiskTemplates(opts)
 
-  if not opts.drbd_storage and opts.drbd_helper:
-    ToStderr("Options --no-drbd-storage and --drbd-usermode-helper conflict.")
+  try:
+    vg_name = _InitVgName(opts, enabled_disk_templates)
+    drbd_helper = _InitDrbdHelper(opts, enabled_disk_templates)
+  except errors.OpPrereqError, e:
+    ToStderr(str(e))
     return 1
 
-  drbd_helper = opts.drbd_helper
-  if opts.drbd_storage and not opts.drbd_helper:
-    drbd_helper = constants.DEFAULT_DRBD_HELPER
-
   master_netdev = opts.master_netdev
   if master_netdev is None:
-    master_netdev = constants.DEFAULT_BRIDGE
+    nic_mode = opts.nicparams.get(constants.NIC_MODE, None)
+    if not nic_mode:
+      # default case, use bridging
+      master_netdev = constants.DEFAULT_BRIDGE
+    elif nic_mode == constants.NIC_MODE_OVS:
+      # default ovs is different from default bridge
+      master_netdev = constants.DEFAULT_OVS
+      opts.nicparams[constants.NIC_LINK] = constants.DEFAULT_OVS
 
   hvlist = opts.enabled_hypervisors
   if hvlist is None:
@@ -352,7 +408,10 @@ def RedistributeConfig(opts, args):
 
   """
   op = opcodes.OpClusterRedistConf()
-  SubmitOrSend(op, opts)
+  if opts.yes_do_it:
+    SubmitOpCodeToDrainedQueue(op)
+  else:
+    SubmitOrSend(op, opts)
   return 0
 
 
@@ -964,6 +1023,48 @@ def RenewCrypto(opts, args):
                       opts.force)
 
 
+def _GetEnabledDiskTemplates(opts):
+  """Determine the list of enabled disk templates.
+
+  """
+  if opts.enabled_disk_templates:
+    return opts.enabled_disk_templates.split(",")
+  else:
+    return None
+
+
+def _GetVgName(opts, enabled_disk_templates):
+  """Determine the volume group name.
+
+  @type enabled_disk_templates: list of strings
+  @param enabled_disk_templates: cluster-wide enabled disk-templates
+
+  """
+  # consistency between vg name and enabled disk templates
+  vg_name = None
+  if opts.vg_name is not None:
+    vg_name = opts.vg_name
+  if enabled_disk_templates:
+    if vg_name and not utils.IsLvmEnabled(enabled_disk_templates):
+      ToStdout("You specified a volume group with --vg-name, but you did not"
+               " enable any of the following lvm-based disk templates: %s" %
+               utils.CommaJoin(constants.DTS_LVM))
+  return vg_name
+
+
+def _GetDrbdHelper(opts, enabled_disk_templates):
+  """Determine the DRBD usermode helper.
+
+  """
+  drbd_helper = opts.drbd_helper
+  if enabled_disk_templates:
+    drbd_enabled = constants.DT_DRBD8 in enabled_disk_templates
+    if not drbd_enabled and opts.drbd_helper:
+      ToStdout("You specified a DRBD usermode helper with "
+               " --drbd-usermode-helper while DRBD is not enabled.")
+  return drbd_helper
+
+
 def SetClusterParams(opts, args):
   """Modify the cluster.
 
@@ -974,7 +1075,8 @@ def SetClusterParams(opts, args):
   @return: the desired exit code
 
   """
-  if not (opts.vg_name is not None or opts.drbd_helper or
+  if not (opts.vg_name is not None or
+          opts.drbd_helper is not None or
           opts.enabled_hypervisors or opts.hvparams or
           opts.beparams or opts.nicparams or
           opts.ndparams or opts.diskparams or
@@ -1005,28 +1107,15 @@ def SetClusterParams(opts, args):
   if _CheckNoLvmStorageOptDeprecated(opts):
     return 1
 
-  enabled_disk_templates = None
-  if opts.enabled_disk_templates:
-    enabled_disk_templates = opts.enabled_disk_templates.split(",")
+  enabled_disk_templates = _GetEnabledDiskTemplates(opts)
+  vg_name = _GetVgName(opts, enabled_disk_templates)
 
-  # consistency between vg name and enabled disk templates
-  vg_name = None
-  if opts.vg_name is not None:
-    vg_name = opts.vg_name
-  if enabled_disk_templates:
-    if vg_name and not utils.IsLvmEnabled(enabled_disk_templates):
-      ToStdout("You specified a volume group with --vg-name, but you did not"
-               " enable any of the following lvm-based disk templates: %s" %
-               utils.CommaJoin(utils.GetLvmDiskTemplates()))
-
-  drbd_helper = opts.drbd_helper
-  if not opts.drbd_storage and opts.drbd_helper:
-    ToStderr("Options --no-drbd-storage and --drbd-usermode-helper conflict.")
+  try:
+    drbd_helper = _GetDrbdHelper(opts, enabled_disk_templates)
+  except errors.OpPrereqError, e:
+    ToStderr(str(e))
     return 1
 
-  if not opts.drbd_storage:
-    drbd_helper = ""
-
   hvlist = opts.enabled_hypervisors
   if hvlist is not None:
     hvlist = hvlist.split(",")
@@ -1537,6 +1626,415 @@ def ShowCreateCommand(opts, args):
   ToStdout(_GetCreateCommand(result))
 
 
+def _RunCommandAndReport(cmd):
+  """Run a command and report its output, iff it failed.
+
+  @param cmd: the command to execute
+  @type cmd: list
+  @rtype: bool
+  @return: False, if the execution failed.
+
+  """
+  result = utils.RunCmd(cmd)
+  if result.failed:
+    ToStderr("Command %s failed: %s; Output %s" %
+             (cmd, result.fail_reason, result.output))
+    return False
+  return True
+
+
+def _VerifyCommand(cmd):
+  """Verify that a given command succeeds on all online nodes.
+
+  As this function is intended to run during upgrades, it
+  is implemented in such a way that it still works, if all Ganeti
+  daemons are down.
+
+  @param cmd: the command to execute
+  @type cmd: list
+  @rtype: list
+  @return: the list of node names that are online where
+      the command failed.
+
+  """
+  command = utils.text.ShellQuoteArgs([str(val) for val in cmd])
+
+  nodes = ssconf.SimpleStore().GetOnlineNodeList()
+  master_node = ssconf.SimpleStore().GetMasterNode()
+  cluster_name = ssconf.SimpleStore().GetClusterName()
+
+  # If master node is in 'nodes', make sure master node is at list end
+  if master_node in nodes:
+    nodes.remove(master_node)
+    nodes.append(master_node)
+
+  failed = []
+
+  srun = ssh.SshRunner(cluster_name=cluster_name)
+  for name in nodes:
+    result = srun.Run(name, constants.SSH_LOGIN_USER, command)
+    if result.exit_code != 0:
+      failed.append(name)
+
+  return failed
+
+
+def _VerifyVersionInstalled(versionstring):
+  """Verify that the given version of ganeti is installed on all online nodes.
+
+  Do nothing, if this is the case, otherwise print an appropriate
+  message to stderr.
+
+  @param versionstring: the version to check for
+  @type versionstring: string
+  @rtype: bool
+  @return: True, if the version is installed on all online nodes
+
+  """
+  badnodes = _VerifyCommand(["test", "-d",
+                             os.path.join(pathutils.PKGLIBDIR, versionstring)])
+  if badnodes:
+    ToStderr("Ganeti version %s not installed on nodes %s"
+             % (versionstring, ", ".join(badnodes)))
+    return False
+
+  return True
+
+
+def _GetRunning():
+  """Determine the list of running jobs.
+
+  @rtype: list
+  @return: the number of jobs still running
+
+  """
+  cl = GetClient()
+  qfilter = qlang.MakeSimpleFilter("status",
+                                   frozenset([constants.JOB_STATUS_RUNNING]))
+  return len(cl.Query(constants.QR_JOB, [], qfilter).data)
+
+
+def _SetGanetiVersion(versionstring):
+  """Set the active version of ganeti to the given versionstring
+
+  @type versionstring: string
+  @rtype: list
+  @return: the list of nodes where the version change failed
+
+  """
+  failed = []
+  if constants.HAS_GNU_LN:
+    failed.extend(_VerifyCommand(
+        ["ln", "-s", "-f", "-T",
+         os.path.join(pathutils.PKGLIBDIR, versionstring),
+         os.path.join(pathutils.SYSCONFDIR, "ganeti/lib")]))
+    failed.extend(_VerifyCommand(
+        ["ln", "-s", "-f", "-T",
+         os.path.join(pathutils.SHAREDIR, versionstring),
+         os.path.join(pathutils.SYSCONFDIR, "ganeti/share")]))
+  else:
+    failed.extend(_VerifyCommand(
+        ["rm", "-f", os.path.join(pathutils.SYSCONFDIR, "ganeti/lib")]))
+    failed.extend(_VerifyCommand(
+        ["ln", "-s", "-f", os.path.join(pathutils.PKGLIBDIR, versionstring),
+         os.path.join(pathutils.SYSCONFDIR, "ganeti/lib")]))
+    failed.extend(_VerifyCommand(
+        ["rm", "-f", os.path.join(pathutils.SYSCONFDIR, "ganeti/share")]))
+    failed.extend(_VerifyCommand(
+        ["ln", "-s", "-f", os.path.join(pathutils.SHAREDIR, versionstring),
+         os.path.join(pathutils.SYSCONFDIR, "ganeti/share")]))
+  return list(set(failed))
+
+
+def _ExecuteCommands(fns):
+  """Execute a list of functions, in reverse order.
+
+  @type fns: list of functions.
+  @param fns: the functions to be executed.
+
+  """
+  for fn in reversed(fns):
+    fn()
+
+
+def _GetConfigVersion():
+  """Determine the version the configuration file currently has.
+
+  @rtype: tuple or None
+  @return: (major, minor, revision) if the version can be determined,
+      None otherwise
+
+  """
+  config_data = serializer.LoadJson(utils.ReadFile(pathutils.CLUSTER_CONF_FILE))
+  try:
+    config_version = config_data["version"]
+  except KeyError:
+    return None
+  return utils.SplitVersion(config_version)
+
+
+def _ReadIntentToUpgrade():
+  """Read the file documenting the intent to upgrade the cluster.
+
+  @rtype: string or None
+  @return: the version to upgrade to, if the file exists, and None
+      otherwise.
+
+  """
+  if not os.path.isfile(pathutils.INTENT_TO_UPGRADE):
+    return None
+
+  contentstring = utils.ReadFile(pathutils.INTENT_TO_UPGRADE)
+  contents = utils.UnescapeAndSplit(contentstring)
+  if len(contents) != 2:
+    # file syntactically mal-formed
+    return None
+  return contents[0]
+
+
+def _WriteIntentToUpgrade(version):
+  """Write file documenting the intent to upgrade the cluster.
+
+  @type version: string
+  @param version: the version we intent to upgrade to
+
+  """
+  utils.WriteFile(pathutils.INTENT_TO_UPGRADE,
+                  data=utils.EscapeAndJoin([version, "%d" % os.getpid()]))
+
+
+def _UpgradeBeforeConfigurationChange(versionstring):
+  """
+  Carry out all the tasks necessary for an upgrade that happen before
+  the configuration file, or Ganeti version, changes.
+
+  @type versionstring: string
+  @param versionstring: the version to upgrade to
+  @rtype: (bool, list)
+  @return: tuple of a bool indicating success and a list of rollback tasks
+
+  """
+  rollback = []
+
+  if not _VerifyVersionInstalled(versionstring):
+    return (False, rollback)
+
+  _WriteIntentToUpgrade(versionstring)
+  rollback.append(
+    lambda: utils.RunCmd(["rm", "-f", pathutils.INTENT_TO_UPGRADE]))
+
+  ToStdout("Draining queue")
+  client = GetClient()
+  client.SetQueueDrainFlag(True)
+
+  rollback.append(lambda: GetClient().SetQueueDrainFlag(False))
+
+  if utils.SimpleRetry(0, _GetRunning,
+                       constants.UPGRADE_QUEUE_POLL_INTERVAL,
+                       constants.UPGRADE_QUEUE_DRAIN_TIMEOUT):
+    ToStderr("Failed to completely empty the queue.")
+    return (False, rollback)
+
+  ToStdout("Stopping daemons on master node.")
+  if not _RunCommandAndReport([pathutils.DAEMON_UTIL, "stop-all"]):
+    return (False, rollback)
+
+  if not _VerifyVersionInstalled(versionstring):
+    utils.RunCmd([pathutils.DAEMON_UTIL, "start-all"])
+    return (False, rollback)
+
+  ToStdout("Stopping daemons everywhere.")
+  rollback.append(lambda: _VerifyCommand([pathutils.DAEMON_UTIL, "start-all"]))
+  badnodes = _VerifyCommand([pathutils.DAEMON_UTIL, "stop-all"])
+  if badnodes:
+    ToStderr("Failed to stop daemons on %s." % (", ".join(badnodes),))
+    return (False, rollback)
+
+  backuptar = os.path.join(pathutils.LOCALSTATEDIR,
+                           "lib/ganeti%d.tar" % time.time())
+  ToStdout("Backing up configuration as %s" % backuptar)
+  if not _RunCommandAndReport(["tar", "cf", backuptar,
+                               pathutils.DATA_DIR]):
+    return (False, rollback)
+
+  return (True, rollback)
+
+
+def _SwitchVersionAndConfig(versionstring, downgrade):
+  """
+  Switch to the new Ganeti version and change the configuration,
+  in correct order.
+
+  @type versionstring: string
+  @param versionstring: the version to change to
+  @type downgrade: bool
+  @param downgrade: True, if the configuration should be downgraded
+  @rtype: (bool, list)
+  @return: tupe of a bool indicating success, and a list of
+      additional rollback tasks
+
+  """
+  rollback = []
+  if downgrade:
+    ToStdout("Downgrading configuration")
+    if not _RunCommandAndReport([pathutils.CFGUPGRADE, "--downgrade", "-f"]):
+      return (False, rollback)
+
+  # Configuration change is the point of no return. From then onwards, it is
+  # safer to push through the up/dowgrade than to try to roll it back.
+
+  ToStdout("Switching to version %s on all nodes" % versionstring)
+  rollback.append(lambda: _SetGanetiVersion(constants.DIR_VERSION))
+  badnodes = _SetGanetiVersion(versionstring)
+  if badnodes:
+    ToStderr("Failed to switch to Ganeti version %s on nodes %s"
+             % (versionstring, ", ".join(badnodes)))
+    if not downgrade:
+      return (False, rollback)
+
+  # Now that we have changed to the new version of Ganeti we should
+  # not communicate over luxi any more, as luxi might have changed in
+  # incompatible ways. Therefore, manually call the corresponding ganeti
+  # commands using their canonical (version independent) path.
+
+  if not downgrade:
+    ToStdout("Upgrading configuration")
+    if not _RunCommandAndReport([pathutils.CFGUPGRADE, "-f"]):
+      return (False, rollback)
+
+  return (True, rollback)
+
+
+def _UpgradeAfterConfigurationChange():
+  """
+  Carry out the upgrade actions necessary after switching to the new
+  Ganeti version and updating the configuration.
+
+  As this part is run at a time where the new version of Ganeti is already
+  running, no communication should happen via luxi, as this is not a stable
+  interface. Also, as the configuration change is the point of no return,
+  all actions are pushed trough, even if some of them fail.
+
+  @rtype: int
+  @return: the intended return value
+
+  """
+  returnvalue = 0
+
+  ToStdout("Starting daemons everywhere.")
+  badnodes = _VerifyCommand([pathutils.DAEMON_UTIL, "start-all"])
+  if badnodes:
+    ToStderr("Warning: failed to start daemons on %s." % (", ".join(badnodes),))
+    returnvalue = 1
+
+  ToStdout("Ensuring directories everywhere.")
+  badnodes = _VerifyCommand([pathutils.ENSURE_DIRS])
+  if badnodes:
+    ToStderr("Warning: failed to ensure directories on %s." %
+             (", ".join(badnodes)))
+    returnvalue = 1
+
+  ToStdout("Redistributing the configuration.")
+  if not _RunCommandAndReport(["gnt-cluster", "redist-conf", "--yes-do-it"]):
+    returnvalue = 1
+
+  ToStdout("Restarting daemons everywhere.")
+  badnodes = _VerifyCommand([pathutils.DAEMON_UTIL, "stop-all"])
+  badnodes.extend(_VerifyCommand([pathutils.DAEMON_UTIL, "start-all"]))
+  if badnodes:
+    ToStderr("Warning: failed to start daemons on %s." %
+             (", ".join(list(set(badnodes))),))
+    returnvalue = 1
+
+  ToStdout("Undraining the queue.")
+  if not _RunCommandAndReport(["gnt-cluster", "queue", "undrain"]):
+    returnvalue = 1
+
+  _RunCommandAndReport(["rm", "-f", pathutils.INTENT_TO_UPGRADE])
+
+  ToStdout("Verifying cluster.")
+  if not _RunCommandAndReport(["gnt-cluster", "verify"]):
+    returnvalue = 1
+
+  return returnvalue
+
+
+def UpgradeGanetiCommand(opts, args):
+  """Upgrade a cluster to a new ganeti version.
+
+  @param opts: the command line options selected by the user
+  @type args: list
+  @param args: should be an empty list
+  @rtype: int
+  @return: the desired exit code
+
+  """
+  if ((not opts.resume and opts.to is None)
+      or (opts.resume and opts.to is not None)):
+    ToStderr("Precisely one of the options --to and --resume"
+             " has to be given")
+    return 1
+
+  if opts.resume:
+    ssconf.CheckMaster(False)
+    versionstring = _ReadIntentToUpgrade()
+    if versionstring is None:
+      return 0
+    version = utils.version.ParseVersion(versionstring)
+    if version is None:
+      return 1
+    configversion = _GetConfigVersion()
+    if configversion is None:
+      return 1
+    # If the upgrade we resume was an upgrade between compatible
+    # versions (like 2.10.0 to 2.10.1), the correct configversion
+    # does not guarantee that the config has been updated.
+    # However, in the case of a compatible update with the configuration
+    # not touched, we are running a different dirversion with the same
+    # config version.
+    config_already_modified = \
+      (utils.IsCorrectConfigVersion(version, configversion) and
+       not (versionstring != constants.DIR_VERSION and
+            configversion == (constants.CONFIG_MAJOR, constants.CONFIG_MINOR,
+                              constants.CONFIG_REVISION)))
+    if not config_already_modified:
+      # We have to start from the beginning; however, some daemons might have
+      # already been stopped, so the only way to get into a well-defined state
+      # is by starting all daemons again.
+      _VerifyCommand([pathutils.DAEMON_UTIL, "start-all"])
+  else:
+    versionstring = opts.to
+    config_already_modified = False
+    version = utils.version.ParseVersion(versionstring)
+    if version is None:
+      ToStderr("Could not parse version string %s" % versionstring)
+      return 1
+
+  msg = utils.version.UpgradeRange(version)
+  if msg is not None:
+    ToStderr("Cannot upgrade to %s: %s" % (versionstring, msg))
+    return 1
+
+  if not config_already_modified:
+    success, rollback = _UpgradeBeforeConfigurationChange(versionstring)
+    if not success:
+      _ExecuteCommands(rollback)
+      return 1
+  else:
+    rollback = []
+
+  downgrade = utils.version.ShouldCfgdowngrade(version)
+
+  success, additionalrollback =  \
+      _SwitchVersionAndConfig(versionstring, downgrade)
+  if not success:
+    rollback.extend(additionalrollback)
+    _ExecuteCommands(rollback)
+    return 1
+
+  return _UpgradeAfterConfigurationChange()
+
+
 commands = {
   "init": (
     InitCluster, [ArgHost(min=1, max=1)],
@@ -1544,7 +2042,7 @@ commands = {
      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,
+     MAINTAIN_NODE_HEALTH_OPT, UIDPOOL_OPT, DRBD_HELPER_OPT,
      DEFAULT_IALLOCATOR_OPT, PRIMARY_IP_VERSION_OPT, PREALLOC_WIPE_DISKS_OPT,
      NODE_PARAMS_OPT, GLOBAL_SHARED_FILEDIR_OPT, USE_EXTERNAL_MIP_SCRIPT,
      DISK_PARAMS_OPT, HV_STATE_OPT, DISK_STATE_OPT, ENABLED_DISK_TEMPLATES_OPT,
@@ -1559,7 +2057,8 @@ commands = {
     "<new_name>",
     "Renames the cluster"),
   "redist-conf": (
-    RedistributeConfig, ARGS_NONE, SUBMIT_OPTS + [DRY_RUN_OPT, PRIORITY_OPT],
+    RedistributeConfig, ARGS_NONE, SUBMIT_OPTS +
+    [DRY_RUN_OPT, PRIORITY_OPT, FORCE_DISTRIBUTION],
     "", "Forces a push of the configuration file and ssconf files"
     " to the nodes in the cluster"),
   "verify": (
@@ -1624,7 +2123,7 @@ commands = {
      BACKEND_OPT, CP_SIZE_OPT, ENABLED_HV_OPT, HVLIST_OPT, MASTER_NETDEV_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,
+     DRBD_HELPER_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_OPTS +
@@ -1654,6 +2153,9 @@ commands = {
   "show-ispecs-cmd": (
     ShowCreateCommand, ARGS_NONE, [], "",
     "Show the command line to re-create the cluster"),
+  "upgrade": (
+    UpgradeGanetiCommand, ARGS_NONE, [TO_OPT, RESUME_OPT], "",
+    "Upgrade (or downgrade) to a new Ganeti version"),
   }
 
 
index ba31adc..7910a19 100644 (file)
@@ -673,10 +673,10 @@ commands = {
                 help="Select number of VCPUs for the instance"),
      cli_option("--tags", default=None,
                 help="Comma separated list of tags"),
-     cli_option("--evac-mode", default=constants.IALLOCATOR_NEVAC_ALL,
-                choices=list(constants.IALLOCATOR_NEVAC_MODES),
+     cli_option("--evac-mode", default=constants.NODE_EVAC_ALL,
+                choices=list(constants.NODE_EVAC_MODES),
                 help=("Node evacuation mode (one of %s)" %
-                      utils.CommaJoin(constants.IALLOCATOR_NEVAC_MODES))),
+                      utils.CommaJoin(constants.NODE_EVAC_MODES))),
      cli_option("--target-groups", help="Target groups for relocation",
                 default=[], action="append"),
      cli_option("--spindle-use", help="How many spindles to use",
index de8239f..82647c8 100644 (file)
@@ -942,10 +942,6 @@ def _FormatDiskDetails(dev_type, dev, roman):
   return data
 
 
-def _FormatListInfo(data):
-  return list(str(i) for i in data)
-
-
 def _FormatBlockDevInfo(idx, top_level, dev, roman):
   """Show block device information.
 
@@ -1043,8 +1039,6 @@ def _FormatBlockDevInfo(idx, top_level, dev, roman):
       data.append(("logical_id", l_id[0]))
     else:
       data.extend(l_id)
-  elif dev["physical_id"] is not None:
-    data.append(("physical_id:", _FormatListInfo(dev["physical_id"])))
 
   if dev["pstatus"]:
     data.append(("on primary", helper(dev["dev_type"], dev["pstatus"])))
@@ -1065,7 +1059,7 @@ def _FormatBlockDevInfo(idx, top_level, dev, roman):
 
 def _FormatInstanceNicInfo(idx, nic):
   """Helper function for L{_FormatInstanceInfo()}"""
-  (name, uuid, ip, mac, mode, link, _, netinfo) = nic
+  (name, uuid, ip, mac, mode, link, vlan, _, netinfo) = nic
   network_name = None
   if netinfo:
     network_name = netinfo["name"]
@@ -1075,6 +1069,7 @@ def _FormatInstanceNicInfo(idx, nic):
     ("IP", str(ip)),
     ("mode", str(mode)),
     ("link", str(link)),
+    ("vlan", str(vlan)),
     ("network", str(network_name)),
     ("UUID", str(uuid)),
     ("name", str(name)),
@@ -1319,6 +1314,14 @@ def SetInstanceParams(opts, args):
                       allowed_values=[constants.VALUE_DEFAULT])
 
   nics = _ConvertNicDiskModifications(opts.nics)
+  for action, _, __ in nics:
+    if action == constants.DDM_MODIFY and opts.hotplug and not opts.force:
+      usertext = ("You are about to hot-modify a NIC. This will be done"
+                  " by removing the exisiting and then adding a new one."
+                  " Network connection might be lost. Continue?")
+      if not AskUser(usertext):
+        return 1
+
   disks = _ParseDiskSizes(_ConvertNicDiskModifications(opts.disks))
 
   if (opts.disk_template and
@@ -1338,6 +1341,7 @@ def SetInstanceParams(opts, args):
   op = opcodes.OpInstanceSetParams(instance_name=args[0],
                                    nics=nics,
                                    disks=disks,
+                                   hotplug=opts.hotplug,
                                    disk_template=opts.disk_template,
                                    remote_node=opts.node,
                                    pnode=opts.new_primary_node,
@@ -1545,7 +1549,7 @@ commands = {
     [DISK_TEMPLATE_OPT, SINGLE_NODE_OPT, OS_OPT, FORCE_VARIANT_OPT,
      OSPARAMS_OPT, DRY_RUN_OPT, PRIORITY_OPT, NWSYNC_OPT, OFFLINE_INST_OPT,
      ONLINE_INST_OPT, IGNORE_IPOLICY_OPT, RUNTIME_MEM_OPT,
-     NOCONFLICTSCHECK_OPT, NEW_PRIMARY_OPT],
+     NOCONFLICTSCHECK_OPT, NEW_PRIMARY_OPT, HOTPLUG_OPT],
     "<instance>", "Alters the parameters of an instance"),
   "shutdown": (
     GenericManyOps("shutdown", _ShutdownInstance), [ArgInstance()],
index 48ed7dd..fbfab30 100644 (file)
@@ -123,6 +123,18 @@ IGNORE_STATUS_OPT = cli_option("--ignore-status", default=False,
                                help=("Ignore the Node(s) offline status"
                                      " (potentially DANGEROUS)"))
 
+OVS_OPT = cli_option("--ovs", default=False, action="store_true", dest="ovs",
+                     help=("Enable OpenvSwitch on the new node. This will"
+                           " initialize OpenvSwitch during gnt-node add"))
+
+OVS_NAME_OPT = cli_option("--ovs-name", action="store", dest="ovs_name",
+                          type="string", default=None,
+                          help=("Set name of OpenvSwitch to connect instances"))
+
+OVS_LINK_OPT = cli_option("--ovs-link", action="store", dest="ovs_link",
+                          type="string", default=None,
+                          help=("Physical trunk interface for OpenvSwitch"))
+
 
 def ConvertStorageType(user_storage_type):
   """Converts a user storage type to its internal name.
@@ -278,9 +290,19 @@ def AddNode(opts, args):
 
   hv_state = dict(opts.hv_state)
 
+  if not opts.ndparams:
+    ndparams = {constants.ND_OVS: opts.ovs,
+                constants.ND_OVS_NAME: opts.ovs_name,
+                constants.ND_OVS_LINK: opts.ovs_link}
+  else:
+    ndparams = opts.ndparams
+    ndparams[constants.ND_OVS] = opts.ovs
+    ndparams[constants.ND_OVS_NAME] = opts.ovs_name
+    ndparams[constants.ND_OVS_LINK] = opts.ovs_link
+
   op = opcodes.OpNodeAdd(node_name=args[0], secondary_ip=sip,
                          readd=opts.readd, group=opts.nodegroup,
-                         vm_capable=opts.vm_capable, ndparams=opts.ndparams,
+                         vm_capable=opts.vm_capable, ndparams=ndparams,
                          master_capable=opts.master_capable,
                          disk_state=disk_state,
                          hv_state=hv_state)
@@ -795,16 +817,10 @@ def ListStorage(opts, args):
   @return: the desired exit code
 
   """
-  # TODO: Default to ST_FILE if LVM is disabled on the cluster
-  if opts.user_storage_type is None:
-    opts.user_storage_type = constants.ST_LVM_PV
-
-  storage_type = ConvertStorageType(opts.user_storage_type)
-
   selected_fields = ParseFields(opts.output, _LIST_STOR_DEF_FIELDS)
 
   op = opcodes.OpNodeQueryStorage(nodes=args,
-                                  storage_type=storage_type,
+                                  storage_type=opts.user_storage_type,
                                   output_fields=selected_fields)
   output = SubmitOpCode(op, opts=opts)
 
@@ -1080,12 +1096,12 @@ 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, HV_STATE_OPT,
-     DISK_STATE_OPT],
+     NONODE_SETUP_OPT, VERBOSE_OPT, OVS_OPT, OVS_NAME_OPT, OVS_LINK_OPT,
+     NODEGROUP_OPT, PRIORITY_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>",
+    " [--no-node-setup] [--verbose] [--network] [--ovs] [--ovs-name <vswitch>]"
+    " [--ovs-link <phys. if>] <node_name>",
     "Add a node to the cluster"),
   "evacuate": (
     EvacuateNode, ARGS_ONE_NODE,
index 3014a21..79e7d27 100644 (file)
@@ -85,12 +85,12 @@ class ExportQuery(QueryBase):
     node_uuids = self._GetNames(lu, lu.cfg.GetNodeList(), locking.LEVEL_NODE)
 
     result = []
-
     for (node_uuid, nres) in lu.rpc.call_export_list(node_uuids).items():
+      node = lu.cfg.GetNodeInfo(node_uuid)
       if nres.fail_msg:
-        result.append((node_uuid, None))
+        result.append((node.name, None))
       else:
-        result.extend((node_uuid, expname) for expname in nres.payload)
+        result.extend((node.name, expname) for expname in nres.payload)
 
     return result
 
@@ -393,15 +393,10 @@ class LUBackupExport(LogicalUnit):
                    " node %s" % (self.instance.name,
                                  self.cfg.GetNodeName(src_node_uuid)))
 
-    # set the disks ID correctly since call_instance_start needs the
-    # correct drbd minor to create the symlinks
-    for disk in self.instance.disks:
-      self.cfg.SetDiskID(disk, src_node_uuid)
-
     activate_disks = not self.instance.disks_active
 
     if activate_disks:
-      # Activate the instance disks if we'exporting a stopped instance
+      # Activate the instance disks if we're exporting a stopped instance
       feedback_fn("Activating disks for %s" % self.instance.name)
       StartInstanceDisks(self, self.instance, None)
 
index b855f5a..a723cfc 100644 (file)
@@ -57,7 +57,8 @@ from ganeti.cmdlib.common import ShareAll, RunPostHook, \
   GetUpdatedIPolicy, ComputeNewInstanceViolations, GetUpdatedParams, \
   CheckOSParams, CheckHVParams, AdjustCandidatePool, CheckNodePVs, \
   ComputeIPolicyInstanceViolation, AnnotateDiskParams, SupportsOob, \
-  CheckIpolicyVsDiskTemplates
+  CheckIpolicyVsDiskTemplates, CheckDiskAccessModeValidity, \
+  CheckDiskAccessModeConsistency
 
 import ganeti.masterd.instance
 
@@ -180,6 +181,29 @@ class LUClusterPostInit(LogicalUnit):
   HPATH = "cluster-init"
   HTYPE = constants.HTYPE_CLUSTER
 
+  def CheckArguments(self):
+    self.master_uuid = self.cfg.GetMasterNode()
+    self.master_ndparams = self.cfg.GetNdParams(self.cfg.GetMasterNodeInfo())
+
+    # TODO: When Issue 584 is solved, and None is properly parsed when used
+    # as a default value, ndparams.get(.., None) can be changed to
+    # ndparams[..] to access the values directly
+
+    # OpenvSwitch: Warn user if link is missing
+    if (self.master_ndparams[constants.ND_OVS] and not
+        self.master_ndparams.get(constants.ND_OVS_LINK, None)):
+      self.LogInfo("No physical interface for OpenvSwitch was given."
+                   " OpenvSwitch will not have an outside connection. This"
+                   " might not be what you want.")
+
+    # OpenvSwitch: Warn if parameters are given, but OVS is not enabled.
+    if (not self.master_ndparams[constants.ND_OVS] and
+        (self.master_ndparams[constants.ND_OVS_NAME] or
+         self.master_ndparams.get(constants.ND_OVS_LINK, None))):
+      self.LogInfo("OpenvSwitch name or link were given, but"
+                   " OpenvSwitch is not enabled. Please enable"
+                   " OpenvSwitch with 'ovs=true' or create it manually")
+
   def BuildHooksEnv(self):
     """Build hooks env.
 
@@ -195,9 +219,15 @@ class LUClusterPostInit(LogicalUnit):
     return ([], [self.cfg.GetMasterNode()])
 
   def Exec(self, feedback_fn):
-    """Nothing to do.
+    """Create and configure Open vSwitch
 
     """
+    if self.master_ndparams[constants.ND_OVS]:
+      result = self.rpc.call_node_configure_ovs(
+                 self.master_uuid,
+                 self.master_ndparams[constants.ND_OVS_NAME],
+                 self.master_ndparams.get(constants.ND_OVS_LINK, None))
+      result.Raise("Could not successully configure Open vSwitch")
     return True
 
 
@@ -533,9 +563,11 @@ class LUClusterRepairDiskSizes(NoHooksLU):
 
     changed = []
     for node_uuid, dskl in per_node_disks.items():
-      newl = [v[2].Copy() for v in dskl]
-      for dsk in newl:
-        self.cfg.SetDiskID(dsk, node_uuid)
+      if not dskl:
+        # no disks on the node
+        continue
+
+      newl = [([v[2].Copy()], v[0]) for v in dskl]
       node_name = self.cfg.GetNodeName(node_uuid)
       result = self.rpc.call_blockdev_getdimensions(node_uuid, newl)
       if result.fail_msg:
@@ -702,6 +734,7 @@ class LUClusterSetParams(LogicalUnit):
         utils.ForceDictType(dt_params, constants.DISK_DT_TYPES)
       try:
         utils.VerifyDictOptions(self.op.diskparams, constants.DISK_DT_DEFAULTS)
+        CheckDiskAccessModeValidity(self.op.diskparams)
       except errors.OpPrereqError, err:
         raise errors.OpPrereqError("While verify diskparams options: %s" % err,
                                    errors.ECODE_INVAL)
@@ -787,30 +820,42 @@ class LUClusterSetParams(LogicalUnit):
                                    errors.ECODE_ENVIRON)
 
   @staticmethod
-  def _GetEnabledDiskTemplatesInner(op_enabled_disk_templates,
-                                    old_enabled_disk_templates):
-    """Determines the enabled disk templates and the subset of disk templates
-       that are newly enabled by this operation.
+  def _GetDiskTemplateSetsInner(op_enabled_disk_templates,
+                                old_enabled_disk_templates):
+    """Computes three sets of disk templates.
+
+    @see: C{_GetDiskTemplateSets} for more details.
 
     """
     enabled_disk_templates = None
     new_enabled_disk_templates = []
+    disabled_disk_templates = []
     if op_enabled_disk_templates:
       enabled_disk_templates = op_enabled_disk_templates
       new_enabled_disk_templates = \
         list(set(enabled_disk_templates)
              - set(old_enabled_disk_templates))
+      disabled_disk_templates = \
+        list(set(old_enabled_disk_templates)
+             - set(enabled_disk_templates))
     else:
       enabled_disk_templates = old_enabled_disk_templates
-    return (enabled_disk_templates, new_enabled_disk_templates)
+    return (enabled_disk_templates, new_enabled_disk_templates,
+            disabled_disk_templates)
+
+  def _GetDiskTemplateSets(self, cluster):
+    """Computes three sets of disk templates.
 
-  def _GetEnabledDiskTemplates(self, cluster):
-    """Determines the enabled disk templates and the subset of disk templates
-       that are newly enabled by this operation.
+    The three sets are:
+      - disk templates that will be enabled after this operation (no matter if
+        they were enabled before or not)
+      - disk templates that get enabled by this operation (thus haven't been
+        enabled before.)
+      - disk templates that get disabled by this operation
 
     """
-    return self._GetEnabledDiskTemplatesInner(self.op.enabled_disk_templates,
-                                              cluster.enabled_disk_templates)
+    return self._GetDiskTemplateSetsInner(self.op.enabled_disk_templates,
+                                          cluster.enabled_disk_templates)
 
   def _CheckIpolicy(self, cluster, enabled_disk_templates):
     """Checks the ipolicy.
@@ -851,19 +896,89 @@ class LUClusterSetParams(LogicalUnit):
       CheckIpolicyVsDiskTemplates(cluster.ipolicy,
                                   enabled_disk_templates)
 
-  def CheckPrereq(self):
-    """Check prerequisites.
+  def _CheckDrbdHelperOnNodes(self, drbd_helper, node_uuids):
+    """Checks whether the set DRBD helper actually exists on the nodes.
 
-    This checks whether the given params don't conflict and
-    if the given volume group is valid.
+    @type drbd_helper: string
+    @param drbd_helper: path of the drbd usermode helper binary
+    @type node_uuids: list of strings
+    @param node_uuids: list of node UUIDs to check for the helper
 
     """
-    if self.op.drbd_helper is not None and not self.op.drbd_helper:
+    # checks given drbd helper on all nodes
+    helpers = self.rpc.call_drbd_helper(node_uuids)
+    for (_, ninfo) in self.cfg.GetMultiNodeInfo(node_uuids):
+      if ninfo.offline:
+        self.LogInfo("Not checking drbd helper on offline node %s",
+                     ninfo.name)
+        continue
+      msg = helpers[ninfo.uuid].fail_msg
+      if msg:
+        raise errors.OpPrereqError("Error checking drbd helper on node"
+                                   " '%s': %s" % (ninfo.name, msg),
+                                   errors.ECODE_ENVIRON)
+      node_helper = helpers[ninfo.uuid].payload
+      if node_helper != drbd_helper:
+        raise errors.OpPrereqError("Error on node '%s': drbd helper is %s" %
+                                   (ninfo.name, node_helper),
+                                   errors.ECODE_ENVIRON)
+
+  def _CheckDrbdHelper(self, node_uuids, drbd_enabled, drbd_gets_enabled):
+    """Check the DRBD usermode helper.
+
+    @type node_uuids: list of strings
+    @param node_uuids: a list of nodes' UUIDs
+    @type drbd_enabled: boolean
+    @param drbd_enabled: whether DRBD will be enabled after this operation
+      (no matter if it was disabled before or not)
+    @type drbd_gets_enabled: boolen
+    @param drbd_gets_enabled: true if DRBD was disabled before this
+      operation, but will be enabled afterwards
+
+    """
+    if self.op.drbd_helper == '':
+      if drbd_enabled:
+        raise errors.OpPrereqError("Cannot disable drbd helper while"
+                                   " DRBD is enabled.")
       if self.cfg.HasAnyDiskOfType(constants.DT_DRBD8):
         raise errors.OpPrereqError("Cannot disable drbd helper while"
                                    " drbd-based instances exist",
                                    errors.ECODE_INVAL)
 
+    else:
+      if self.op.drbd_helper is not None and drbd_enabled:
+        self._CheckDrbdHelperOnNodes(self.op.drbd_helper, node_uuids)
+      else:
+        if drbd_gets_enabled:
+          current_drbd_helper = self.cfg.GetClusterInfo().drbd_usermode_helper
+          if current_drbd_helper is not None:
+            self._CheckDrbdHelperOnNodes(current_drbd_helper, node_uuids)
+          else:
+            raise errors.OpPrereqError("Cannot enable DRBD without a"
+                                       " DRBD usermode helper set.")
+
+  def _CheckInstancesOfDisabledDiskTemplates(
+      self, disabled_disk_templates):
+    """Check whether we try to disable a disk template that is in use.
+
+    @type disabled_disk_templates: list of string
+    @param disabled_disk_templates: list of disk templates that are going to
+      be disabled by this operation
+
+    """
+    for disk_template in disabled_disk_templates:
+      if self.cfg.HasAnyDiskOfType(disk_template):
+        raise errors.OpPrereqError(
+            "Cannot disable disk template '%s', because there is at least one"
+            " instance using it." % disk_template)
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    This checks whether the given params don't conflict and
+    if the given volume group is valid.
+
+    """
     node_uuids = self.owned_locks(locking.LEVEL_NODE)
     self.cluster = cluster = self.cfg.GetClusterInfo()
 
@@ -871,8 +986,9 @@ class LUClusterSetParams(LogicalUnit):
                              for node in self.cfg.GetAllNodesInfo().values()
                              if node.uuid in node_uuids and node.vm_capable]
 
-    (enabled_disk_templates, new_enabled_disk_templates) = \
-      self._GetEnabledDiskTemplates(cluster)
+    (enabled_disk_templates, new_enabled_disk_templates,
+      disabled_disk_templates) = self._GetDiskTemplateSets(cluster)
+    self._CheckInstancesOfDisabledDiskTemplates(disabled_disk_templates)
 
     self._CheckVgName(vm_capable_node_uuids, enabled_disk_templates,
                       new_enabled_disk_templates)
@@ -886,24 +1002,9 @@ class LUClusterSetParams(LogicalUnit):
           self.LogWarning, self.op.shared_file_storage_dir,
           enabled_disk_templates)
 
-    if self.op.drbd_helper:
-      # checks given drbd helper on all nodes
-      helpers = self.rpc.call_drbd_helper(node_uuids)
-      for (_, ninfo) in self.cfg.GetMultiNodeInfo(node_uuids):
-        if ninfo.offline:
-          self.LogInfo("Not checking drbd helper on offline node %s",
-                       ninfo.name)
-          continue
-        msg = helpers[ninfo.uuid].fail_msg
-        if msg:
-          raise errors.OpPrereqError("Error checking drbd helper on node"
-                                     " '%s': %s" % (ninfo.name, msg),
-                                     errors.ECODE_ENVIRON)
-        node_helper = helpers[ninfo.uuid].payload
-        if node_helper != self.op.drbd_helper:
-          raise errors.OpPrereqError("Error on node '%s': drbd helper is %s" %
-                                     (ninfo.name, node_helper),
-                                     errors.ECODE_ENVIRON)
+    drbd_enabled = constants.DT_DRBD8 in enabled_disk_templates
+    drbd_gets_enabled = constants.DT_DRBD8 in new_enabled_disk_templates
+    self._CheckDrbdHelper(node_uuids, drbd_enabled, drbd_gets_enabled)
 
     # validate params changes
     if self.op.beparams:
@@ -982,6 +1083,7 @@ class LUClusterSetParams(LogicalUnit):
           self.new_diskparams[dt_name] = dt_params
         else:
           self.new_diskparams[dt_name].update(dt_params)
+      CheckDiskAccessModeConsistency(self.op.diskparams, self.cfg)
 
     # os hypervisor parameters
     self.new_os_hvp = objects.FillDict(cluster.os_hvp, {})
@@ -1111,21 +1213,14 @@ class LUClusterSetParams(LogicalUnit):
       else:
         self.cluster.file_storage_dir = self.op.file_storage_dir
 
-  def Exec(self, feedback_fn):
-    """Change the parameters of the cluster.
+  def _SetDrbdHelper(self, feedback_fn):
+    """Set the DRBD usermode helper.
 
     """
-    if self.op.enabled_disk_templates:
-      self.cluster.enabled_disk_templates = \
-        list(set(self.op.enabled_disk_templates))
-
-    self._SetVgName(feedback_fn)
-    self._SetFileStorageDir(feedback_fn)
-
     if self.op.drbd_helper is not None:
       if not constants.DT_DRBD8 in self.cluster.enabled_disk_templates:
-        feedback_fn("Note that you specified a drbd user helper, but did"
-                    " enabled the drbd disk template.")
+        feedback_fn("Note that you specified a drbd user helper, but did not"
+                    " enable the drbd disk template.")
       new_helper = self.op.drbd_helper
       if not new_helper:
         new_helper = None
@@ -1134,6 +1229,19 @@ class LUClusterSetParams(LogicalUnit):
       else:
         feedback_fn("Cluster DRBD helper already in desired state,"
                     " not changing")
+
+  def Exec(self, feedback_fn):
+    """Change the parameters of the cluster.
+
+    """
+    if self.op.enabled_disk_templates:
+      self.cluster.enabled_disk_templates = \
+        list(self.op.enabled_disk_templates)
+
+    self._SetVgName(feedback_fn)
+    self._SetFileStorageDir(feedback_fn)
+    self._SetDrbdHelper(feedback_fn)
+
     if self.op.hvparams:
       self.cluster.hvparams = self.new_hvparams
     if self.op.os_hvp:
@@ -2136,7 +2244,7 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
          self.all_node_info[node_uuid].group != self.group_uuid:
         # we're skipping nodes marked offline and nodes in other groups from
         # the N+1 warning, since most likely we don't have good memory
-        # infromation from them; we already list instances living on such
+        # information from them; we already list instances living on such
         # nodes, and that's enough warning
         continue
       #TODO(dynmem): also consider ballooning out other instances
@@ -2267,17 +2375,8 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
                     "File %s found with %s different checksums (%s)",
                     filename, len(checksums), "; ".join(variants))
 
-  def _VerifyNodeDrbd(self, ninfo, nresult, instanceinfo, drbd_helper,
-                      drbd_map):
-    """Verifies and the node DRBD status.
-
-    @type ninfo: L{objects.Node}
-    @param ninfo: the node to check
-    @param nresult: the remote results for the node
-    @param instanceinfo: the dict of instances
-    @param drbd_helper: the configured DRBD usermode helper
-    @param drbd_map: the DRBD map as returned by
-        L{ganeti.config.ConfigWriter.ComputeDRBDMap}
+  def _VerifyNodeDrbdHelper(self, ninfo, nresult, drbd_helper):
+    """Verify the drbd helper.
 
     """
     if drbd_helper:
@@ -2294,6 +2393,21 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
         self._ErrorIf(test, constants.CV_ENODEDRBDHELPER, ninfo.name,
                       "wrong drbd usermode helper: %s", payload)
 
+  def _VerifyNodeDrbd(self, ninfo, nresult, instanceinfo, drbd_helper,
+                      drbd_map):
+    """Verifies and the node DRBD status.
+
+    @type ninfo: L{objects.Node}
+    @param ninfo: the node to check
+    @param nresult: the remote results for the node
+    @param instanceinfo: the dict of instances
+    @param drbd_helper: the configured DRBD usermode helper
+    @param drbd_map: the DRBD map as returned by
+        L{ganeti.config.ConfigWriter.ComputeDRBDMap}
+
+    """
+    self._VerifyNodeDrbdHelper(ninfo, nresult, drbd_helper)
+
     # compute the DRBD minors
     node_drbd = {}
     for minor, inst_uuid in drbd_map[ninfo.uuid].items():
@@ -2615,7 +2729,7 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
 
     """
     node_disks = {}
-    node_disks_devonly = {}
+    node_disks_dev_inst_only = {}
     diskless_instances = set()
     diskless = constants.DT_DISKLESS
 
@@ -2635,20 +2749,19 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
       node_disks[nuuid] = disks
 
       # _AnnotateDiskParams makes already copies of the disks
-      devonly = []
+      dev_inst_only = []
       for (inst_uuid, dev) in disks:
         (anno_disk,) = AnnotateDiskParams(instanceinfo[inst_uuid], [dev],
                                           self.cfg)
-        self.cfg.SetDiskID(anno_disk, nuuid)
-        devonly.append(anno_disk)
+        dev_inst_only.append((anno_disk, instanceinfo[inst_uuid]))
 
-      node_disks_devonly[nuuid] = devonly
+      node_disks_dev_inst_only[nuuid] = dev_inst_only
 
-    assert len(node_disks) == len(node_disks_devonly)
+    assert len(node_disks) == len(node_disks_dev_inst_only)
 
     # Collect data from all nodes with disks
-    result = self.rpc.call_blockdev_getmirrorstatus_multi(node_disks.keys(),
-                                                          node_disks_devonly)
+    result = self.rpc.call_blockdev_getmirrorstatus_multi(
+               node_disks.keys(), node_disks_dev_inst_only)
 
     assert len(result) == len(node_disks)
 
@@ -2833,10 +2946,11 @@ class LUClusterVerifyGroup(LogicalUnit, _VerifyErrors):
       node_verify_param[constants.NV_LVLIST] = vg_name
       node_verify_param[constants.NV_PVLIST] = [vg_name]
 
-    if drbd_helper:
-      node_verify_param[constants.NV_DRBDVERSION] = None
-      node_verify_param[constants.NV_DRBDLIST] = None
-      node_verify_param[constants.NV_DRBDHELPER] = drbd_helper
+    if cluster.IsDiskTemplateEnabled(constants.DT_DRBD8):
+      if drbd_helper:
+        node_verify_param[constants.NV_DRBDVERSION] = None
+        node_verify_param[constants.NV_DRBDLIST] = None
+        node_verify_param[constants.NV_DRBDHELPER] = drbd_helper
 
     if cluster.IsFileStorageEnabled() or \
         cluster.IsSharedFileStorageEnabled():
index 73132c2..5a1a017 100644 (file)
@@ -194,7 +194,7 @@ def RedistributeAncillaryFiles(lu):
   """
   # Gather target nodes
   cluster = lu.cfg.GetClusterInfo()
-  master_info = lu.cfg.GetNodeInfo(lu.cfg.GetMasterNode())
+  master_info = lu.cfg.GetMasterNodeInfo()
 
   online_node_uuids = lu.cfg.GetOnlineNodeList()
   online_node_uuid_set = frozenset(online_node_uuids)
@@ -720,8 +720,7 @@ def AnnotateDiskParams(instance, devs, cfg):
   @see L{rpc.AnnotateDiskParams}
 
   """
-  return rpc.AnnotateDiskParams(instance.disk_template, devs,
-                                cfg.GetInstanceDiskParams(instance))
+  return rpc.AnnotateDiskParams(devs, cfg.GetInstanceDiskParams(instance))
 
 
 def SupportsOob(cfg, node):
@@ -1037,9 +1036,6 @@ def CheckIAllocatorOrNode(lu, iallocator_slot, node_slot):
 def FindFaultyInstanceDisks(cfg, rpc_runner, instance, node_uuid, prereq):
   faulty = []
 
-  for dev in instance.disks:
-    cfg.SetDiskID(dev, node_uuid)
-
   result = rpc_runner.call_blockdev_getmirrorstatus(
              node_uuid, (instance.disks, instance))
   result.Raise("Failed to get disk status from node %s" %
@@ -1137,3 +1133,88 @@ def CheckIpolicyVsDiskTemplates(ipolicy, enabled_disk_templates):
     raise errors.OpPrereqError("The following disk template are allowed"
                                " by the ipolicy, but not enabled on the"
                                " cluster: %s" % utils.CommaJoin(not_enabled))
+
+
+def CheckDiskAccessModeValidity(parameters):
+  """Checks if the access parameter is legal.
+
+  @see: L{CheckDiskAccessModeConsistency} for cluster consistency checks.
+  @raise errors.OpPrereqError: if the check fails.
+
+  """
+  if constants.DT_RBD in parameters:
+    access = parameters[constants.DT_RBD].get(constants.RBD_ACCESS,
+                                              constants.DISK_KERNELSPACE)
+    if access not in constants.DISK_VALID_ACCESS_MODES:
+      valid_vals_str = utils.CommaJoin(constants.DISK_VALID_ACCESS_MODES)
+      raise errors.OpPrereqError("Invalid value of '{d}:{a}': '{v}' (expected"
+                                 " one of {o})".format(d=constants.DT_RBD,
+                                                       a=constants.RBD_ACCESS,
+                                                       v=access,
+                                                       o=valid_vals_str))
+
+
+def CheckDiskAccessModeConsistency(parameters, cfg, group=None):
+  """Checks if the access param is consistent with the cluster configuration.
+
+  @note: requires a configuration lock to run.
+  @param parameters: the parameters to validate
+  @param cfg: the cfg object of the cluster
+  @param group: if set, only check for consistency within this group.
+  @raise errors.OpPrereqError: if the LU attempts to change the access parameter
+                               to an invalid value, such as "pink bunny".
+  @raise errors.OpPrereqError: if the LU attempts to change the access parameter
+                               to an inconsistent value, such as asking for RBD
+                               userspace access to the chroot hypervisor.
+
+  """
+  CheckDiskAccessModeValidity(parameters)
+
+  if constants.DT_RBD in parameters:
+    access = parameters[constants.DT_RBD].get(constants.RBD_ACCESS,
+                                              constants.DISK_KERNELSPACE)
+
+    #Check the combination of instance hypervisor, disk template and access
+    #protocol is sane.
+    inst_uuids = cfg.GetNodeGroupInstances(group) if group else \
+                 cfg.GetInstanceList()
+
+    for entry in inst_uuids:
+      #hyp, disk, access
+      inst = cfg.GetInstanceInfo(entry)
+      hv = inst.hypervisor
+      dt = inst.disk_template
+
+      #do not check for disk types that don't have this setting.
+      if dt != constants.DT_RBD:
+        continue
+
+      if not IsValidDiskAccessModeCombination(hv, dt, access):
+        raise errors.OpPrereqError("Instance {i}: cannot use '{a}' access"
+                                   " setting with {h} hypervisor and {d} disk"
+                                   " type.".format(i=inst.name,
+                                                   a=access,
+                                                   h=hv,
+                                                   d=dt))
+
+
+def IsValidDiskAccessModeCombination(hv, disk_template, mode):
+  """Checks if an hypervisor can read a disk template with given mode.
+
+  @param hv: the hypervisor that will access the data
+  @param disk_template: the disk template the data is stored as
+  @param mode: how the hypervisor should access the data
+  @return: True if the hypervisor can read a given read disk_template
+           in the specified mode.
+
+  """
+  if mode == constants.DISK_KERNELSPACE:
+    return True
+
+  if (hv == constants.HT_KVM and
+      disk_template == constants.DT_RBD and
+      mode == constants.DISK_USERSPACE):
+    return True
+
+  # Everything else:
+  return False
index 98a3891..3af8507 100644 (file)
@@ -39,7 +39,8 @@ from ganeti.cmdlib.common import MergeAndVerifyHvState, \
   CheckNodeGroupInstances, GetUpdatedIPolicy, \
   ComputeNewInstanceViolations, GetDefaultIAllocator, ShareAll, \
   CheckInstancesNodeGroups, LoadNodeEvacResult, MapInstanceLvsToNodes, \
-  CheckIpolicyVsDiskTemplates
+  CheckIpolicyVsDiskTemplates, CheckDiskAccessModeValidity, \
+  CheckDiskAccessModeConsistency
 
 import ganeti.masterd.instance
 
@@ -406,6 +407,9 @@ class LUGroupSetParams(LogicalUnit):
       raise errors.OpPrereqError("Please pass at least one modification",
                                  errors.ECODE_INVAL)
 
+    if self.op.diskparams:
+      CheckDiskAccessModeValidity(self.op.diskparams)
+
   def ExpandNames(self):
     # This raises errors.OpPrereqError on its own:
     self.group_uuid = self.cfg.LookupNodeGroup(self.op.group_name)
@@ -500,8 +504,11 @@ class LUGroupSetParams(LogicalUnit):
       # 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)
+        CheckDiskAccessModeConsistency(self.new_diskparams, self.cfg,
+                                       group=self.group)
       except errors.OpPrereqError, err:
         raise errors.OpPrereqError("While verify diskparams options: %s" % err,
                                    errors.ECODE_INVAL)
@@ -968,12 +975,9 @@ class LUGroupVerifyDisks(NoHooksLU):
                                        inst.secondary_nodes):
         node_to_inst.setdefault(node_uuid, []).append(inst)
 
-    nodes_ip = dict((uuid, node.secondary_ip) for (uuid, node)
-                    in self.cfg.GetMultiNodeInfo(node_to_inst.keys()))
     for (node_uuid, insts) in node_to_inst.items():
       node_disks = [(inst.disks, inst) for inst in insts]
-      node_res = self.rpc.call_drbd_needs_activation(node_uuid, nodes_ip,
-                                                     node_disks)
+      node_res = self.rpc.call_drbd_needs_activation(node_uuid, node_disks)
       msg = node_res.fail_msg
       if msg:
         logging.warning("Error getting DRBD status on node %s: %s",
index 5110036..023cf45 100644 (file)
@@ -36,7 +36,6 @@ from ganeti.masterd import iallocator
 from ganeti import masterd
 from ganeti import netutils
 from ganeti import objects
-from ganeti import opcodes
 from ganeti import pathutils
 from ganeti import rpc
 from ganeti import utils
@@ -50,7 +49,7 @@ from ganeti.cmdlib.common import INSTANCE_DOWN, \
   IsExclusiveStorageEnabledNode, CheckHVParams, CheckOSParams, \
   AnnotateDiskParams, GetUpdatedParams, ExpandInstanceUuidAndName, \
   ComputeIPolicySpecViolation, CheckInstanceState, ExpandNodeUuidAndName, \
-  CheckDiskTemplateEnabled
+  CheckDiskTemplateEnabled, IsValidDiskAccessModeCombination
 from ganeti.cmdlib.instance_storage import CreateDisks, \
   CheckNodesFreeDiskPerVG, WipeDisks, WipeOrCleanupDisks, WaitForSync, \
   IsExclusiveStorageEnabledNodeUuid, CreateSingleBlockDev, ComputeDisks, \
@@ -174,6 +173,7 @@ def _ComputeNics(op, cluster, default_ip, cfg, ec_id):
     net = nic.get(constants.INIC_NETWORK, None)
     link = nic.get(constants.NIC_LINK, None)
     ip = nic.get(constants.INIC_IP, None)
+    vlan = nic.get(constants.INIC_VLAN, None)
 
     if net is None or net.lower() == constants.VALUE_NONE:
       net = None
@@ -183,6 +183,10 @@ def _ComputeNics(op, cluster, default_ip, cfg, ec_id):
                                    " is allowed to be passed",
                                    errors.ECODE_INVAL)
 
+    if vlan is not None and nic_mode != constants.NIC_MODE_OVS:
+      raise errors.OpPrereqError("VLAN is given, but network mode is not"
+                                 " openvswitch", errors.ECODE_INVAL)
+
     # ip validity checks
     if ip is None or ip.lower() == constants.VALUE_NONE:
       nic_ip = None
@@ -231,6 +235,8 @@ def _ComputeNics(op, cluster, default_ip, cfg, ec_id):
       nicparams[constants.NIC_MODE] = nic_mode
     if link:
       nicparams[constants.NIC_LINK] = link
+    if vlan:
+      nicparams[constants.NIC_VLAN] = vlan
 
     check_params = cluster.SimpleFillNIC(nicparams)
     objects.NIC.CheckParameterSyntax(check_params)
@@ -379,6 +385,38 @@ class LUInstanceCreate(LogicalUnit):
 
     self.adopt_disks = has_adopt
 
+  def _CheckVLANArguments(self):
+    """ Check validity of VLANs if given
+
+    """
+    for nic in self.op.nics:
+      vlan = nic.get(constants.INIC_VLAN, None)
+      if vlan:
+        if vlan[0] == ".":
+          # vlan starting with dot means single untagged vlan,
+          # might be followed by trunk (:)
+          if not vlan[1:].isdigit():
+            vlanlist = vlan[1:].split(':')
+            for vl in vlanlist:
+              if not vl.isdigit():
+                raise errors.OpPrereqError("Specified VLAN parameter is "
+                                           "invalid : %s" % vlan,
+                                             errors.ECODE_INVAL)
+        elif vlan[0] == ":":
+          # Trunk - tagged only
+          vlanlist = vlan[1:].split(':')
+          for vl in vlanlist:
+            if not vl.isdigit():
+              raise errors.OpPrereqError("Specified VLAN parameter is invalid"
+                                           " : %s" % vlan, errors.ECODE_INVAL)
+        elif vlan.isdigit():
+          # This is the simplest case. No dots, only single digit
+          # -> Create untagged access port, dot needs to be added
+          nic[constants.INIC_VLAN] = "." + vlan
+        else:
+          raise errors.OpPrereqError("Specified VLAN parameter is invalid"
+                                       " : %s" % vlan, errors.ECODE_INVAL)
+
   def CheckArguments(self):
     """Check arguments.
 
@@ -403,7 +441,10 @@ class LUInstanceCreate(LogicalUnit):
     # check that NIC's parameters names are unique and valid
     utils.ValidateDeviceNames("NIC", self.op.nics)
 
+    self._CheckVLANArguments()
+
     self._CheckDiskArguments()
+    assert self.op.disk_template is not None
 
     # instance name verification
     if self.op.name_check:
@@ -441,8 +482,6 @@ class LUInstanceCreate(LogicalUnit):
 
     _CheckOpportunisticLocking(self.op)
 
-    self._cds = GetClusterDomainSecret()
-
     if self.op.mode == constants.INSTANCE_IMPORT:
       # On import force_variant must be True, because if we forced it at
       # initial install, our only chance when importing it back is that it
@@ -460,11 +499,9 @@ class LUInstanceCreate(LogicalUnit):
         raise errors.OpPrereqError("Guest OS '%s' is not allowed for"
                                    " installation" % self.op.os_type,
                                    errors.ECODE_STATE)
-      if self.op.disk_template is None:
-        raise errors.OpPrereqError("No disk template specified",
-                                   errors.ECODE_INVAL)
-
     elif self.op.mode == constants.INSTANCE_REMOTE_IMPORT:
+      self._cds = GetClusterDomainSecret()
+
       # Check handshake to ensure both clusters have the same domain secret
       src_handshake = self.op.source_handshake
       if not src_handshake:
@@ -585,8 +622,6 @@ class LUInstanceCreate(LogicalUnit):
     else:
       node_name_whitelist = None
 
-    #TODO Export network to iallocator so that it chooses a pnode
-    #     in a nodegroup that has the desired network connected to
     req = _CreateInstanceAllocRequest(self.op, self.disks,
                                       self.nics, self.be_full,
                                       node_name_whitelist)
@@ -986,7 +1021,7 @@ class LUInstanceCreate(LogicalUnit):
         netparams = self.cfg.GetGroupNetParams(net_uuid, self.pnode.uuid)
         if netparams is None:
           raise errors.OpPrereqError("No netparams found for network"
-                                     " %s. Propably not connected to"
+                                     " %s. Probably not connected to"
                                      " node's %s nodegroup" %
                                      (nobj.name, self.pnode.name),
                                      errors.ECODE_INVAL)
@@ -1055,7 +1090,7 @@ class LUInstanceCreate(LogicalUnit):
       elif self.op.disk_template == constants.DT_EXT:
         # FIXME: Function that checks prereqs if needed
         pass
-      elif self.op.disk_template in utils.GetLvmDiskTemplates():
+      elif self.op.disk_template in constants.DTS_LVM:
         # Check lv size requirements, if not adopting
         req_sizes = ComputeDiskSizePerVG(self.op.disk_template, self.disks)
         CheckNodesFreeDiskPerVG(self, node_uuids, req_sizes)
@@ -1133,6 +1168,22 @@ class LUInstanceCreate(LogicalUnit):
         dsk[constants.IDISK_SIZE] = \
           int(float(node_disks[dsk[constants.IDISK_ADOPT]]))
 
+    # Check disk access param to be compatible with specified hypervisor
+    node_info = self.cfg.GetNodeInfo(self.op.pnode_uuid)
+    node_group = self.cfg.GetNodeGroup(node_info.group)
+    disk_params = self.cfg.GetGroupDiskParams(node_group)
+    access_type = disk_params[self.op.disk_template].get(
+      constants.RBD_ACCESS, constants.DISK_KERNELSPACE
+    )
+
+    if not IsValidDiskAccessModeCombination(self.op.hypervisor,
+                                            self.op.disk_template,
+                                            access_type):
+      raise errors.OpPrereqError("Selected hypervisor (%s) cannot be"
+                                 " used with %s disk access param" %
+                                 (self.op.hypervisor, access_type),
+                                  errors.ECODE_STATE)
+
     # Verify instance specs
     spindle_use = self.be_full.get(constants.BE_SPINDLE_USE, None)
     ispec = {
@@ -1237,7 +1288,6 @@ class LUInstanceCreate(LogicalUnit):
         for t_dsk, a_dsk in zip(tmp_disks, self.disks):
           rename_to.append(t_dsk.logical_id)
           t_dsk.logical_id = (t_dsk.logical_id[0], a_dsk[constants.IDISK_ADOPT])
-          self.cfg.SetDiskID(t_dsk, self.pnode.uuid)
         result = self.rpc.call_blockdev_rename(self.pnode.uuid,
                                                zip(tmp_disks, rename_to))
         result.Raise("Failed to rename adoped LVs")
@@ -1302,11 +1352,6 @@ class LUInstanceCreate(LogicalUnit):
     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, self.pnode.uuid)
       if self.op.mode == constants.INSTANCE_CREATE:
         if not self.op.no_install:
           pause_sync = (iobj.disk_template in constants.DTS_INT_MIRROR and
@@ -1354,7 +1399,7 @@ class LUInstanceCreate(LogicalUnit):
             dt = masterd.instance.DiskTransfer("disk/%s" % idx,
                                                constants.IEIO_FILE, (image, ),
                                                constants.IEIO_SCRIPT,
-                                               (iobj.disks[idx], idx),
+                                               ((iobj.disks[idx], iobj), idx),
                                                None)
             transfers.append(dt)
 
@@ -1538,8 +1583,8 @@ class LUInstanceRename(LogicalUnit):
     info = GetInstanceInfoText(renamed_inst)
     for (idx, disk) in enumerate(renamed_inst.disks):
       for node_uuid in renamed_inst.all_nodes:
-        self.cfg.SetDiskID(disk, node_uuid)
-        result = self.rpc.call_blockdev_setinfo(node_uuid, disk, info)
+        result = self.rpc.call_blockdev_setinfo(node_uuid,
+                                                (disk, renamed_inst), info)
         result.Warn("Error setting info on node %s for disk %s" %
                     (self.cfg.GetNodeName(node_uuid), idx), self.LogWarning)
     try:
@@ -1789,7 +1834,7 @@ class LUInstanceMove(LogicalUnit):
                         idx, result.fail_msg)
         errs.append(result.fail_msg)
         break
-      dev_path = result.payload
+      dev_path, _ = result.payload
       result = self.rpc.call_blockdev_export(source_node.uuid, (disk,
                                                                 self.instance),
                                              target_node.secondary_ip,
@@ -1967,8 +2012,8 @@ class LUInstanceMultiAlloc(NoHooksLU):
       failed_insts = []
 
     return {
-      opcodes.OpInstanceMultiAlloc.ALLOCATABLE_KEY: allocatable_insts,
-      opcodes.OpInstanceMultiAlloc.FAILED_KEY: failed_insts,
+      constants.ALLOCATABLE_KEY: allocatable_insts,
+      constants.FAILED_KEY: failed_insts,
       }
 
   def Exec(self, feedback_fn):
@@ -2107,7 +2152,8 @@ def GetItemFromContainer(identifier, kind, container):
 
 
 def _ApplyContainerMods(kind, container, chgdesc, mods,
-                        create_fn, modify_fn, remove_fn):
+                        create_fn, modify_fn, remove_fn,
+                        post_add_fn=None):
   """Applies descriptions in C{mods} to C{container}.
 
   @type kind: string
@@ -2131,6 +2177,10 @@ def _ApplyContainerMods(kind, container, chgdesc, mods,
   @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}
+  @type post_add_fn: callable
+  @param post_add_fn: Callable for post-processing a newly created item after
+    it has been put into the container. It receives the index of the new item
+    and the new item as parameters.
 
   """
   for (op, identifier, params, private) in mods:
@@ -2167,6 +2217,10 @@ def _ApplyContainerMods(kind, container, chgdesc, mods,
         assert idx <= len(container)
         # list.insert does so before the specified index
         container.insert(idx, item)
+
+      if post_add_fn is not None:
+        post_add_fn(addidx, item)
+
     else:
       # Retrieve existing item
       (absidx, item) = GetItemFromContainer(identifier, kind, container)
@@ -2174,11 +2228,13 @@ def _ApplyContainerMods(kind, container, chgdesc, mods,
       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")]
 
+        if remove_fn is not None:
+          msg = remove_fn(absidx, item, private)
+          if msg:
+            changes.append(("%s/%s" % (kind, absidx), msg))
+
         assert container[absidx] == item
         del container[absidx]
       elif op == constants.DDM_MODIFY:
@@ -2276,12 +2332,7 @@ class LUInstanceSetParams(LogicalUnit):
       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)
+      size = int(size)
 
       params[constants.IDISK_SIZE] = size
       name = params.get(constants.IDISK_NAME, None)
@@ -2365,9 +2416,9 @@ class LUInstanceSetParams(LogicalUnit):
                            "hypervisor", "instance", "cluster")
 
     self.op.disks = self._UpgradeDiskNicMods(
-      "disk", self.op.disks, opcodes.OpInstanceSetParams.TestDiskModifications)
+      "disk", self.op.disks, ht.TSetParamsMods(ht.TIDiskParams))
     self.op.nics = self._UpgradeDiskNicMods(
-      "NIC", self.op.nics, opcodes.OpInstanceSetParams.TestNicModifications)
+      "NIC", self.op.nics, ht.TSetParamsMods(ht.TINicParams))
 
     if self.op.disks and self.op.disk_template is not None:
       raise errors.OpPrereqError("Disk template conversion and other disk"
@@ -2715,6 +2766,14 @@ class LUInstanceSetParams(LogicalUnit):
                                       constants.DT_EXT),
                                      errors.ECODE_INVAL)
 
+    if not self.op.wait_for_sync and self.instance.disks_active:
+      for mod in self.diskmod:
+        if mod[0] == constants.DDM_ADD:
+          raise errors.OpPrereqError("Can't add a disk to an instance with"
+                                     " activated disks and"
+                                     " --no-wait-for-sync given.",
+                                     errors.ECODE_INVAL)
+
     if self.op.disks and self.instance.disk_template == constants.DT_DISKLESS:
       raise errors.OpPrereqError("Disk operations not supported for"
                                  " diskless instances", errors.ECODE_INVAL)
@@ -2750,6 +2809,7 @@ class LUInstanceSetParams(LogicalUnit):
     assert self.op.instance_name in self.owned_locks(locking.LEVEL_INSTANCE)
     self.instance = self.cfg.GetInstanceInfo(self.op.instance_uuid)
     self.cluster = self.cfg.GetClusterInfo()
+    cluster_hvparams = self.cluster.hvparams[self.instance.hypervisor]
 
     assert self.instance is not None, \
       "Cannot retrieve locked instance %s" % self.op.instance_name
@@ -2763,7 +2823,7 @@ class LUInstanceSetParams(LogicalUnit):
       # verify that the instance is not up
       instance_info = self.rpc.call_instance_info(
           pnode_uuid, self.instance.name, self.instance.hypervisor,
-          self.instance.hvparams)
+          cluster_hvparams)
       if instance_info.fail_msg:
         self.warn.append("Can't get instance runtime information: %s" %
                          instance_info.fail_msg)
@@ -2882,9 +2942,9 @@ class LUInstanceSetParams(LogicalUnit):
         mem_check_list.extend(self.instance.secondary_nodes)
       instance_info = self.rpc.call_instance_info(
           pnode_uuid, self.instance.name, self.instance.hypervisor,
-          self.instance.hvparams)
+          cluster_hvparams)
       hvspecs = [(self.instance.hypervisor,
-                  self.cluster.hvparams[self.instance.hypervisor])]
+                  cluster_hvparams)]
       nodeinfo = self.rpc.call_node_info(mem_check_list, None,
                                          hvspecs)
       pninfo = nodeinfo[pnode_uuid]
@@ -2945,7 +3005,7 @@ class LUInstanceSetParams(LogicalUnit):
       remote_info = self.rpc.call_instance_info(
          self.instance.primary_node, self.instance.name,
          self.instance.hypervisor,
-         self.cluster.hvparams[self.instance.hypervisor])
+         cluster_hvparams)
       remote_info.Raise("Error checking node %s" %
                         self.cfg.GetNodeName(self.instance.primary_node))
       if not remote_info.payload: # not running already
@@ -3006,7 +3066,8 @@ class LUInstanceSetParams(LogicalUnit):
       # Operate on copies as this is still in prereq
       nics = [nic.Copy() for nic in self.instance.nics]
       _ApplyContainerMods("NIC", nics, self._nic_chgdesc, self.nicmod,
-                          self._CreateNewNic, self._ApplyNicMods, None)
+                          self._CreateNewNic, self._ApplyNicMods,
+                          self._RemoveNic)
       # Verify that NIC names are unique and valid
       utils.ValidateDeviceNames("NIC", nics)
       self._new_nics = nics
@@ -3068,8 +3129,7 @@ class LUInstanceSetParams(LogicalUnit):
                                      self.instance.uuid, pnode_uuid,
                                      [snode_uuid], disk_info, None, None, 0,
                                      feedback_fn, self.diskparams)
-    anno_disks = rpc.AnnotateDiskParams(constants.DT_DRBD8, new_disks,
-                                        self.diskparams)
+    anno_disks = rpc.AnnotateDiskParams(new_disks, self.diskparams)
     p_excl_stor = IsExclusiveStorageEnabledNodeUuid(self.cfg, pnode_uuid)
     s_excl_stor = IsExclusiveStorageEnabledNodeUuid(self.cfg, snode_uuid)
     info = GetInstanceInfoText(self.instance)
@@ -3102,8 +3162,6 @@ class LUInstanceSetParams(LogicalUnit):
     except errors.GenericError, e:
       feedback_fn("Initializing of DRBD devices failed;"
                   " renaming back original volumes...")
-      for disk in new_disks:
-        self.cfg.SetDiskID(disk, pnode_uuid)
       rename_back_list = [(n.children[0], o.logical_id)
                           for (n, o) in zip(new_disks, self.instance.disks)]
       result = self.rpc.call_blockdev_rename(pnode_uuid, rename_back_list)
@@ -3164,22 +3222,36 @@ class LUInstanceSetParams(LogicalUnit):
 
     feedback_fn("Removing volumes on the secondary node...")
     for disk in old_disks:
-      self.cfg.SetDiskID(disk, snode_uuid)
-      msg = self.rpc.call_blockdev_remove(snode_uuid, disk).fail_msg
-      if msg:
-        self.LogWarning("Could not remove block device %s on node %s,"
-                        " continuing anyway: %s", disk.iv_name,
-                        self.cfg.GetNodeName(snode_uuid), msg)
+      result = self.rpc.call_blockdev_remove(snode_uuid, (disk, self.instance))
+      result.Warn("Could not remove block device %s on node %s,"
+                  " continuing anyway" %
+                  (disk.iv_name, self.cfg.GetNodeName(snode_uuid)),
+                  self.LogWarning)
 
     feedback_fn("Removing unneeded volumes on the primary node...")
     for idx, disk in enumerate(old_disks):
       meta = disk.children[1]
-      self.cfg.SetDiskID(meta, pnode_uuid)
-      msg = self.rpc.call_blockdev_remove(pnode_uuid, meta).fail_msg
-      if msg:
-        self.LogWarning("Could not remove metadata for disk %d on node %s,"
-                        " continuing anyway: %s", idx,
-                        self.cfg.GetNodeName(pnode_uuid), msg)
+      result = self.rpc.call_blockdev_remove(pnode_uuid, (meta, self.instance))
+      result.Warn("Could not remove metadata for disk %d on node %s,"
+                  " continuing anyway" %
+                  (idx, self.cfg.GetNodeName(pnode_uuid)),
+                  self.LogWarning)
+
+  def _HotplugDevice(self, action, dev_type, device, extra, seq):
+    self.LogInfo("Trying to hotplug device...")
+    msg = "hotplug:"
+    result = self.rpc.call_hotplug_device(self.instance.primary_node,
+                                          self.instance, action, dev_type,
+                                          (device, self.instance),
+                                          extra, seq)
+    if result.fail_msg:
+      self.LogWarning("Could not hotplug device: %s" % result.fail_msg)
+      self.LogInfo("Continuing execution..")
+      msg += "failed"
+    else:
+      self.LogInfo("Hotplug done.")
+      msg += "done"
+    return msg
 
   def _CreateNewDisk(self, idx, params, _):
     """Creates a new disk.
@@ -3206,9 +3278,37 @@ class LUInstanceSetParams(LogicalUnit):
                          disks=[(idx, disk, 0)],
                          cleanup=new_disks)
 
-    return (disk, [
-      ("disk/%d" % idx, "add:size=%s,mode=%s" % (disk.size, disk.mode)),
-      ])
+    changes = [
+      ("disk/%d" % idx,
+      "add:size=%s,mode=%s" % (disk.size, disk.mode)),
+      ]
+    if self.op.hotplug:
+      result = self.rpc.call_blockdev_assemble(self.instance.primary_node,
+                                               (disk, self.instance),
+                                               self.instance.name, True, idx)
+      if result.fail_msg:
+        changes.append(("disk/%d" % idx, "assemble:failed"))
+        self.LogWarning("Can't assemble newly created disk %d: %s",
+                        idx, result.fail_msg)
+      else:
+        _, link_name = result.payload
+        msg = self._HotplugDevice(constants.HOTPLUG_ACTION_ADD,
+                                  constants.HOTPLUG_TARGET_DISK,
+                                  disk, link_name, idx)
+        changes.append(("disk/%d" % idx, msg))
+
+    return (disk, changes)
+
+  def _PostAddDisk(self, _, disk):
+    if not WaitForSync(self, self.instance, disks=[disk],
+                       oneshot=not self.op.wait_for_sync):
+      raise errors.OpExecError("Failed to sync disks of %s" %
+                               self.instance.name)
+
+    # the disk is active at this point, so deactivate it if the instance disks
+    # are supposed to be inactive
+    if not self.instance.disks_active:
+      ShutdownInstanceDisks(self, self.instance, disks=[disk])
 
   @staticmethod
   def _ModifyDisk(idx, disk, params, _):
@@ -3231,11 +3331,18 @@ class LUInstanceSetParams(LogicalUnit):
     """Removes a disk.
 
     """
+    hotmsg = ""
+    if self.op.hotplug:
+      hotmsg = self._HotplugDevice(constants.HOTPLUG_ACTION_REMOVE,
+                                   constants.HOTPLUG_TARGET_DISK,
+                                   root, None, idx)
+      ShutdownInstanceDisks(self, self.instance, [root])
+
     (anno_disk,) = AnnotateDiskParams(self.instance, [root], self.cfg)
     for node_uuid, disk in anno_disk.ComputeNodeTree(
                              self.instance.primary_node):
-      self.cfg.SetDiskID(disk, node_uuid)
-      msg = self.rpc.call_blockdev_remove(node_uuid, disk).fail_msg
+      msg = self.rpc.call_blockdev_remove(node_uuid, (disk, self.instance)) \
+              .fail_msg
       if msg:
         self.LogWarning("Could not remove disk/%d on node '%s': %s,"
                         " continuing anyway", idx,
@@ -3245,6 +3352,8 @@ class LUInstanceSetParams(LogicalUnit):
     if root.dev_type in constants.DTS_DRBD:
       self.cfg.AddTcpUdpPort(root.logical_id[2])
 
+    return hotmsg
+
   def _CreateNewNic(self, idx, params, private):
     """Creates data structure for a new network interface.
 
@@ -3260,13 +3369,20 @@ class LUInstanceSetParams(LogicalUnit):
                        nicparams=nicparams)
     nobj.uuid = self.cfg.GenerateUniqueID(self.proc.GetECId())
 
-    return (nobj, [
+    changes = [
       ("nic.%d" % idx,
        "add:mac=%s,ip=%s,mode=%s,link=%s,network=%s" %
        (mac, ip, private.filled[constants.NIC_MODE],
-       private.filled[constants.NIC_LINK],
-       net)),
-      ])
+       private.filled[constants.NIC_LINK], net)),
+      ]
+
+    if self.op.hotplug:
+      msg = self._HotplugDevice(constants.HOTPLUG_ACTION_ADD,
+                                constants.HOTPLUG_TARGET_NIC,
+                                nobj, None, idx)
+      changes.append(("nic.%d" % idx, msg))
+
+    return (nobj, changes)
 
   def _ApplyNicMods(self, idx, nic, params, private):
     """Modifies a network interface.
@@ -3291,8 +3407,20 @@ class LUInstanceSetParams(LogicalUnit):
       for (key, val) in nic.nicparams.items():
         changes.append(("nic.%s/%d" % (key, idx), val))
 
+    if self.op.hotplug:
+      msg = self._HotplugDevice(constants.HOTPLUG_ACTION_MODIFY,
+                                constants.HOTPLUG_TARGET_NIC,
+                                nic, None, idx)
+      changes.append(("nic/%d" % idx, msg))
+
     return changes
 
+  def _RemoveNic(self, idx, nic, _):
+    if self.op.hotplug:
+      return self._HotplugDevice(constants.HOTPLUG_ACTION_REMOVE,
+                                 constants.HOTPLUG_TARGET_NIC,
+                                 nic, None, idx)
+
   def Exec(self, feedback_fn):
     """Modifies an instance.
 
@@ -3326,7 +3454,7 @@ class LUInstanceSetParams(LogicalUnit):
     # Apply disk changes
     _ApplyContainerMods("disk", self.instance.disks, result, self.diskmod,
                         self._CreateNewDisk, self._ModifyDisk,
-                        self._RemoveDisk)
+                        self._RemoveDisk, post_add_fn=self._PostAddDisk)
     _UpdateIvNames(0, self.instance.disks)
 
     if self.op.disk_template:
index ab3fbab..32eaf25 100644 (file)
@@ -480,7 +480,6 @@ class TLMigrateInstance(Tasklet):
     while not all_done:
       all_done = True
       result = self.rpc.call_drbd_wait_sync(self.all_node_uuids,
-                                            self.nodes_ip,
                                             (self.instance.disks,
                                              self.instance))
       min_percent = 100
@@ -503,11 +502,8 @@ class TLMigrateInstance(Tasklet):
     self.feedback_fn("* switching node %s to secondary mode" %
                      self.cfg.GetNodeName(node_uuid))
 
-    for dev in self.instance.disks:
-      self.cfg.SetDiskID(dev, node_uuid)
-
     result = self.rpc.call_blockdev_close(node_uuid, self.instance.name,
-                                          self.instance.disks)
+                                          (self.instance.disks, self.instance))
     result.Raise("Cannot change disk to secondary on node %s" %
                  self.cfg.GetNodeName(node_uuid))
 
@@ -516,9 +512,8 @@ class TLMigrateInstance(Tasklet):
 
     """
     self.feedback_fn("* changing into standalone mode")
-    result = self.rpc.call_drbd_disconnect_net(self.all_node_uuids,
-                                               self.nodes_ip,
-                                               self.instance.disks)
+    result = self.rpc.call_drbd_disconnect_net(
+               self.all_node_uuids, (self.instance.disks, self.instance))
     for node_uuid, nres in result.items():
       nres.Raise("Cannot disconnect disks node %s" %
                  self.cfg.GetNodeName(node_uuid))
@@ -532,7 +527,7 @@ class TLMigrateInstance(Tasklet):
     else:
       msg = "single-master"
     self.feedback_fn("* changing disks into %s mode" % msg)
-    result = self.rpc.call_drbd_attach_net(self.all_node_uuids, self.nodes_ip,
+    result = self.rpc.call_drbd_attach_net(self.all_node_uuids,
                                            (self.instance.disks, self.instance),
                                            self.instance.name, multimaster)
     for node_uuid, nres in result.items():
index 54d58f2..e58f942 100644 (file)
@@ -230,9 +230,10 @@ class LUInstanceShutdown(LogicalUnit):
       assert self.op.ignore_offline_nodes
       self.LogInfo("Primary node offline, marked instance as stopped")
     else:
-      result = self.rpc.call_instance_shutdown(self.instance.primary_node,
-                                               self.instance,
-                                               self.op.timeout, self.op.reason)
+      result = self.rpc.call_instance_shutdown(
+        self.instance.primary_node,
+        self.instance,
+        self.op.timeout, self.op.reason)
       msg = result.fail_msg
       if msg:
         self.LogWarning("Could not shutdown instance: %s", msg)
@@ -393,8 +394,6 @@ class LUInstanceReboot(LogicalUnit):
     if instance_running and \
         self.op.reboot_type in [constants.INSTANCE_REBOOT_SOFT,
                                 constants.INSTANCE_REBOOT_HARD]:
-      for disk in self.instance.disks:
-        self.cfg.SetDiskID(disk, current_node_uuid)
       result = self.rpc.call_instance_reboot(current_node_uuid, self.instance,
                                              self.op.reboot_type,
                                              self.op.shutdown_timeout,
index ee9ba13..b31cf41 100644 (file)
@@ -308,9 +308,7 @@ class LUInstanceQueryData(NoHooksLU):
     if self.op.static or not node_uuid:
       return None
 
-    self.cfg.SetDiskID(dev, node_uuid)
-
-    result = self.rpc.call_blockdev_find(node_uuid, dev)
+    result = self.rpc.call_blockdev_find(node_uuid, (dev, instance))
     if result.offline:
       return None
 
@@ -373,7 +371,6 @@ class LUInstanceQueryData(NoHooksLU):
       "dev_type": dev.dev_type,
       "logical_id": dev.logical_id,
       "drbd_info": drbd_info,
-      "physical_id": dev.physical_id,
       "pstatus": dev_pstatus,
       "sstatus": dev_sstatus,
       "children": dev_children,
index 6f6cdd5..d971f16 100644 (file)
@@ -79,16 +79,13 @@ def CreateSingleBlockDev(lu, node_uuid, instance, device, info, force_open,
   @param excl_stor: Whether exclusive_storage is active for the node
 
   """
-  lu.cfg.SetDiskID(device, node_uuid)
-  result = lu.rpc.call_blockdev_create(node_uuid, device, device.size,
-                                       instance.name, force_open, info,
-                                       excl_stor)
+  result = lu.rpc.call_blockdev_create(node_uuid, (device, instance),
+                                       device.size, instance.name, force_open,
+                                       info, excl_stor)
   result.Raise("Can't create block device %s on"
                " node %s for instance %s" % (device,
                                              lu.cfg.GetNodeName(node_uuid),
                                              instance.name))
-  if device.physical_id is None:
-    device.physical_id = result.payload
 
 
 def _CreateBlockDevInner(lu, node_uuid, instance, device, force_create,
@@ -184,7 +181,7 @@ def _CreateBlockDev(lu, node_uuid, instance, device, force_create, info,
                               force_open, excl_stor)
 
 
-def _UndoCreateDisks(lu, disks_created):
+def _UndoCreateDisks(lu, disks_created, instance):
   """Undo the work performed by L{CreateDisks}.
 
   This function is called in case of an error to undo the work of
@@ -193,11 +190,12 @@ def _UndoCreateDisks(lu, disks_created):
   @type lu: L{LogicalUnit}
   @param lu: the logical unit on whose behalf we execute
   @param disks_created: the result returned by L{CreateDisks}
+  @type instance: L{objects.Instance}
+  @param instance: the instance for which disks were created
 
   """
   for (node_uuid, disk) in disks_created:
-    lu.cfg.SetDiskID(disk, node_uuid)
-    result = lu.rpc.call_blockdev_remove(node_uuid, disk)
+    result = lu.rpc.call_blockdev_remove(node_uuid, (disk, instance))
     result.Warn("Failed to remove newly-created disk %s on node %s" %
                 (disk, lu.cfg.GetNodeName(node_uuid)), logging.warning)
 
@@ -259,7 +257,7 @@ def CreateDisks(lu, instance, to_skip=None, target_node_uuid=None, disks=None):
         logging.warning("Creating disk %s for instance '%s' failed",
                         idx, instance.name)
         disks_created.extend(e.created_devices)
-        _UndoCreateDisks(lu, disks_created)
+        _UndoCreateDisks(lu, disks_created, instance)
         raise errors.OpExecError(e.message)
   return disks_created
 
@@ -934,9 +932,9 @@ def _CheckNodesFreeDiskOnVG(lu, node_uuids, vg, requested):
 
   """
   nodeinfo = _PerformNodeInfoCall(lu, node_uuids, vg)
-  for node in node_uuids:
-    node_name = lu.cfg.GetNodeName(node)
-    info = nodeinfo[node]
+  for node_uuid in node_uuids:
+    node_name = lu.cfg.GetNodeName(node_uuid)
+    info = nodeinfo[node_uuid]
     info.Raise("Cannot get current information from node %s" % node_name,
                prereq=True, ecode=errors.ECODE_ENVIRON)
     _CheckVgCapacityForNode(node_name, info.payload, vg, requested)
@@ -1014,9 +1012,6 @@ def WipeDisks(lu, instance, disks=None):
     disks = [(idx, disk, 0)
              for (idx, disk) in enumerate(instance.disks)]
 
-  for (_, device, _) in disks:
-    lu.cfg.SetDiskID(device, node_uuid)
-
   logging.info("Pausing synchronization of disks of instance '%s'",
                instance.name)
   result = lu.rpc.call_blockdev_pause_resume_sync(node_uuid,
@@ -1110,7 +1105,7 @@ def WipeOrCleanupDisks(lu, instance, disks=None, cleanup=None):
   except errors.OpExecError:
     logging.warning("Wiping disks for instance '%s' failed",
                     instance.name)
-    _UndoCreateDisks(lu, cleanup)
+    _UndoCreateDisks(lu, cleanup, instance)
     raise
 
 
@@ -1148,9 +1143,6 @@ def WaitForSync(lu, instance, disks=None, oneshot=False):
   node_uuid = instance.primary_node
   node_name = lu.cfg.GetNodeName(node_uuid)
 
-  for dev in disks:
-    lu.cfg.SetDiskID(dev, node_uuid)
-
   # TODO: Convert to utils.Retry
 
   retries = 0
@@ -1219,13 +1211,15 @@ def ShutdownInstanceDisks(lu, instance, disks=None, ignore_primary=False):
   ignored.
 
   """
-  lu.cfg.MarkInstanceDisksInactive(instance.uuid)
   all_result = True
+
+  if disks is None:
+    # only mark instance disks as inactive if all disks are affected
+    lu.cfg.MarkInstanceDisksInactive(instance.uuid)
   disks = ExpandCheckDisks(instance, disks)
 
   for disk in disks:
     for node_uuid, top_disk in disk.ComputeNodeTree(instance.primary_node):
-      lu.cfg.SetDiskID(top_disk, node_uuid)
       result = lu.rpc.call_blockdev_shutdown(node_uuid, (top_disk, instance))
       msg = result.fail_msg
       if msg:
@@ -1249,7 +1243,7 @@ def _SafeShutdownInstanceDisks(lu, instance, disks=None):
 
 
 def AssembleInstanceDisks(lu, instance, disks=None, ignore_secondaries=False,
-                           ignore_size=False):
+                          ignore_size=False):
   """Prepare the block devices for an instance.
 
   This sets up the block devices on all nodes.
@@ -1274,6 +1268,11 @@ def AssembleInstanceDisks(lu, instance, disks=None, ignore_secondaries=False,
   """
   device_info = []
   disks_ok = True
+
+  if disks is None:
+    # only mark instance disks as active if all disks are affected
+    lu.cfg.MarkInstanceDisksActive(instance.uuid)
+
   disks = ExpandCheckDisks(instance, disks)
 
   # With the two passes mechanism we try to reduce the window of
@@ -1285,10 +1284,6 @@ def AssembleInstanceDisks(lu, instance, disks=None, ignore_secondaries=False,
   # into any other network-connected state (Connected, SyncTarget,
   # SyncSource, etc.)
 
-  # mark instance disks as active before doing actual work, so watcher does
-  # not try to shut them down erroneously
-  lu.cfg.MarkInstanceDisksActive(instance.uuid)
-
   # 1st pass, assemble on all nodes in secondary mode
   for idx, inst_disk in enumerate(disks):
     for node_uuid, node_disk in inst_disk.ComputeNodeTree(
@@ -1296,7 +1291,6 @@ def AssembleInstanceDisks(lu, instance, disks=None, ignore_secondaries=False,
       if ignore_size:
         node_disk = node_disk.Copy()
         node_disk.UnsetSize()
-      lu.cfg.SetDiskID(node_disk, node_uuid)
       result = lu.rpc.call_blockdev_assemble(node_uuid, (node_disk, instance),
                                              instance.name, False, idx)
       msg = result.fail_msg
@@ -1322,7 +1316,6 @@ def AssembleInstanceDisks(lu, instance, disks=None, ignore_secondaries=False,
       if ignore_size:
         node_disk = node_disk.Copy()
         node_disk.UnsetSize()
-      lu.cfg.SetDiskID(node_disk, node_uuid)
       result = lu.rpc.call_blockdev_assemble(node_uuid, (node_disk, instance),
                                              instance.name, True, idx)
       msg = result.fail_msg
@@ -1332,17 +1325,11 @@ def AssembleInstanceDisks(lu, instance, disks=None, ignore_secondaries=False,
                       inst_disk.iv_name, lu.cfg.GetNodeName(node_uuid), msg)
         disks_ok = False
       else:
-        dev_path = result.payload
+        dev_path, _ = result.payload
 
     device_info.append((lu.cfg.GetNodeName(instance.primary_node),
                         inst_disk.iv_name, dev_path))
 
-  # leave the disks configured for the primary node
-  # this is a workaround that would be fixed better by
-  # improving the logical/physical id handling
-  for disk in disks:
-    lu.cfg.SetDiskID(disk, instance.primary_node)
-
   if not disks_ok:
     lu.cfg.MarkInstanceDisksInactive(instance.uuid)
 
@@ -1479,7 +1466,6 @@ class LUInstanceGrowDisk(LogicalUnit):
 
     # First run all grow ops in dry-run mode
     for node_uuid in self.instance.all_nodes:
-      self.cfg.SetDiskID(self.disk, node_uuid)
       result = self.rpc.call_blockdev_grow(node_uuid,
                                            (self.disk, self.instance),
                                            self.delta, True, True,
@@ -1489,9 +1475,8 @@ class LUInstanceGrowDisk(LogicalUnit):
 
     if wipe_disks:
       # Get disk size from primary node for wiping
-      self.cfg.SetDiskID(self.disk, self.instance.primary_node)
-      result = self.rpc.call_blockdev_getdimensions(self.instance.primary_node,
-                                                    [self.disk])
+      result = self.rpc.call_blockdev_getdimensions(
+                 self.instance.primary_node, [([self.disk], self.instance)])
       result.Raise("Failed to retrieve disk size from node '%s'" %
                    self.instance.primary_node)
 
@@ -1513,7 +1498,6 @@ class LUInstanceGrowDisk(LogicalUnit):
     # We know that (as far as we can test) operations across different
     # nodes will succeed, time to run it for real on the backing storage
     for node_uuid in self.instance.all_nodes:
-      self.cfg.SetDiskID(self.disk, node_uuid)
       result = self.rpc.call_blockdev_grow(node_uuid,
                                            (self.disk, self.instance),
                                            self.delta, False, True,
@@ -1523,7 +1507,6 @@ class LUInstanceGrowDisk(LogicalUnit):
 
     # And now execute it for logical storage, on the primary node
     node_uuid = self.instance.primary_node
-    self.cfg.SetDiskID(self.disk, node_uuid)
     result = self.rpc.call_blockdev_grow(node_uuid, (self.disk, self.instance),
                                          self.delta, False, False,
                                          self.node_es_flags[node_uuid])
@@ -1796,12 +1779,10 @@ def _CheckDiskConsistencyInner(lu, instance, dev, node_uuid, on_primary,
   the device(s)) to the ldisk (representing the local storage status).
 
   """
-  lu.cfg.SetDiskID(dev, node_uuid)
-
   result = True
 
   if on_primary or dev.AssembleOnSecondary():
-    rstats = lu.rpc.call_blockdev_find(node_uuid, dev)
+    rstats = lu.rpc.call_blockdev_find(node_uuid, (dev, instance))
     msg = rstats.fail_msg
     if msg:
       lu.LogWarning("Can't find disk on node %s: %s",
@@ -1844,7 +1825,7 @@ def _BlockdevFind(lu, node_uuid, dev, instance):
 
   """
   (disk,) = AnnotateDiskParams(instance, [dev], lu.cfg)
-  return lu.rpc.call_blockdev_find(node_uuid, disk)
+  return lu.rpc.call_blockdev_find(node_uuid, (disk, instance))
 
 
 def _GenerateUniqueNames(lu, exts):
@@ -1941,7 +1922,6 @@ class TLReplaceDisks(Tasklet):
       for node_uuid in node_uuids:
         self.lu.LogInfo("Checking disk/%d on %s", idx,
                         self.cfg.GetNodeName(node_uuid))
-        self.cfg.SetDiskID(dev, node_uuid)
 
         result = _BlockdevFind(self, node_uuid, dev, instance)
 
@@ -2201,7 +2181,6 @@ class TLReplaceDisks(Tasklet):
       for node_uuid in node_uuids:
         self.lu.LogInfo("Checking disk/%d on %s", idx,
                         self.cfg.GetNodeName(node_uuid))
-        self.cfg.SetDiskID(dev, node_uuid)
 
         result = _BlockdevFind(self, node_uuid, dev, self.instance)
 
@@ -2244,8 +2223,6 @@ class TLReplaceDisks(Tasklet):
       self.lu.LogInfo("Adding storage on %s for disk/%d",
                       self.cfg.GetNodeName(node_uuid), idx)
 
-      self.cfg.SetDiskID(dev, node_uuid)
-
       lv_names = [".disk%d_%s" % (idx, suffix) for suffix in ["data", "meta"]]
       names = _GenerateUniqueNames(self.lu, lv_names)
 
@@ -2278,8 +2255,6 @@ class TLReplaceDisks(Tasklet):
 
   def _CheckDevices(self, node_uuid, iv_names):
     for name, (dev, _, _) in iv_names.iteritems():
-      self.cfg.SetDiskID(dev, node_uuid)
-
       result = _BlockdevFind(self, node_uuid, dev, self.instance)
 
       msg = result.fail_msg
@@ -2297,9 +2272,8 @@ class TLReplaceDisks(Tasklet):
       self.lu.LogInfo("Remove logical volumes for %s", name)
 
       for lv in old_lvs:
-        self.cfg.SetDiskID(lv, node_uuid)
-
-        msg = self.rpc.call_blockdev_remove(node_uuid, lv).fail_msg
+        msg = self.rpc.call_blockdev_remove(node_uuid, (lv, self.instance)) \
+                .fail_msg
         if msg:
           self.lu.LogWarning("Can't remove old LV: %s", msg,
                              hint="remove unused LVs manually")
@@ -2348,8 +2322,9 @@ class TLReplaceDisks(Tasklet):
     for dev, old_lvs, new_lvs in iv_names.itervalues():
       self.lu.LogInfo("Detaching %s drbd from local storage", dev.iv_name)
 
-      result = self.rpc.call_blockdev_removechildren(self.target_node_uuid, dev,
-                                                     old_lvs)
+      result = self.rpc.call_blockdev_removechildren(self.target_node_uuid,
+                                                     (dev, self.instance),
+                                                     (old_lvs, self.instance))
       result.Raise("Can't detach drbd from local storage on node"
                    " %s for device %s" %
                    (self.cfg.GetNodeName(self.target_node_uuid), dev.iv_name))
@@ -2359,18 +2334,18 @@ class TLReplaceDisks(Tasklet):
       # ok, we created the new LVs, so now we know we have the needed
       # storage; as such, we proceed on the target node to rename
       # old_lv to _old, and new_lv to old_lv; note that we rename LVs
-      # using the assumption that logical_id == physical_id (which in
-      # turn is the unique_id on that node)
+      # using the assumption that logical_id == unique_id on that node
 
       # FIXME(iustin): use a better name for the replaced LVs
       temp_suffix = int(time.time())
-      ren_fn = lambda d, suff: (d.physical_id[0],
-                                d.physical_id[1] + "_replaced-%s" % suff)
+      ren_fn = lambda d, suff: (d.logical_id[0],
+                                d.logical_id[1] + "_replaced-%s" % suff)
 
       # Build the rename list based on what LVs exist on the node
       rename_old_to_new = []
       for to_ren in old_lvs:
-        result = self.rpc.call_blockdev_find(self.target_node_uuid, to_ren)
+        result = self.rpc.call_blockdev_find(self.target_node_uuid,
+                                             (to_ren, self.instance))
         if not result.fail_msg and result.payload:
           # device exists
           rename_old_to_new.append((to_ren, ren_fn(to_ren, temp_suffix)))
@@ -2383,7 +2358,7 @@ class TLReplaceDisks(Tasklet):
 
       # Now we rename the new LVs to the old LVs
       self.lu.LogInfo("Renaming the new LVs on the target node")
-      rename_new_to_old = [(new, old.physical_id)
+      rename_new_to_old = [(new, old.logical_id)
                            for old, new in zip(old_lvs, new_lvs)]
       result = self.rpc.call_blockdev_rename(self.target_node_uuid,
                                              rename_new_to_old)
@@ -2393,25 +2368,24 @@ class TLReplaceDisks(Tasklet):
       # Intermediate steps of in memory modifications
       for old, new in zip(old_lvs, new_lvs):
         new.logical_id = old.logical_id
-        self.cfg.SetDiskID(new, self.target_node_uuid)
 
       # We need to modify old_lvs so that removal later removes the
       # right LVs, not the newly added ones; note that old_lvs is a
       # copy here
       for disk in old_lvs:
         disk.logical_id = ren_fn(disk, temp_suffix)
-        self.cfg.SetDiskID(disk, self.target_node_uuid)
 
       # 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.cfg.GetNodeName(self.target_node_uuid))
       result = self.rpc.call_blockdev_addchildren(self.target_node_uuid,
-                                                  (dev, self.instance), new_lvs)
+                                                  (dev, self.instance),
+                                                  (new_lvs, self.instance))
       msg = result.fail_msg
       if msg:
         for new_lv in new_lvs:
           msg2 = self.rpc.call_blockdev_remove(self.target_node_uuid,
-                                               new_lv).fail_msg
+                                               (new_lv, self.instance)).fail_msg
           if msg2:
             self.lu.LogWarning("Can't rollback device %s: %s", dev, msg2,
                                hint=("cleanup manually the unused logical"
@@ -2550,7 +2524,6 @@ class TLReplaceDisks(Tasklet):
     # We have new devices, shutdown the drbd on the old secondary
     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_uuid)
       msg = self.rpc.call_blockdev_shutdown(self.target_node_uuid,
                                             (dev, self.instance)).fail_msg
       if msg:
@@ -2560,8 +2533,8 @@ class TLReplaceDisks(Tasklet):
                                  " soon as possible"))
 
     self.lu.LogInfo("Detaching primary drbds from the network (=> standalone)")
-    result = self.rpc.call_drbd_disconnect_net([pnode], self.node_secondary_ip,
-                                               self.instance.disks)[pnode]
+    result = self.rpc.call_drbd_disconnect_net(
+               [pnode], (self.instance.disks, self.instance))[pnode]
 
     msg = result.fail_msg
     if msg:
@@ -2575,7 +2548,6 @@ class TLReplaceDisks(Tasklet):
     self.lu.LogInfo("Updating instance configuration")
     for dev, _, new_logical_id in iv_names.itervalues():
       dev.logical_id = new_logical_id
-      self.cfg.SetDiskID(dev, self.instance.primary_node)
 
     self.cfg.Update(self.instance, feedback_fn)
 
@@ -2587,7 +2559,6 @@ class TLReplaceDisks(Tasklet):
                     " (standalone => connected)")
     result = self.rpc.call_drbd_attach_net([self.instance.primary_node,
                                             self.new_node_uuid],
-                                           self.node_secondary_ip,
                                            (self.instance.disks, self.instance),
                                            self.instance.name,
                                            False)
index f4bfa09..752e66d 100644 (file)
@@ -59,8 +59,8 @@ def BuildInstanceHookEnv(name, primary_node_name, secondary_node_names, os_type,
   @type vcpus: string
   @param vcpus: the count of VCPUs the instance has
   @type nics: list
-  @param nics: list of tuples (name, uuid, ip, mac, mode, link, net, netinfo)
-      representing the NICs the instance has
+  @param nics: list of tuples (name, uuid, ip, mac, mode, link, vlan, net,
+      netinfo) representing the NICs the instance has
   @type disk_template: string
   @param disk_template: the disk template of the instance
   @type disks: list
@@ -94,7 +94,8 @@ def BuildInstanceHookEnv(name, primary_node_name, secondary_node_names, os_type,
     }
   if nics:
     nic_count = len(nics)
-    for idx, (name, uuid, ip, mac, mode, link, net, netinfo) in enumerate(nics):
+    for idx, (name, uuid, ip, mac, mode, link, vlan, net, netinfo) \
+        in enumerate(nics):
       if ip is None:
         ip = ""
       if name:
@@ -104,6 +105,7 @@ def BuildInstanceHookEnv(name, primary_node_name, secondary_node_names, os_type,
       env["INSTANCE_NIC%d_MAC" % idx] = mac
       env["INSTANCE_NIC%d_MODE" % idx] = mode
       env["INSTANCE_NIC%d_LINK" % idx] = link
+      env["INSTANCE_NIC%d_VLAN" % idx] = vlan
       if netinfo:
         nobj = objects.Network.FromDict(netinfo)
         env.update(nobj.HooksDict("INSTANCE_NIC%d_" % idx))
@@ -112,7 +114,8 @@ def BuildInstanceHookEnv(name, primary_node_name, secondary_node_names, os_type,
         # network, but the relevant network entry was not in the config. This
         # should be made impossible.
         env["INSTANCE_NIC%d_NETWORK_NAME" % idx] = net
-      if mode == constants.NIC_MODE_BRIDGED:
+      if mode == constants.NIC_MODE_BRIDGED or \
+         mode == constants.NIC_MODE_OVS:
         env["INSTANCE_NIC%d_BRIDGE" % idx] = link
   else:
     nic_count = 0
@@ -271,8 +274,7 @@ def RemoveDisks(lu, instance, target_node_uuid=None, ignore_failures=False):
     else:
       edata = device.ComputeNodeTree(instance.primary_node)
     for node_uuid, disk in edata:
-      lu.cfg.SetDiskID(disk, node_uuid)
-      result = lu.rpc.call_blockdev_remove(node_uuid, disk)
+      result = lu.rpc.call_blockdev_remove(node_uuid, (disk, instance))
       if result.fail_msg:
         lu.LogWarning("Could not remove disk %s on node %s,"
                       " continuing anyway: %s", idx,
@@ -318,11 +320,13 @@ def NICToTuple(lu, nic):
   filled_params = cluster.SimpleFillNIC(nic.nicparams)
   mode = filled_params[constants.NIC_MODE]
   link = filled_params[constants.NIC_LINK]
+  vlan = filled_params[constants.NIC_VLAN]
   netinfo = None
   if nic.network:
     nobj = lu.cfg.GetNetwork(nic.network)
     netinfo = objects.Network.ToDict(nobj)
-  return (nic.name, nic.uuid, nic.ip, nic.mac, mode, link, nic.network, netinfo)
+  return (nic.name, nic.uuid, nic.ip, nic.mac, mode, link, vlan,
+          nic.network, netinfo)
 
 
 def NICListToTuple(lu, nics):
index e83049d..89155ac 100644 (file)
@@ -112,6 +112,23 @@ class LUNodeAdd(LogicalUnit):
       raise errors.OpPrereqError("Cannot pass a node group when a node is"
                                  " being readded", errors.ECODE_INVAL)
 
+    if self.op.ndparams:
+      ovs = self.op.ndparams.get(constants.ND_OVS, None)
+      ovs_name = self.op.ndparams.get(constants.ND_OVS_NAME, None)
+      ovs_link = self.op.ndparams.get(constants.ND_OVS_LINK, None)
+
+      # OpenvSwitch: Warn user if link is missing
+      if ovs and not ovs_link:
+        self.LogInfo("No physical interface for OpenvSwitch was given."
+                     " OpenvSwitch will not have an outside connection. This"
+                     " might not be what you want.")
+      # OpenvSwitch: Fail if parameters are given, but OVS is not enabled.
+      if not ovs and (ovs_name or ovs_link):
+        raise errors.OpPrereqError("OpenvSwitch name or link were given, but"
+                                   " OpenvSwitch is not enabled. Please enable"
+                                   " OpenvSwitch with --ovs",
+                                   errors.ECODE_INVAL)
+
   def BuildHooksEnv(self):
     """Build hooks env.
 
@@ -217,7 +234,7 @@ class LUNodeAdd(LogicalUnit):
 
     # check that the type of the node (single versus dual homed) is the
     # same as for the master
-    myself = self.cfg.GetNodeInfo(self.cfg.GetMasterNode())
+    myself = self.cfg.GetMasterNodeInfo()
     master_singlehomed = myself.secondary_ip == myself.primary_ip
     newbie_singlehomed = secondary_ip == self.op.primary_ip
     if master_singlehomed != newbie_singlehomed:
@@ -375,6 +392,15 @@ class LUNodeAdd(LogicalUnit):
                       (verifier, nl_payload[failed]))
         raise errors.OpExecError("ssh/hostname verification failed")
 
+    # OpenvSwitch initialization on the node
+    ovs = self.new_node.ndparams.get(constants.ND_OVS, None)
+    ovs_name = self.new_node.ndparams.get(constants.ND_OVS_NAME, None)
+    ovs_link = self.new_node.ndparams.get(constants.ND_OVS_LINK, None)
+
+    if ovs:
+      result = self.rpc.call_node_configure_ovs(
+                 self.new_node.name, ovs_name, ovs_link)
+
     if self.op.readd:
       self.context.ReaddNode(self.new_node)
       RedistributeAncillaryFiles(self)
@@ -635,7 +661,7 @@ class LUNodeSetParams(LogicalUnit):
     # restrictions.
     if self.op.secondary_ip:
       # Ok even without locking, because this can't be changed by any LU
-      master = self.cfg.GetNodeInfo(self.cfg.GetMasterNode())
+      master = self.cfg.GetMasterNodeInfo()
       master_singlehomed = master.secondary_ip == master.primary_ip
       if master_singlehomed and self.op.secondary_ip != node.primary_ip:
         if self.op.force and node.uuid == master.uuid:
@@ -831,15 +857,6 @@ 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")
 
@@ -996,8 +1013,7 @@ class LUNodeEvacuate(NoHooksLU):
 
     elif self.op.iallocator is not None:
       # TODO: Implement relocation to other group
-      evac_mode = self._MODE2IALLOCATOR[self.op.mode]
-      req = iallocator.IAReqNodeEvac(evac_mode=evac_mode,
+      req = iallocator.IAReqNodeEvac(evac_mode=self.op.mode,
                                      instances=list(self.instance_names))
       ial = iallocator.IAllocator(self.cfg, self.rpc, req)
 
@@ -1189,13 +1205,9 @@ class NodeQuery(QueryBase):
       # filter out non-vm_capable nodes
       toquery_node_uuids = [node.uuid for node in all_info.values()
                             if node.vm_capable and node.uuid in node_uuids]
-      lvm_enabled = utils.storage.IsLvmEnabled(
-          lu.cfg.GetClusterInfo().enabled_disk_templates)
-      # FIXME: this per default asks for storage space information for all
-      # enabled disk templates. Fix this by making it possible to specify
-      # space report fields for specific disk templates.
-      raw_storage_units = utils.storage.GetStorageUnitsOfCluster(
-          lu.cfg, include_spindles=lvm_enabled)
+      default_template = lu.cfg.GetClusterInfo().enabled_disk_templates[0]
+      raw_storage_units = utils.storage.GetStorageUnits(
+          lu.cfg, [default_template])
       storage_units = rpc.PrepareStorageUnitsForNodes(
           lu.cfg, raw_storage_units, toquery_node_uuids)
       default_hypervisor = lu.cfg.GetHypervisorType()
@@ -1204,8 +1216,7 @@ class NodeQuery(QueryBase):
       node_data = lu.rpc.call_node_info(toquery_node_uuids, storage_units,
                                         hvspecs)
       live_data = dict(
-          (uuid, rpc.MakeLegacyNodeInfo(nresult.payload,
-                                        require_spindles=lvm_enabled))
+          (uuid, rpc.MakeLegacyNodeInfo(nresult.payload, default_template))
           for (uuid, nresult) in node_data.items()
           if not nresult.fail_msg and nresult.payload)
     else:
@@ -1269,20 +1280,16 @@ class LUNodeQuery(NoHooksLU):
     return self.nq.OldStyleQuery(self)
 
 
-def _CheckOutputFields(static, dynamic, selected):
-  """Checks whether all selected fields are valid.
+def _CheckOutputFields(fields, selected):
+  """Checks whether all selected fields are valid according to fields.
 
-  @type static: L{utils.FieldSet}
-  @param static: static fields set
-  @type dynamic: L{utils.FieldSet}
-  @param dynamic: dynamic fields set
+  @type fields: L{utils.FieldSet}
+  @param fields: fields set
+  @type selected: L{utils.FieldSet}
+  @param selected: fields set
 
   """
-  f = utils.FieldSet()
-  f.Extend(static)
-  f.Extend(dynamic)
-
-  delta = f.NonMatching(selected)
+  delta = fields.NonMatching(selected)
   if delta:
     raise errors.OpPrereqError("Unknown output fields selected: %s"
                                % ",".join(delta), errors.ECODE_INVAL)
@@ -1293,13 +1300,12 @@ class LUNodeQueryvols(NoHooksLU):
 
   """
   REQ_BGL = False
-  _FIELDS_DYNAMIC = utils.FieldSet("phys", "vg", "name", "size", "instance")
-  _FIELDS_STATIC = utils.FieldSet("node")
 
   def CheckArguments(self):
-    _CheckOutputFields(static=self._FIELDS_STATIC,
-                       dynamic=self._FIELDS_DYNAMIC,
-                       selected=self.op.output_fields)
+    _CheckOutputFields(utils.FieldSet(constants.VF_NODE, constants.VF_PHYS,
+                                      constants.VF_VG, constants.VF_NAME,
+                                      constants.VF_SIZE, constants.VF_INSTANCE),
+                       self.op.output_fields)
 
   def ExpandNames(self):
     self.share_locks = ShareAll()
@@ -1336,24 +1342,24 @@ class LUNodeQueryvols(NoHooksLU):
         continue
 
       node_vols = sorted(nresult.payload,
-                         key=operator.itemgetter("dev"))
+                         key=operator.itemgetter(constants.VF_DEV))
 
       for vol in node_vols:
         node_output = []
         for field in self.op.output_fields:
-          if field == "node":
+          if field == constants.VF_NODE:
             val = self.cfg.GetNodeName(node_uuid)
-          elif field == "phys":
-            val = vol["dev"]
-          elif field == "vg":
-            val = vol["vg"]
-          elif field == "name":
-            val = vol["name"]
-          elif field == "size":
-            val = int(float(vol["size"]))
-          elif field == "instance":
-            inst = vol2inst.get((node_uuid, vol["vg"] + "/" + vol["name"]),
-                                None)
+          elif field == constants.VF_PHYS:
+            val = vol[constants.VF_DEV]
+          elif field == constants.VF_VG:
+            val = vol[constants.VF_VG]
+          elif field == constants.VF_NAME:
+            val = vol[constants.VF_NAME]
+          elif field == constants.VF_SIZE:
+            val = int(float(vol[constants.VF_SIZE]))
+          elif field == constants.VF_INSTANCE:
+            inst = vol2inst.get((node_uuid, vol[constants.VF_VG] + "/" +
+                                 vol[constants.VF_NAME]), None)
             if inst is not None:
               val = inst.name
             else:
@@ -1371,13 +1377,11 @@ class LUNodeQueryStorage(NoHooksLU):
   """Logical unit for getting information on storage units on node(s).
 
   """
-  _FIELDS_STATIC = utils.FieldSet(constants.SF_NODE)
   REQ_BGL = False
 
   def CheckArguments(self):
-    _CheckOutputFields(static=self._FIELDS_STATIC,
-                       dynamic=utils.FieldSet(*constants.VALID_STORAGE_FIELDS),
-                       selected=self.op.output_fields)
+    _CheckOutputFields(utils.FieldSet(*constants.VALID_STORAGE_FIELDS),
+                       self.op.output_fields)
 
   def ExpandNames(self):
     self.share_locks = ShareAll()
@@ -1392,16 +1396,41 @@ class LUNodeQueryStorage(NoHooksLU):
         locking.LEVEL_NODE_ALLOC: locking.ALL_SET,
         }
 
+  def _DetermineStorageType(self):
+    """Determines the default storage type of the cluster.
+
+    """
+    enabled_disk_templates = self.cfg.GetClusterInfo().enabled_disk_templates
+    default_storage_type = \
+        constants.MAP_DISK_TEMPLATE_STORAGE_TYPE[enabled_disk_templates[0]]
+    return default_storage_type
+
   def CheckPrereq(self):
     """Check prerequisites.
 
     """
-    CheckStorageTypeEnabled(self.cfg.GetClusterInfo(), self.op.storage_type)
+    if self.op.storage_type:
+      CheckStorageTypeEnabled(self.cfg.GetClusterInfo(), self.op.storage_type)
+      self.storage_type = self.op.storage_type
+    else:
+      self.storage_type = self._DetermineStorageType()
+      if self.storage_type not in constants.STS_REPORT:
+        raise errors.OpPrereqError(
+            "Storage reporting for storage type '%s' is not supported. Please"
+            " use the --storage-type option to specify one of the supported"
+            " storage types (%s) or set the default disk template to one that"
+            " supports storage reporting." %
+            (self.storage_type, utils.CommaJoin(constants.STS_REPORT)))
 
   def Exec(self, feedback_fn):
     """Computes the list of nodes and their attributes.
 
     """
+    if self.op.storage_type:
+      self.storage_type = self.op.storage_type
+    else:
+      self.storage_type = self._DetermineStorageType()
+
     self.node_uuids = self.owned_locks(locking.LEVEL_NODE)
 
     # Always get name to sort by
@@ -1418,9 +1447,9 @@ class LUNodeQueryStorage(NoHooksLU):
     field_idx = dict([(name, idx) for (idx, name) in enumerate(fields)])
     name_idx = field_idx[constants.SF_NAME]
 
-    st_args = _GetStorageTypeArgs(self.cfg, self.op.storage_type)
+    st_args = _GetStorageTypeArgs(self.cfg, self.storage_type)
     data = self.rpc.call_storage_list(self.node_uuids,
-                                      self.op.storage_type, st_args,
+                                      self.storage_type, st_args,
                                       self.op.name, fields)
 
     result = []
@@ -1448,7 +1477,7 @@ class LUNodeQueryStorage(NoHooksLU):
           if field == constants.SF_NODE:
             val = node_name
           elif field == constants.SF_TYPE:
-            val = self.op.storage_type
+            val = self.storage_type
           elif field in field_idx:
             val = row[field_idx[field]]
           else:
index 4b02898..3aad869 100644 (file)
@@ -66,7 +66,7 @@ class LUTestDelay(NoHooksLU):
 
     """
     if self.op.on_master:
-      if not utils.TestDelay(self.op.duration):
+      if not utils.TestDelay(self.op.duration)[0]:
         raise errors.OpExecError("Error during master delay test")
     if self.op.on_node_uuids:
       result = self.rpc.call_test_delay(self.op.on_node_uuids, self.op.duration)
@@ -237,21 +237,10 @@ class LUTestAllocator(NoHooksLU):
     """
     if self.op.mode in (constants.IALLOCATOR_MODE_ALLOC,
                         constants.IALLOCATOR_MODE_MULTI_ALLOC):
-      for attr in ["memory", "disks", "disk_template",
-                   "os", "tags", "nics", "vcpus"]:
-        if not hasattr(self.op, attr):
-          raise errors.OpPrereqError("Missing attribute '%s' on opcode input" %
-                                     attr, errors.ECODE_INVAL)
       (self.inst_uuid, iname) = self.cfg.ExpandInstanceName(self.op.name)
       if iname is not None:
         raise errors.OpPrereqError("Instance '%s' already in the cluster" %
                                    iname, errors.ECODE_EXISTS)
-      if not isinstance(self.op.nics, list):
-        raise errors.OpPrereqError("Invalid parameter 'nics'",
-                                   errors.ECODE_INVAL)
-      if not isinstance(self.op.disks, list):
-        raise errors.OpPrereqError("Invalid parameter 'disks'",
-                                   errors.ECODE_INVAL)
       for row in self.op.disks:
         if (not isinstance(row, dict) or
             constants.IDISK_SIZE not in row or
@@ -263,10 +252,10 @@ class LUTestAllocator(NoHooksLU):
       if self.op.hypervisor is None:
         self.op.hypervisor = self.cfg.GetHypervisorType()
     elif self.op.mode == constants.IALLOCATOR_MODE_RELOC:
-      (fuuid, fname) = ExpandInstanceUuidAndName(self.cfg, None, self.op.name)
-      self.op.name = fname
+      (self.inst_uuid, self.op.name) = ExpandInstanceUuidAndName(self.cfg, None,
+                                                                 self.op.name)
       self.relocate_from_node_uuids = \
-          list(self.cfg.GetInstanceInfo(fuuid).secondary_nodes)
+          list(self.cfg.GetInstanceInfo(self.inst_uuid).secondary_nodes)
     elif self.op.mode in (constants.IALLOCATOR_MODE_CHG_GROUP,
                           constants.IALLOCATOR_MODE_NODE_EVAC):
       if not self.op.instances:
@@ -280,9 +269,6 @@ class LUTestAllocator(NoHooksLU):
       if self.op.iallocator is None:
         raise errors.OpPrereqError("Missing allocator name",
                                    errors.ECODE_INVAL)
-    elif self.op.direction != constants.IALLOCATOR_DIR_IN:
-      raise errors.OpPrereqError("Wrong allocator test '%s'" %
-                                 self.op.direction, errors.ECODE_INVAL)
 
   def Exec(self, feedback_fn):
     """Run the allocator test.
@@ -321,7 +307,8 @@ class LUTestAllocator(NoHooksLU):
                                              nics=self.op.nics,
                                              vcpus=self.op.vcpus,
                                              spindle_use=self.op.spindle_use,
-                                             hypervisor=self.op.hypervisor)
+                                             hypervisor=self.op.hypervisor,
+                                             node_whitelist=None)
                for idx in range(self.op.count)]
       req = iallocator.IAReqMultiInstanceAlloc(instances=insts)
     else:
index 8542a15..1f07d29 100644 (file)
@@ -167,7 +167,7 @@ def _CheckInstanceDiskIvNames(disks):
   return result
 
 
-class ConfigWriter:
+class ConfigWriter(object):
   """The interface to the cluster configuration.
 
   @ivar _temporary_lvs: reservation manager for temporary LVs
@@ -553,15 +553,13 @@ class ConfigWriter:
 
     return result
 
-  def _CheckDiskIDs(self, disk, l_ids, p_ids):
+  def _CheckDiskIDs(self, disk, l_ids):
     """Compute duplicate disk IDs
 
     @type disk: L{objects.Disk}
     @param disk: the disk at which to start searching
     @type l_ids: list
     @param l_ids: list of current logical ids
-    @type p_ids: list
-    @param p_ids: list of current physical ids
     @rtype: list
     @return: a list of error messages
 
@@ -572,15 +570,10 @@ class ConfigWriter:
         result.append("duplicate logical id %s" % str(disk.logical_id))
       else:
         l_ids.append(disk.logical_id)
-    if disk.physical_id is not None:
-      if disk.physical_id in p_ids:
-        result.append("duplicate physical id %s" % str(disk.physical_id))
-      else:
-        p_ids.append(disk.physical_id)
 
     if disk.children:
       for child in disk.children:
-        result.extend(self._CheckDiskIDs(child, l_ids, p_ids))
+        result.extend(self._CheckDiskIDs(child, l_ids))
     return result
 
   def _UnlockedVerifyConfig(self):
@@ -598,7 +591,6 @@ class ConfigWriter:
     data = self._config_data
     cluster = data.cluster
     seen_lids = []
-    seen_pids = []
 
     # global cluster checks
     if not cluster.enabled_hypervisors:
@@ -675,6 +667,16 @@ class ConfigWriter:
             constants.NDS_PARAMETER_TYPES)
     _helper_ipolicy("cluster", cluster.ipolicy, True)
 
+    if constants.DT_RBD in cluster.diskparams:
+      access = cluster.diskparams[constants.DT_RBD][constants.RBD_ACCESS]
+      if access not in constants.DISK_VALID_ACCESS_MODES:
+        result.append(
+          "Invalid value of '%s:%s': '%s' (expected one of %s)" % (
+            constants.DT_RBD, constants.RBD_ACCESS, access,
+            utils.CommaJoin(constants.DISK_VALID_ACCESS_MODES)
+          )
+        )
+
     # per-instance checks
     for instance_uuid in data.instances:
       instance = data.instances[instance_uuid]
@@ -729,7 +731,7 @@ class ConfigWriter:
       for idx, disk in enumerate(instance.disks):
         result.extend(["instance '%s' disk %d error: %s" %
                        (instance.name, idx, msg) for msg in disk.Verify()])
-        result.extend(self._CheckDiskIDs(disk, seen_lids, seen_pids))
+        result.extend(self._CheckDiskIDs(disk, seen_lids))
 
       wrong_names = _CheckInstanceDiskIvNames(instance.disks)
       if wrong_names:
@@ -873,57 +875,6 @@ class ConfigWriter:
     """
     return self._UnlockedVerifyConfig()
 
-  def _UnlockedSetDiskID(self, disk, node_uuid):
-    """Convert the unique ID to the ID needed on the target nodes.
-
-    This is used only for drbd, which needs ip/port configuration.
-
-    The routine descends down and updates its children also, because
-    this helps when the only the top device is passed to the remote
-    node.
-
-    This function is for internal use, when the config lock is already held.
-
-    """
-    if disk.children:
-      for child in disk.children:
-        self._UnlockedSetDiskID(child, node_uuid)
-
-    if disk.logical_id is None and disk.physical_id is not None:
-      return
-    if disk.dev_type == constants.DT_DRBD8:
-      pnode, snode, port, pminor, sminor, secret = disk.logical_id
-      if node_uuid not in (pnode, snode):
-        raise errors.ConfigurationError("DRBD device not knowing node %s" %
-                                        node_uuid)
-      pnode_info = self._UnlockedGetNodeInfo(pnode)
-      snode_info = self._UnlockedGetNodeInfo(snode)
-      if pnode_info is None or snode_info is None:
-        raise errors.ConfigurationError("Can't find primary or secondary node"
-                                        " for %s" % str(disk))
-      p_data = (pnode_info.secondary_ip, port)
-      s_data = (snode_info.secondary_ip, port)
-      if pnode == node_uuid:
-        disk.physical_id = p_data + s_data + (pminor, secret)
-      else: # it must be secondary, we tested above
-        disk.physical_id = s_data + p_data + (sminor, secret)
-    else:
-      disk.physical_id = disk.logical_id
-    return
-
-  @locking.ssynchronized(_config_lock)
-  def SetDiskID(self, disk, node_uuid):
-    """Convert the unique ID to the ID needed on the target nodes.
-
-    This is used only for drbd, which needs ip/port configuration.
-
-    The routine descends down and updates its children also, because
-    this helps when the only the top device is passed to the remote
-    node.
-
-    """
-    return self._UnlockedSetDiskID(disk, node_uuid)
-
   @locking.ssynchronized(_config_lock)
   def AddTcpUdpPort(self, port):
     """Adds a new port to the available port pool.
@@ -1153,6 +1104,16 @@ class ConfigWriter:
     return self._UnlockedGetNodeName(self._config_data.cluster.master_node)
 
   @locking.ssynchronized(_config_lock, shared=1)
+  def GetMasterNodeInfo(self):
+    """Get the master node information for this cluster.
+
+    @rtype: objects.Node
+    @return: Master node L{objects.Node} object
+
+    """
+    return self._UnlockedGetNodeInfo(self._config_data.cluster.master_node)
+
+  @locking.ssynchronized(_config_lock, shared=1)
   def GetMasterIP(self):
     """Get the IP of the master node for this cluster.
 
@@ -1576,7 +1537,6 @@ class ConfigWriter:
         disk.logical_id = (disk.logical_id[0],
                            utils.PathJoin(file_storage_dir, inst.name,
                                           "disk%s" % idx))
-        disk.physical_id = disk.logical_id
 
     # Force update of ssconf files
     self._config_data.cluster.serial_no += 1
index c761023..4ea849a 100644 (file)
 import re
 import socket
 
-from ganeti import _autoconf
+from ganeti import _constants
 from ganeti import _vcsversion
 from ganeti import compat
 from ganeti import pathutils
 
 
 # various versions
-RELEASE_VERSION = _autoconf.PACKAGE_VERSION
-OS_API_V10 = 10
-OS_API_V15 = 15
-OS_API_V20 = 20
-OS_API_VERSIONS = compat.UniqueFrozenset([
-  OS_API_V10,
-  OS_API_V15,
-  OS_API_V20,
-  ])
+RELEASE_VERSION = _constants.RELEASE_VERSION
+OS_API_V10 = _constants.OS_API_V10
+OS_API_V15 = _constants.OS_API_V15
+OS_API_V20 = _constants.OS_API_V20
+OS_API_VERSIONS = _constants.OS_API_VERSIONS
 VCS_VERSION = _vcsversion.VCS_VERSION
-EXPORT_VERSION = 0
-RAPI_VERSION = 2
-
-
-# Format for CONFIG_VERSION:
-#   01 03 0123 = 01030123
-#   ^^ ^^ ^^^^
-#   |  |  + Configuration version/revision
-#   |  + Minor version
-#   + Major version
-#
-# It is stored as an integer. Make sure not to write an octal number.
-
-# BuildVersion and SplitVersion must be in here because we can't import other
-# modules. The cfgupgrade tool must be able to read and write version numbers
-# and thus requires these functions. To avoid code duplication, they're kept in
-# here.
-
-def BuildVersion(major, minor, revision):
-  """Calculates int version number from major, minor and revision numbers.
-
-  Returns: int representing version number
-
-  """
-  assert isinstance(major, int)
-  assert isinstance(minor, int)
-  assert isinstance(revision, int)
-  return (1000000 * major +
-            10000 * minor +
-                1 * revision)
-
-
-def SplitVersion(version):
-  """Splits version number stored in an int.
-
-  Returns: tuple; (major, minor, revision)
-
-  """
-  assert isinstance(version, int)
-
-  (major, remainder) = divmod(version, 1000000)
-  (minor, revision) = divmod(remainder, 10000)
-
-  return (major, minor, revision)
-
-
-CONFIG_MAJOR = int(_autoconf.VERSION_MAJOR)
-CONFIG_MINOR = int(_autoconf.VERSION_MINOR)
-CONFIG_REVISION = 0
-CONFIG_VERSION = BuildVersion(CONFIG_MAJOR, CONFIG_MINOR, CONFIG_REVISION)
-
-#: RPC protocol version
-PROTOCOL_VERSION = BuildVersion(CONFIG_MAJOR, CONFIG_MINOR, 0)
-
-# user separation
-DAEMONS_GROUP = _autoconf.DAEMONS_GROUP
-ADMIN_GROUP = _autoconf.ADMIN_GROUP
-MASTERD_USER = _autoconf.MASTERD_USER
-MASTERD_GROUP = _autoconf.MASTERD_GROUP
-RAPI_USER = _autoconf.RAPI_USER
-RAPI_GROUP = _autoconf.RAPI_GROUP
-CONFD_USER = _autoconf.CONFD_USER
-CONFD_GROUP = _autoconf.CONFD_GROUP
-LUXID_USER = _autoconf.LUXID_USER
-LUXID_GROUP = _autoconf.LUXID_GROUP
-NODED_USER = _autoconf.NODED_USER
-NODED_GROUP = _autoconf.NODED_GROUP
-MOND_USER = _autoconf.MOND_USER
-MOND_GROUP = _autoconf.MOND_GROUP
-SSH_LOGIN_USER = _autoconf.SSH_LOGIN_USER
-SSH_CONSOLE_USER = _autoconf.SSH_CONSOLE_USER
-
-# 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"
-MAX_WIPE_CHUNK = 1024 # 1GB
-MIN_WIPE_CHUNK_PERCENT = 10
-
-RUN_DIRS_MODE = 0775
-SECURE_DIR_MODE = 0700
-SECURE_FILE_MODE = 0600
-ADOPTABLE_BLOCKDEV_ROOT = "/dev/disk/"
-ENABLE_CONFD = _autoconf.ENABLE_CONFD
-ENABLE_MOND = _autoconf.ENABLE_MOND
-ENABLE_SPLIT_QUERY = _autoconf.ENABLE_SPLIT_QUERY
-ENABLE_RESTRICTED_COMMANDS = _autoconf.ENABLE_RESTRICTED_COMMANDS
-
-# SSH constants
-SSH = "ssh"
-SCP = "scp"
-
-NODED = "ganeti-noded"
-CONFD = "ganeti-confd"
-LUXID = "ganeti-luxid"
-RAPI = "ganeti-rapi"
-MASTERD = "ganeti-masterd"
-MOND = "ganeti-mond"
-
-DAEMONS = compat.UniqueFrozenset([
-  NODED,
-  CONFD,
-  LUXID,
-  RAPI,
-  MASTERD,
-  MOND,
-  ])
-
-DAEMONS_PORTS = {
-  # daemon-name: ("proto", "default-port")
-  NODED: ("tcp", 1811),
-  CONFD: ("udp", 1814),
-  MOND: ("tcp", 1815),
-  RAPI: ("tcp", 5080),
-  SSH: ("tcp", 22),
-}
-
-DEFAULT_NODED_PORT = DAEMONS_PORTS[NODED][1]
-DEFAULT_CONFD_PORT = DAEMONS_PORTS[CONFD][1]
-DEFAULT_MOND_PORT = DAEMONS_PORTS[MOND][1]
-DEFAULT_RAPI_PORT = DAEMONS_PORTS[RAPI][1]
-
-FIRST_DRBD_PORT = 11000
-LAST_DRBD_PORT = 14999
-
-DAEMONS_LOGBASE = {
-  NODED: "node-daemon",
-  CONFD: "conf-daemon",
-  LUXID: "luxi-daemon",
-  RAPI: "rapi-daemon",
-  MASTERD: "master-daemon",
-  MOND: "monitoring-daemon",
-  }
+EXPORT_VERSION = _constants.EXPORT_VERSION
+RAPI_VERSION = _constants.RAPI_VERSION
+
+VERSION_MAJOR = _constants.VERSION_MAJOR
+VERSION_MINOR = _constants.VERSION_MINOR
+VERSION_REVISION = _constants.VERSION_REVISION
+
+DIR_VERSION = _constants.DIR_VERSION
+
+CONFIG_MAJOR = _constants.CONFIG_MAJOR
+CONFIG_MINOR = _constants.CONFIG_MINOR
+CONFIG_REVISION = _constants.CONFIG_REVISION
+CONFIG_VERSION = _constants.CONFIG_VERSION
+
+PROTOCOL_VERSION = _constants.PROTOCOL_VERSION
+
+DAEMONS_GROUP = _constants.DAEMONS_GROUP
+ADMIN_GROUP = _constants.ADMIN_GROUP
+MASTERD_USER = _constants.MASTERD_USER
+MASTERD_GROUP = _constants.MASTERD_GROUP
+RAPI_USER = _constants.RAPI_USER
+RAPI_GROUP = _constants.RAPI_GROUP
+CONFD_USER = _constants.CONFD_USER
+CONFD_GROUP = _constants.CONFD_GROUP
+LUXID_USER = _constants.LUXID_USER
+LUXID_GROUP = _constants.LUXID_GROUP
+NODED_USER = _constants.NODED_USER
+NODED_GROUP = _constants.NODED_GROUP
+MOND_USER = _constants.MOND_USER
+MOND_GROUP = _constants.MOND_GROUP
+SSH_LOGIN_USER = _constants.SSH_LOGIN_USER
+SSH_CONSOLE_USER = _constants.SSH_CONSOLE_USER
+
+CPU_PINNING_SEP = _constants.CPU_PINNING_SEP
+CPU_PINNING_ALL = _constants.CPU_PINNING_ALL
+CPU_PINNING_ALL_VAL = _constants.CPU_PINNING_ALL_VAL
+CPU_PINNING_OFF = _constants.CPU_PINNING_OFF
+
+CPU_PINNING_ALL_XEN = _constants.CPU_PINNING_ALL_XEN
+
+CPU_PINNING_ALL_KVM = _constants.CPU_PINNING_ALL_KVM
+
+DD_CMD = _constants.DD_CMD
+MAX_WIPE_CHUNK = _constants.MAX_WIPE_CHUNK
+MIN_WIPE_CHUNK_PERCENT = _constants.MIN_WIPE_CHUNK_PERCENT
+
+RUN_DIRS_MODE = _constants.RUN_DIRS_MODE
+SECURE_DIR_MODE = _constants.SECURE_DIR_MODE
+SECURE_FILE_MODE = _constants.SECURE_FILE_MODE
+ADOPTABLE_BLOCKDEV_ROOT = _constants.ADOPTABLE_BLOCKDEV_ROOT
+ENABLE_CONFD = _constants.ENABLE_CONFD
+ENABLE_MOND = _constants.ENABLE_MOND
+ENABLE_SPLIT_QUERY = _constants.ENABLE_SPLIT_QUERY
+ENABLE_RESTRICTED_COMMANDS = _constants.ENABLE_RESTRICTED_COMMANDS
+
+SSH = _constants.SSH
+SCP = _constants.SCP
+
+NODED = _constants.NODED
+CONFD = _constants.CONFD
+LUXID = _constants.LUXID
+RAPI = _constants.RAPI
+MASTERD = _constants.MASTERD
+MOND = _constants.MOND
+
+DAEMONS = _constants.DAEMONS
+
+DAEMONS_PORTS = _constants.DAEMONS_PORTS
+
+DEFAULT_NODED_PORT = _constants.DEFAULT_NODED_PORT
+DEFAULT_CONFD_PORT = _constants.DEFAULT_CONFD_PORT
+DEFAULT_MOND_PORT = _constants.DEFAULT_MOND_PORT
+DEFAULT_RAPI_PORT = _constants.DEFAULT_RAPI_PORT
+
+FIRST_DRBD_PORT = _constants.FIRST_DRBD_PORT
+LAST_DRBD_PORT = _constants.LAST_DRBD_PORT
+
+DAEMONS_LOGBASE = _constants.DAEMONS_LOGBASE
 
 DAEMONS_LOGFILES = \
     dict((daemon, pathutils.GetLogFilename(DAEMONS_LOGBASE[daemon]))
          for daemon in DAEMONS_LOGBASE)
 
-# Some daemons might require more than one logfile.
-# Specifically, right now only the Haskell http library "snap", used by the
-# monitoring daemon, requires multiple log files.
-
-# These are the only valid reasons for having an extra logfile
-EXTRA_LOGREASON_ACCESS = "access"
-EXTRA_LOGREASON_ERROR = "error"
-
-VALID_EXTRA_LOGREASONS = compat.UniqueFrozenset([
-  EXTRA_LOGREASON_ACCESS,
-  EXTRA_LOGREASON_ERROR,
-  ])
-
-# These are the extra logfiles, grouped by daemon
-DAEMONS_EXTRA_LOGBASE = {
-  MOND: {
-    EXTRA_LOGREASON_ACCESS: "monitoring-daemon-access",
-    EXTRA_LOGREASON_ERROR: "monitoring-daemon-error",
-    }
-  }
+DAEMONS_EXTRA_LOGBASE = _constants.DAEMONS_EXTRA_LOGBASE
 
 DAEMONS_EXTRA_LOGFILES = \
   dict((daemon, dict((extra,
@@ -227,37 +128,30 @@ DAEMONS_EXTRA_LOGFILES = \
        for extra in DAEMONS_EXTRA_LOGBASE[daemon]))
          for daemon in DAEMONS_EXTRA_LOGBASE)
 
-DEV_CONSOLE = "/dev/console"
+DEV_CONSOLE = _constants.DEV_CONSOLE
 
-PROC_MOUNTS = "/proc/mounts"
+PROC_MOUNTS = _constants.PROC_MOUNTS
 
-# Local UniX Interface related constants
-LUXI_EOM = chr(3)
-LUXI_VERSION = CONFIG_VERSION
-#: Environment variable for the luxi override socket
-LUXI_OVERRIDE = "FORCE_LUXI_SOCKET"
-LUXI_OVERRIDE_MASTER = "master"
-LUXI_OVERRIDE_QUERY = "query"
+LUXI_EOM = _constants.LUXI_EOM
+LUXI_VERSION = _constants.LUXI_VERSION
+LUXI_OVERRIDE = _constants.LUXI_OVERRIDE
+LUXI_OVERRIDE_MASTER = _constants.LUXI_OVERRIDE_MASTER
+LUXI_OVERRIDE_QUERY = _constants.LUXI_OVERRIDE_QUERY
 
-# one of "no", "yes", "only"
-SYSLOG_USAGE = _autoconf.SYSLOG_USAGE
-SYSLOG_NO = "no"
-SYSLOG_YES = "yes"
-SYSLOG_ONLY = "only"
-SYSLOG_SOCKET = "/dev/log"
+SYSLOG_USAGE = _constants.SYSLOG_USAGE
+SYSLOG_NO = _constants.SYSLOG_NO
+SYSLOG_YES = _constants.SYSLOG_YES
+SYSLOG_ONLY = _constants.SYSLOG_ONLY
+SYSLOG_SOCKET = _constants.SYSLOG_SOCKET
 
-EXPORT_CONF_FILE = "config.ini"
+EXPORT_CONF_FILE = _constants.EXPORT_CONF_FILE
 
-XEN_BOOTLOADER = _autoconf.XEN_BOOTLOADER
-XEN_KERNEL = _autoconf.XEN_KERNEL
-XEN_INITRD = _autoconf.XEN_INITRD
-XEN_CMD_XM = "xm"
-XEN_CMD_XL = "xl"
-
-KNOWN_XEN_COMMANDS = compat.UniqueFrozenset([
-  XEN_CMD_XM,
-  XEN_CMD_XL,
-  ])
+XEN_BOOTLOADER = _constants.XEN_BOOTLOADER
+XEN_KERNEL = _constants.XEN_KERNEL
+XEN_INITRD = _constants.XEN_INITRD
+XEN_CMD_XM = _constants.XEN_CMD_XM
+XEN_CMD_XL = _constants.XEN_CMD_XL
+KNOWN_XEN_COMMANDS = _constants.KNOWN_XEN_COMMANDS
 
 # When the Xen toolstack used is "xl", live migration requires the source host
 # to connect to the target host via ssh (xl runs this command). We need to pass
@@ -270,681 +164,392 @@ XL_SSH_CMD = ("ssh -l %s -oGlobalKnownHostsFile=%s"
               " -oHostKeyAlias=%%s") % (SSH_LOGIN_USER,
                                         pathutils.SSH_KNOWN_HOSTS_FILE)
 
-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
-SOCAT_ESCAPE_CODE = "0x1d"
-
-#: Console as SSH command
-CONS_SSH = "ssh"
-
-#: Console as VNC server
-CONS_VNC = "vnc"
-
-#: Console as SPICE server
-CONS_SPICE = "spice"
+KVM_PATH = _constants.KVM_PATH
+KVM_KERNEL = _constants.KVM_KERNEL
+SOCAT_PATH = _constants.SOCAT_PATH
+SOCAT_USE_ESCAPE = _constants.SOCAT_USE_ESCAPE
+SOCAT_USE_COMPRESS = _constants.SOCAT_USE_COMPRESS
+SOCAT_ESCAPE_CODE = _constants.SOCAT_ESCAPE_CODE
 
-#: Display a message for console access
-CONS_MESSAGE = "msg"
 
-#: All console types
-CONS_ALL = compat.UniqueFrozenset([
-  CONS_SSH,
-  CONS_VNC,
-  CONS_SPICE,
-  CONS_MESSAGE,
-  ])
+CONS_SSH = _constants.CONS_SSH
+CONS_VNC = _constants.CONS_VNC
+CONS_SPICE = _constants.CONS_SPICE
+CONS_MESSAGE = _constants.CONS_MESSAGE
+CONS_ALL = _constants.CONS_ALL
 
-# 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
-# 2010 on.
-RSA_KEY_BITS = 2048
+RSA_KEY_BITS = _constants.RSA_KEY_BITS
+OPENSSL_CIPHERS = _constants.OPENSSL_CIPHERS
 
-# Ciphers allowed for SSL connections. For the format, see ciphers(1). A better
-# way to disable ciphers would be to use the exclamation mark (!), but socat
-# versions below 1.5 can't parse exclamation marks in options properly. When
-# modifying the ciphers, ensure not to accidentially add something after it's
-# been removed. Use the "openssl" utility to check the allowed ciphers, e.g.
-# "openssl ciphers -v HIGH:-DES".
-OPENSSL_CIPHERS = "HIGH:-DES:-3DES:-EXPORT:-ADH"
-
-# Digest used to sign certificates ("openssl x509" uses SHA1 by default)
-X509_CERT_SIGN_DIGEST = "SHA1"
-
-# Default validity of certificates in days
-X509_CERT_DEFAULT_VALIDITY = 365 * 5
-
-# commonName (CN) used in certificates
-X509_CERT_CN = "ganeti.example.com"
-
-X509_CERT_SIGNATURE_HEADER = "X-Ganeti-Signature"
+X509_CERT_SIGN_DIGEST = _constants.X509_CERT_SIGN_DIGEST
+X509_CERT_DEFAULT_VALIDITY = _constants.X509_CERT_DEFAULT_VALIDITY
+X509_CERT_CN = _constants.X509_CERT_CN
+X509_CERT_SIGNATURE_HEADER = _constants.X509_CERT_SIGNATURE_HEADER
 
 # Import/export daemon mode
-IEM_IMPORT = "import"
-IEM_EXPORT = "export"
+IEM_IMPORT = _constants.IEM_IMPORT
+IEM_EXPORT = _constants.IEM_EXPORT
 
 # Import/export transport compression
-IEC_NONE = "none"
-IEC_GZIP = "gzip"
-IEC_ALL = compat.UniqueFrozenset([
-  IEC_NONE,
-  IEC_GZIP,
-  ])
+IEC_NONE = _constants.IEC_NONE
+IEC_GZIP = _constants.IEC_GZIP
+IEC_ALL = _constants.IEC_ALL
 
-IE_CUSTOM_SIZE = "fd"
+IE_CUSTOM_SIZE = _constants.IE_CUSTOM_SIZE
 
 IE_MAGIC_RE = re.compile(r"^[-_.a-zA-Z0-9]{5,100}$")
 
-# Import/export I/O
-# Direct file I/O, equivalent to a shell's I/O redirection using '<' or '>'
-IEIO_FILE = "file"
-# Raw block device I/O using "dd"
-IEIO_RAW_DISK = "raw"
-# OS definition import/export script
-IEIO_SCRIPT = "script"
-
-VALUE_DEFAULT = "default"
-VALUE_AUTO = "auto"
-VALUE_GENERATE = "generate"
-VALUE_NONE = "none"
-VALUE_TRUE = "true"
-VALUE_FALSE = "false"
+IEIO_FILE = _constants.IEIO_FILE
+IEIO_RAW_DISK = _constants.IEIO_RAW_DISK
+IEIO_SCRIPT = _constants.IEIO_SCRIPT
+
+VALUE_DEFAULT = _constants.VALUE_DEFAULT
+VALUE_AUTO = _constants.VALUE_AUTO
+VALUE_GENERATE = _constants.VALUE_GENERATE
+VALUE_NONE = _constants.VALUE_NONE
+VALUE_TRUE = _constants.VALUE_TRUE
+VALUE_FALSE = _constants.VALUE_FALSE
+VALUE_HS_NOTHING = _constants.VALUE_HS_NOTHING
 
 # External script validation mask
 EXT_PLUGIN_MASK = re.compile("^[a-zA-Z0-9_-]+$")
 
-# hooks-related constants
-HOOKS_PHASE_PRE = "pre"
-HOOKS_PHASE_POST = "post"
-HOOKS_NAME_CFGUPDATE = "config-update"
-HOOKS_NAME_WATCHER = "watcher"
-HOOKS_VERSION = 2
-HOOKS_PATH = "/sbin:/bin:/usr/sbin:/usr/bin"
-
-# hooks subject type (what object type does the LU deal with)
-HTYPE_CLUSTER = "CLUSTER"
-HTYPE_NODE = "NODE"
-HTYPE_GROUP = "GROUP"
-HTYPE_INSTANCE = "INSTANCE"
-HTYPE_NETWORK = "NETWORK"
-
-HKR_SKIP = 0
-HKR_FAIL = 1
-HKR_SUCCESS = 2
-
-# Storage types
-ST_BLOCK = "blockdev"
-ST_DISKLESS = "diskless"
-ST_EXT = "ext"
-ST_FILE = "file"
-ST_LVM_PV = "lvm-pv"
-ST_LVM_VG = "lvm-vg"
-ST_RADOS = "rados"
-
-STORAGE_TYPES = compat.UniqueFrozenset([
-  ST_BLOCK,
-  ST_DISKLESS,
-  ST_EXT,
-  ST_FILE,
-  ST_LVM_PV,
-  ST_LVM_VG,
-  ST_RADOS,
-  ])
-
-# the set of storage types for which storage reporting is available
-# FIXME: Remove this, once storage reporting is available for all types.
-STS_REPORT = compat.UniqueFrozenset([ST_FILE, ST_LVM_PV, ST_LVM_VG])
+HOOKS_PHASE_PRE = _constants.HOOKS_PHASE_PRE
+HOOKS_PHASE_POST = _constants.HOOKS_PHASE_POST
+HOOKS_NAME_CFGUPDATE = _constants.HOOKS_NAME_CFGUPDATE
+HOOKS_NAME_WATCHER = _constants.HOOKS_NAME_WATCHER
+HOOKS_VERSION = _constants.HOOKS_VERSION
+HOOKS_PATH = _constants.HOOKS_PATH
+
+HTYPE_CLUSTER = _constants.HTYPE_CLUSTER
+HTYPE_NODE = _constants.HTYPE_NODE
+HTYPE_GROUP = _constants.HTYPE_GROUP
+HTYPE_INSTANCE = _constants.HTYPE_INSTANCE
+HTYPE_NETWORK = _constants.HTYPE_NETWORK
+
+HKR_SKIP = _constants.HKR_SKIP
+HKR_FAIL = _constants.HKR_FAIL
+HKR_SUCCESS = _constants.HKR_SUCCESS
+
+ST_BLOCK = _constants.ST_BLOCK
+ST_DISKLESS = _constants.ST_DISKLESS
+ST_EXT = _constants.ST_EXT
+ST_FILE = _constants.ST_FILE
+ST_LVM_PV = _constants.ST_LVM_PV
+ST_LVM_VG = _constants.ST_LVM_VG
+ST_RADOS = _constants.ST_RADOS
+STORAGE_TYPES = _constants.STORAGE_TYPES
+STS_REPORT = _constants.STS_REPORT
 
 # Storage fields
 # first two are valid in LU context only, not passed to backend
-SF_NODE = "node"
-SF_TYPE = "type"
+SF_NODE = _constants.SF_NODE
+SF_TYPE = _constants.SF_TYPE
 # and the rest are valid in backend
-SF_NAME = "name"
-SF_SIZE = "size"
-SF_FREE = "free"
-SF_USED = "used"
-SF_ALLOCATABLE = "allocatable"
-
-# Storage operations
-SO_FIX_CONSISTENCY = "fix-consistency"
-
-# Available fields per storage type
-VALID_STORAGE_FIELDS = compat.UniqueFrozenset([
-  SF_NAME,
-  SF_TYPE,
-  SF_SIZE,
-  SF_USED,
-  SF_FREE,
-  SF_ALLOCATABLE,
-  ])
-
-MODIFIABLE_STORAGE_FIELDS = {
-  ST_LVM_PV: frozenset([SF_ALLOCATABLE]),
-  }
-
-VALID_STORAGE_OPERATIONS = {
-  ST_LVM_VG: frozenset([SO_FIX_CONSISTENCY]),
-  }
-
-# Local disk status
-# Note: Code depends on LDS_OKAY < LDS_UNKNOWN < LDS_FAULTY
-(LDS_OKAY,
- LDS_UNKNOWN,
- LDS_FAULTY) = range(1, 4)
-
-LDS_NAMES = {
-  LDS_OKAY: "ok",
-  LDS_UNKNOWN: "unknown",
-  LDS_FAULTY: "faulty",
-}
-
-# disk template types
-DT_BLOCK = "blockdev"
-DT_DISKLESS = "diskless"
-DT_DRBD8 = "drbd"
-DT_EXT = "ext"
-DT_FILE = "file"
-DT_PLAIN = "plain"
-DT_RBD = "rbd"
-DT_SHARED_FILE = "sharedfile"
-
-# This is used to order determine the default disk template when the list
-# of enabled disk templates is inferred from the current state of the cluster.
-# This only happens on an upgrade from a version of Ganeti that did not
-# support the 'enabled_disk_templates' so far.
-DISK_TEMPLATE_PREFERENCE = [
-  DT_BLOCK,
-  DT_DISKLESS,
-  DT_DRBD8,
-  DT_EXT,
-  DT_FILE,
-  DT_PLAIN,
-  DT_RBD,
-  DT_SHARED_FILE,
-  ]
-
-DISK_TEMPLATES = compat.UniqueFrozenset([
-  DT_DISKLESS,
-  DT_PLAIN,
-  DT_DRBD8,
-  DT_FILE,
-  DT_SHARED_FILE,
-  DT_BLOCK,
-  DT_RBD,
-  DT_EXT
-  ])
-
-# disk templates that are enabled by default
-DEFAULT_ENABLED_DISK_TEMPLATES = [
-  DT_DRBD8,
-  DT_PLAIN,
-  ]
-
-# mapping of disk templates to storage types
-MAP_DISK_TEMPLATE_STORAGE_TYPE = {
-  DT_BLOCK: ST_BLOCK,
-  DT_DISKLESS: ST_DISKLESS,
-  DT_DRBD8: ST_LVM_VG,
-  DT_EXT: ST_EXT,
-  DT_FILE: ST_FILE,
-  DT_PLAIN: ST_LVM_VG,
-  DT_RBD: ST_RADOS,
-  DT_SHARED_FILE: ST_FILE,
-  }
-
-# the set of network-mirrored disk templates
-DTS_INT_MIRROR = compat.UniqueFrozenset([DT_DRBD8])
-
-# the set of externally-mirrored disk templates (e.g. SAN, NAS)
-DTS_EXT_MIRROR = compat.UniqueFrozenset([
-  DT_DISKLESS, # 'trivially' externally mirrored
-  DT_SHARED_FILE,
-  DT_BLOCK,
-  DT_RBD,
-  DT_EXT,
-  ])
-
-# the set of non-lvm-based disk templates
-DTS_NOT_LVM = compat.UniqueFrozenset([
-  DT_DISKLESS,
-  DT_FILE,
-  DT_SHARED_FILE,
-  DT_BLOCK,
-  DT_RBD,
-  DT_EXT,
-  ])
-
-# the set of disk templates which can be grown
-DTS_GROWABLE = compat.UniqueFrozenset([
-  DT_PLAIN,
-  DT_DRBD8,
-  DT_FILE,
-  DT_SHARED_FILE,
-  DT_RBD,
-  DT_EXT,
-  ])
-
-# the set of disk templates that allow adoption
-DTS_MAY_ADOPT = compat.UniqueFrozenset([
-  DT_PLAIN,
-  DT_BLOCK,
-  ])
-
-# the set of disk templates that *must* use adoption
-DTS_MUST_ADOPT = compat.UniqueFrozenset([DT_BLOCK])
-
-# the set of disk templates that allow migrations
-DTS_MIRRORED = frozenset.union(DTS_INT_MIRROR, DTS_EXT_MIRROR)
-
-# the set of file based disk templates
-DTS_FILEBASED = compat.UniqueFrozenset([
-  DT_FILE,
-  DT_SHARED_FILE,
-  ])
-
-# the set of disk templates that can be moved by copying
-# Note: a requirement is that they're not accessed externally or shared between
-# nodes; in particular, sharedfile is not suitable.
-DTS_COPYABLE = compat.UniqueFrozenset([
-  DT_FILE,
-  DT_PLAIN,
-  ])
-
-# the set of disk templates that are supported by exclusive_storage
-DTS_EXCL_STORAGE = compat.UniqueFrozenset([DT_PLAIN])
-
-# templates for which we don't perform checks on free space
-DTS_NO_FREE_SPACE_CHECK = compat.UniqueFrozenset([
-  DT_FILE,
-  DT_SHARED_FILE,
-  DT_RBD,
-  DT_EXT,
-  ])
-
-DTS_BLOCK = compat.UniqueFrozenset([
-  DT_PLAIN,
-  DT_DRBD8,
-  DT_BLOCK,
-  DT_RBD,
-  DT_EXT,
-  ])
-
-# the set of drbd-like disk types
-DTS_DRBD = compat.UniqueFrozenset([DT_DRBD8])
-
-# drbd constants
-DRBD_HMAC_ALG = "md5"
-DRBD_DEFAULT_NET_PROTOCOL = "C"
-DRBD_MIGRATION_NET_PROTOCOL = "C"
-DRBD_STATUS_FILE = "/proc/drbd"
-
-#: Size of DRBD meta block device
-DRBD_META_SIZE = 128
-
-# 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 = compat.UniqueFrozenset([
-  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_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"
-FD_BLKTAP = "blktap"
-
-# disk access mode
-DISK_RDONLY = "ro"
-DISK_RDWR = "rw"
-DISK_ACCESS_SET = compat.UniqueFrozenset([DISK_RDONLY, DISK_RDWR])
-
-# disk replacement mode
-REPLACE_DISK_PRI = "replace_on_primary"    # replace disks on primary
-REPLACE_DISK_SEC = "replace_on_secondary"  # replace disks on secondary
-REPLACE_DISK_CHG = "replace_new_secondary" # change secondary node
-REPLACE_DISK_AUTO = "replace_auto"
-REPLACE_MODES = compat.UniqueFrozenset([
-  REPLACE_DISK_PRI,
-  REPLACE_DISK_SEC,
-  REPLACE_DISK_CHG,
-  REPLACE_DISK_AUTO,
-  ])
-
-# Instance export mode
-EXPORT_MODE_LOCAL = "local"
-EXPORT_MODE_REMOTE = "remote"
-EXPORT_MODES = compat.UniqueFrozenset([
-  EXPORT_MODE_LOCAL,
-  EXPORT_MODE_REMOTE,
-  ])
-
-# instance creation modes
-INSTANCE_CREATE = "create"
-INSTANCE_IMPORT = "import"
-INSTANCE_REMOTE_IMPORT = "remote-import"
-INSTANCE_CREATE_MODES = compat.UniqueFrozenset([
-  INSTANCE_CREATE,
-  INSTANCE_IMPORT,
-  INSTANCE_REMOTE_IMPORT,
-  ])
-
-# Remote import/export handshake message and version
-RIE_VERSION = 0
-RIE_HANDSHAKE = "Hi, I'm Ganeti"
-
-# Remote import/export certificate validity in seconds
-RIE_CERT_VALIDITY = 24 * 60 * 60
-
-# Overall timeout for establishing connection
-RIE_CONNECT_TIMEOUT = 180
-
-# Export only: how long to wait per connection attempt (seconds)
-RIE_CONNECT_ATTEMPT_TIMEOUT = 20
-
-# Export only: number of attempts to connect
-RIE_CONNECT_RETRIES = 10
-
-#: Give child process up to 5 seconds to exit after sending a signal
-CHILD_LINGER_TIMEOUT = 5.0
-
-FILE_DRIVER = compat.UniqueFrozenset([FD_LOOP, FD_BLKTAP])
-
-# import/export config options
-INISECT_EXP = "export"
-INISECT_INS = "instance"
-INISECT_HYP = "hypervisor"
-INISECT_BEP = "backend"
-INISECT_OSP = "os"
-
-# dynamic device modification
-DDM_ADD = "add"
-DDM_MODIFY = "modify"
-DDM_REMOVE = "remove"
-DDMS_VALUES = compat.UniqueFrozenset([DDM_ADD, DDM_REMOVE])
-DDMS_VALUES_WITH_MODIFY = (DDMS_VALUES | frozenset([
-  DDM_MODIFY,
-  ]))
-# TODO: DDM_SWAP, DDM_MOVE?
+SF_NAME = _constants.SF_NAME
+SF_SIZE = _constants.SF_SIZE
+SF_FREE = _constants.SF_FREE
+SF_USED = _constants.SF_USED
+SF_ALLOCATABLE = _constants.SF_ALLOCATABLE
+VALID_STORAGE_FIELDS = _constants.VALID_STORAGE_FIELDS
+MODIFIABLE_STORAGE_FIELDS = _constants.MODIFIABLE_STORAGE_FIELDS
+
+SO_FIX_CONSISTENCY = _constants.SO_FIX_CONSISTENCY
+VALID_STORAGE_OPERATIONS = _constants.VALID_STORAGE_OPERATIONS
+
+VF_DEV = _constants.VF_DEV
+VF_INSTANCE = _constants.VF_INSTANCE
+VF_NAME = _constants.VF_NAME
+VF_NODE = _constants.VF_NODE
+VF_PHYS = _constants.VF_PHYS
+VF_SIZE = _constants.VF_SIZE
+VF_VG = _constants.VF_VG
+
+LDS_OKAY = _constants.LDS_OKAY
+LDS_UNKNOWN = _constants.LDS_UNKNOWN
+LDS_FAULTY = _constants.LDS_FAULTY
+LDS_NAMES = _constants.LDS_NAMES
+
+DT_BLOCK = _constants.DT_BLOCK
+DT_DISKLESS = _constants.DT_DISKLESS
+DT_DRBD8 = _constants.DT_DRBD8
+DT_EXT = _constants.DT_EXT
+DT_FILE = _constants.DT_FILE
+DT_PLAIN = _constants.DT_PLAIN
+DT_RBD = _constants.DT_RBD
+DT_SHARED_FILE = _constants.DT_SHARED_FILE
+DISK_TEMPLATE_PREFERENCE = _constants.DISK_TEMPLATE_PREFERENCE
+DISK_TEMPLATES = _constants.DISK_TEMPLATES
+DEFAULT_ENABLED_DISK_TEMPLATES = _constants.DEFAULT_ENABLED_DISK_TEMPLATES
+
+MAP_DISK_TEMPLATE_STORAGE_TYPE = _constants.MAP_DISK_TEMPLATE_STORAGE_TYPE
+
+DTS_INT_MIRROR = _constants.DTS_INT_MIRROR
+DTS_EXT_MIRROR = _constants.DTS_EXT_MIRROR
+DTS_NOT_LVM = _constants.DTS_NOT_LVM
+DTS_GROWABLE = _constants.DTS_GROWABLE
+DTS_MAY_ADOPT = _constants.DTS_MAY_ADOPT
+DTS_MUST_ADOPT = _constants.DTS_MUST_ADOPT
+DTS_MIRRORED = _constants.DTS_MIRRORED
+DTS_FILEBASED = _constants.DTS_FILEBASED
+DTS_COPYABLE = _constants.DTS_COPYABLE
+DTS_EXCL_STORAGE = _constants.DTS_EXCL_STORAGE
+DTS_NO_FREE_SPACE_CHECK = _constants.DTS_NO_FREE_SPACE_CHECK
+DTS_BLOCK = _constants.DTS_BLOCK
+
+DTS_DRBD = _constants.DTS_DRBD
+
+DTS_LVM = _constants.DTS_LVM
+
+DRBD_HMAC_ALG = _constants.DRBD_HMAC_ALG
+DRBD_DEFAULT_NET_PROTOCOL = _constants.DRBD_DEFAULT_NET_PROTOCOL
+DRBD_MIGRATION_NET_PROTOCOL = _constants.DRBD_MIGRATION_NET_PROTOCOL
+DRBD_STATUS_FILE = _constants.DRBD_STATUS_FILE
+DRBD_META_SIZE = _constants.DRBD_META_SIZE
+
+DRBD_B_NONE = _constants.DRBD_B_NONE
+DRBD_B_DISK_BARRIERS = _constants.DRBD_B_DISK_BARRIERS
+DRBD_B_DISK_DRAIN = _constants.DRBD_B_DISK_DRAIN
+DRBD_B_DISK_FLUSH = _constants.DRBD_B_DISK_FLUSH
+
+DRBD_VALID_BARRIER_OPT = _constants.DRBD_VALID_BARRIER_OPT
+
+RBD_CMD = _constants.RBD_CMD
+
+FD_BLKTAP = _constants.FD_BLKTAP
+FD_LOOP = _constants.FD_LOOP
+FILE_DRIVER = _constants.FILE_DRIVER
+
+DISK_RDONLY = _constants.DISK_RDONLY
+DISK_RDWR = _constants.DISK_RDWR
+DISK_ACCESS_SET = _constants.DISK_ACCESS_SET
+DISK_USERSPACE = _constants.DISK_USERSPACE
+DISK_KERNELSPACE = _constants.DISK_KERNELSPACE
+DISK_VALID_ACCESS_MODES = _constants.DISK_VALID_ACCESS_MODES
+
+REPLACE_DISK_PRI = _constants.REPLACE_DISK_PRI
+REPLACE_DISK_SEC = _constants.REPLACE_DISK_SEC
+REPLACE_DISK_CHG = _constants.REPLACE_DISK_CHG
+REPLACE_DISK_AUTO = _constants.REPLACE_DISK_AUTO
+REPLACE_MODES = _constants.REPLACE_MODES
+
+EXPORT_MODE_LOCAL = _constants.EXPORT_MODE_LOCAL
+EXPORT_MODE_REMOTE = _constants.EXPORT_MODE_REMOTE
+EXPORT_MODES = _constants.EXPORT_MODES
+
+INSTANCE_CREATE = _constants.INSTANCE_CREATE
+INSTANCE_IMPORT = _constants.INSTANCE_IMPORT
+INSTANCE_REMOTE_IMPORT = _constants.INSTANCE_REMOTE_IMPORT
+INSTANCE_CREATE_MODES = _constants.INSTANCE_CREATE_MODES
+
+RIE_VERSION = _constants.RIE_VERSION
+RIE_HANDSHAKE = _constants.RIE_HANDSHAKE
+RIE_CERT_VALIDITY = _constants.RIE_CERT_VALIDITY
+RIE_CONNECT_TIMEOUT = _constants.RIE_CONNECT_TIMEOUT
+RIE_CONNECT_ATTEMPT_TIMEOUT = _constants.RIE_CONNECT_ATTEMPT_TIMEOUT
+RIE_CONNECT_RETRIES = _constants.RIE_CONNECT_RETRIES
+CHILD_LINGER_TIMEOUT = _constants.CHILD_LINGER_TIMEOUT
+
+INISECT_EXP = _constants.INISECT_EXP
+INISECT_INS = _constants.INISECT_INS
+INISECT_HYP = _constants.INISECT_HYP
+INISECT_BEP = _constants.INISECT_BEP
+INISECT_OSP = _constants.INISECT_OSP
+
+DDM_ADD = _constants.DDM_ADD
+DDM_MODIFY = _constants.DDM_MODIFY
+DDM_REMOVE = _constants.DDM_REMOVE
+DDMS_VALUES = _constants.DDMS_VALUES
+DDMS_VALUES_WITH_MODIFY = _constants.DDMS_VALUES_WITH_MODIFY
 
 # common exit codes
-EXIT_SUCCESS = 0
-EXIT_FAILURE = 1
-EXIT_NOTCLUSTER = 5
-EXIT_NOTMASTER = 11
-EXIT_NODESETUP_ERROR = 12
-EXIT_CONFIRMATION = 13 # need user confirmation
-
-#: Exit code for query operations with unknown fields
-EXIT_UNKNOWN_FIELD = 14
-
-# tags
-TAG_CLUSTER = "cluster"
-TAG_NODEGROUP = "nodegroup"
-TAG_NODE = "node"
-TAG_INSTANCE = "instance"
-TAG_NETWORK = "network"
-VALID_TAG_TYPES = compat.UniqueFrozenset([
-  TAG_CLUSTER,
-  TAG_NODEGROUP,
-  TAG_NODE,
-  TAG_INSTANCE,
-  TAG_NETWORK,
-  ])
-MAX_TAG_LEN = 128
-MAX_TAGS_PER_OBJ = 4096
+EXIT_SUCCESS = _constants.EXIT_SUCCESS
+EXIT_FAILURE = _constants.EXIT_FAILURE
+EXIT_NOTCLUSTER = _constants.EXIT_NOTCLUSTER
+EXIT_NOTMASTER = _constants.EXIT_NOTMASTER
+EXIT_NODESETUP_ERROR = _constants.EXIT_NODESETUP_ERROR
+EXIT_CONFIRMATION = _constants.EXIT_CONFIRMATION
+EXIT_UNKNOWN_FIELD = _constants.EXIT_UNKNOWN_FIELD
+
+TAG_CLUSTER = _constants.TAG_CLUSTER
+TAG_NODEGROUP = _constants.TAG_NODEGROUP
+TAG_NODE = _constants.TAG_NODE
+TAG_INSTANCE = _constants.TAG_INSTANCE
+TAG_NETWORK = _constants.TAG_NETWORK
+VALID_TAG_TYPES = _constants.VALID_TAG_TYPES
+
+MAX_TAG_LEN = _constants.MAX_TAG_LEN
+MAX_TAGS_PER_OBJ = _constants.MAX_TAGS_PER_OBJ
 
 # others
-DEFAULT_BRIDGE = "xen-br0"
-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"
-IP6_ADDRESS_ANY = "::"
-IP4_VERSION = 4
-IP6_VERSION = 6
-VALID_IP_VERSIONS = compat.UniqueFrozenset([IP4_VERSION, IP6_VERSION])
+DEFAULT_BRIDGE = _constants.DEFAULT_BRIDGE
+DEFAULT_OVS = _constants.DEFAULT_OVS
+CLASSIC_DRBD_SYNC_SPEED = _constants.CLASSIC_DRBD_SYNC_SPEED
+IP4_ADDRESS_LOCALHOST = _constants.IP4_ADDRESS_LOCALHOST
+IP4_ADDRESS_ANY = _constants.IP4_ADDRESS_ANY
+IP6_ADDRESS_LOCALHOST = _constants.IP6_ADDRESS_LOCALHOST
+IP6_ADDRESS_ANY = _constants.IP6_ADDRESS_ANY
+IP4_VERSION = _constants.IP4_VERSION
+IP6_VERSION = _constants.IP6_VERSION
+VALID_IP_VERSIONS = _constants.VALID_IP_VERSIONS
 # for export to htools
 IP4_FAMILY = socket.AF_INET
 IP6_FAMILY = socket.AF_INET6
 
-TCP_PING_TIMEOUT = 10
-DEFAULT_VG = "xenvg"
-DEFAULT_DRBD_HELPER = "/bin/true"
-MIN_VG_SIZE = 20480
-DEFAULT_MAC_PREFIX = "aa:00:00"
-# default maximum instance wait time, in seconds.
-DEFAULT_SHUTDOWN_TIMEOUT = 120
-NODE_MAX_CLOCK_SKEW = 150
-# Time for an intra-cluster disk transfer to wait for a connection
-DISK_TRANSFER_CONNECT_TIMEOUT = 60
-# Disk index separator
-DISK_SEPARATOR = _autoconf.DISK_SEPARATOR
-IP_COMMAND_PATH = _autoconf.IP_PATH
-
-#: Key for job IDs in opcode result
-JOB_IDS_KEY = "jobs"
-
-# runparts results
-(RUNPARTS_SKIP,
- RUNPARTS_RUN,
- RUNPARTS_ERR) = range(3)
-
-RUNPARTS_STATUS = compat.UniqueFrozenset([
-  RUNPARTS_SKIP,
-  RUNPARTS_RUN,
-  RUNPARTS_ERR,
-  ])
-
-# RPC constants
-(RPC_ENCODING_NONE,
- RPC_ENCODING_ZLIB_BASE64) = range(2)
-
-# Various time constants for the timeout table
-RPC_TMO_URGENT = 60 # one minute
-RPC_TMO_FAST = 5 * 60 # five minutes
-RPC_TMO_NORMAL = 15 * 60 # 15 minutes
-RPC_TMO_SLOW = 3600 # one hour
-RPC_TMO_4HRS = 4 * 3600
-RPC_TMO_1DAY = 86400
-
-# Timeout for connecting to nodes (seconds)
-RPC_CONNECT_TIMEOUT = 5
-
-# os related constants
-OS_SCRIPT_CREATE = "create"
-OS_SCRIPT_IMPORT = "import"
-OS_SCRIPT_EXPORT = "export"
-OS_SCRIPT_RENAME = "rename"
-OS_SCRIPT_VERIFY = "verify"
-OS_SCRIPTS = compat.UniqueFrozenset([
-  OS_SCRIPT_CREATE,
-  OS_SCRIPT_IMPORT,
-  OS_SCRIPT_EXPORT,
-  OS_SCRIPT_RENAME,
-  OS_SCRIPT_VERIFY,
-  ])
-
-OS_API_FILE = "ganeti_api_version"
-OS_VARIANTS_FILE = "variants.list"
-OS_PARAMETERS_FILE = "parameters.list"
-
-OS_VALIDATE_PARAMETERS = "parameters"
-OS_VALIDATE_CALLS = compat.UniqueFrozenset([OS_VALIDATE_PARAMETERS])
-
-# External Storage (ES) related constants
-ES_ACTION_CREATE = "create"
-ES_ACTION_REMOVE = "remove"
-ES_ACTION_GROW = "grow"
-ES_ACTION_ATTACH = "attach"
-ES_ACTION_DETACH = "detach"
-ES_ACTION_SETINFO = "setinfo"
-ES_ACTION_VERIFY = "verify"
-
-ES_SCRIPT_CREATE = ES_ACTION_CREATE
-ES_SCRIPT_REMOVE = ES_ACTION_REMOVE
-ES_SCRIPT_GROW = ES_ACTION_GROW
-ES_SCRIPT_ATTACH = ES_ACTION_ATTACH
-ES_SCRIPT_DETACH = ES_ACTION_DETACH
-ES_SCRIPT_SETINFO = ES_ACTION_SETINFO
-ES_SCRIPT_VERIFY = ES_ACTION_VERIFY
-ES_SCRIPTS = frozenset([
-  ES_SCRIPT_CREATE,
-  ES_SCRIPT_REMOVE,
-  ES_SCRIPT_GROW,
-  ES_SCRIPT_ATTACH,
-  ES_SCRIPT_DETACH,
-  ES_SCRIPT_SETINFO,
-  ES_SCRIPT_VERIFY
-  ])
-
-ES_PARAMETERS_FILE = "parameters.list"
-
-# reboot types
-INSTANCE_REBOOT_SOFT = "soft"
-INSTANCE_REBOOT_HARD = "hard"
-INSTANCE_REBOOT_FULL = "full"
-
-REBOOT_TYPES = compat.UniqueFrozenset([
-  INSTANCE_REBOOT_SOFT,
-  INSTANCE_REBOOT_HARD,
-  INSTANCE_REBOOT_FULL,
-  ])
-
-# instance reboot behaviors
-INSTANCE_REBOOT_ALLOWED = "reboot"
-INSTANCE_REBOOT_EXIT = "exit"
-
-REBOOT_BEHAVIORS = compat.UniqueFrozenset([
-  INSTANCE_REBOOT_ALLOWED,
-  INSTANCE_REBOOT_EXIT,
-  ])
-
-VTYPE_STRING = "string"
-VTYPE_MAYBE_STRING = "maybe-string"
-VTYPE_BOOL = "bool"
-VTYPE_SIZE = "size" # size, in MiBs
-VTYPE_INT = "int"
-ENFORCEABLE_TYPES = compat.UniqueFrozenset([
-  VTYPE_STRING,
-  VTYPE_MAYBE_STRING,
-  VTYPE_BOOL,
-  VTYPE_SIZE,
-  VTYPE_INT,
-  ])
-
-# Constant representing that the user does not specify any IP version
-IFACE_NO_IP_VERSION_SPECIFIED = 0
-
-VALID_SERIAL_SPEEDS = compat.UniqueFrozenset([
-  75,
-  110,
-  300,
-  600,
-  1200,
-  1800,
-  2400,
-  4800,
-  9600,
-  14400,
-  19200,
-  28800,
-  38400,
-  57600,
-  115200,
-  230400,
-  345600,
-  460800,
-  ])
-
-# HV parameter names (global namespace)
-HV_BOOT_ORDER = "boot_order"
-HV_CDROM_IMAGE_PATH = "cdrom_image_path"
-HV_KVM_CDROM2_IMAGE_PATH = "cdrom2_image_path"
-HV_KVM_FLOPPY_IMAGE_PATH = "floppy_image_path"
-HV_NIC_TYPE = "nic_type"
-HV_DISK_TYPE = "disk_type"
-HV_KVM_CDROM_DISK_TYPE = "cdrom_disk_type"
-HV_VNC_BIND_ADDRESS = "vnc_bind_address"
-HV_VNC_PASSWORD_FILE = "vnc_password_file"
-HV_VNC_TLS = "vnc_tls"
-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"
-HV_BOOTLOADER_ARGS = "bootloader_args"
-HV_BOOTLOADER_PATH = "bootloader_path"
-HV_KERNEL_ARGS = "kernel_args"
-HV_KERNEL_PATH = "kernel_path"
-HV_INITRD_PATH = "initrd_path"
-HV_ROOT_PATH = "root_path"
-HV_SERIAL_CONSOLE = "serial_console"
-HV_SERIAL_SPEED = "serial_speed"
-HV_USB_MOUSE = "usb_mouse"
-HV_KEYMAP = "keymap"
-HV_DEVICE_MODEL = "device_model"
-HV_INIT_SCRIPT = "init_script"
-HV_MIGRATION_PORT = "migration_port"
-HV_MIGRATION_BANDWIDTH = "migration_bandwidth"
-HV_MIGRATION_DOWNTIME = "migration_downtime"
-HV_MIGRATION_MODE = "migration_mode"
-HV_USE_LOCALTIME = "use_localtime"
-HV_DISK_CACHE = "disk_cache"
-HV_SECURITY_MODEL = "security_model"
-HV_SECURITY_DOMAIN = "security_domain"
-HV_KVM_FLAG = "kvm_flag"
-HV_VHOST_NET = "vhost_net"
-HV_KVM_USE_CHROOT = "use_chroot"
-HV_CPU_MASK = "cpu_mask"
-HV_MEM_PATH = "mem_path"
-HV_PASSTHROUGH = "pci_pass"
-HV_BLOCKDEV_PREFIX = "blockdev_prefix"
-HV_REBOOT_BEHAVIOR = "reboot_behavior"
-HV_CPU_TYPE = "cpu_type"
-HV_CPU_CAP = "cpu_cap"
-HV_CPU_WEIGHT = "cpu_weight"
-HV_CPU_CORES = "cpu_cores"
-HV_CPU_THREADS = "cpu_threads"
-HV_CPU_SOCKETS = "cpu_sockets"
-HV_SOUNDHW = "soundhw"
-HV_USB_DEVICES = "usb_devices"
-HV_VGA = "vga"
-HV_KVM_EXTRA = "kvm_extra"
-HV_KVM_MACHINE_VERSION = "machine_version"
-HV_KVM_PATH = "kvm_path"
-HV_VIF_TYPE = "vif_type"
-HV_VIF_SCRIPT = "vif_script"
-HV_XEN_CMD = "xen_cmd"
-HV_VNET_HDR = "vnet_hdr"
-HV_VIRIDIAN = "viridian"
-
+TCP_PING_TIMEOUT = _constants.TCP_PING_TIMEOUT
+DEFAULT_VG = _constants.DEFAULT_VG
+DEFAULT_DRBD_HELPER = _constants.DEFAULT_DRBD_HELPER
+MIN_VG_SIZE = _constants.MIN_VG_SIZE
+DEFAULT_MAC_PREFIX = _constants.DEFAULT_MAC_PREFIX
+DEFAULT_SHUTDOWN_TIMEOUT = _constants.DEFAULT_SHUTDOWN_TIMEOUT
+NODE_MAX_CLOCK_SKEW = _constants.NODE_MAX_CLOCK_SKEW
+DISK_TRANSFER_CONNECT_TIMEOUT = _constants.DISK_TRANSFER_CONNECT_TIMEOUT
+DISK_SEPARATOR = _constants.DISK_SEPARATOR
+IP_COMMAND_PATH = _constants.IP_COMMAND_PATH
+JOB_IDS_KEY = _constants.JOB_IDS_KEY
+
+RUNPARTS_SKIP = _constants.RUNPARTS_SKIP
+RUNPARTS_RUN = _constants.RUNPARTS_RUN
+RUNPARTS_ERR = _constants.RUNPARTS_ERR
+RUNPARTS_STATUS = _constants.RUNPARTS_STATUS
+
+RPC_ENCODING_NONE = _constants.RPC_ENCODING_NONE
+RPC_ENCODING_ZLIB_BASE64 = _constants.RPC_ENCODING_ZLIB_BASE64
+
+RPC_TMO_URGENT = _constants.RPC_TMO_URGENT
+RPC_TMO_FAST = _constants.RPC_TMO_FAST
+RPC_TMO_NORMAL = _constants.RPC_TMO_NORMAL
+RPC_TMO_SLOW = _constants.RPC_TMO_SLOW
+RPC_TMO_4HRS = _constants.RPC_TMO_4HRS
+RPC_TMO_1DAY = _constants.RPC_TMO_1DAY
+RPC_CONNECT_TIMEOUT = _constants.RPC_CONNECT_TIMEOUT
+
+OS_SCRIPT_CREATE = _constants.OS_SCRIPT_CREATE
+OS_SCRIPT_IMPORT = _constants.OS_SCRIPT_IMPORT
+OS_SCRIPT_EXPORT = _constants.OS_SCRIPT_EXPORT
+OS_SCRIPT_RENAME = _constants.OS_SCRIPT_RENAME
+OS_SCRIPT_VERIFY = _constants.OS_SCRIPT_VERIFY
+OS_SCRIPTS = _constants.OS_SCRIPTS
+
+OS_API_FILE = _constants.OS_API_FILE
+OS_VARIANTS_FILE = _constants.OS_VARIANTS_FILE
+OS_PARAMETERS_FILE = _constants.OS_PARAMETERS_FILE
+
+OS_VALIDATE_PARAMETERS = _constants.OS_VALIDATE_PARAMETERS
+OS_VALIDATE_CALLS = _constants.OS_VALIDATE_CALLS
+
+ES_ACTION_CREATE = _constants.ES_ACTION_CREATE
+ES_ACTION_REMOVE = _constants.ES_ACTION_REMOVE
+ES_ACTION_GROW = _constants.ES_ACTION_GROW
+ES_ACTION_ATTACH = _constants.ES_ACTION_ATTACH
+ES_ACTION_DETACH = _constants.ES_ACTION_DETACH
+ES_ACTION_SETINFO = _constants.ES_ACTION_SETINFO
+ES_ACTION_VERIFY = _constants.ES_ACTION_VERIFY
+
+ES_SCRIPT_CREATE = _constants.ES_SCRIPT_CREATE
+ES_SCRIPT_REMOVE = _constants.ES_SCRIPT_REMOVE
+ES_SCRIPT_GROW = _constants.ES_SCRIPT_GROW
+ES_SCRIPT_ATTACH = _constants.ES_SCRIPT_ATTACH
+ES_SCRIPT_DETACH = _constants.ES_SCRIPT_DETACH
+ES_SCRIPT_SETINFO = _constants.ES_SCRIPT_SETINFO
+ES_SCRIPT_VERIFY = _constants.ES_SCRIPT_VERIFY
+ES_SCRIPTS = _constants.ES_SCRIPTS
+
+ES_PARAMETERS_FILE = _constants.ES_PARAMETERS_FILE
+
+INSTANCE_REBOOT_SOFT = _constants.INSTANCE_REBOOT_SOFT
+INSTANCE_REBOOT_HARD = _constants.INSTANCE_REBOOT_HARD
+INSTANCE_REBOOT_FULL = _constants.INSTANCE_REBOOT_FULL
+REBOOT_TYPES = _constants.REBOOT_TYPES
+
+INSTANCE_REBOOT_ALLOWED = _constants.INSTANCE_REBOOT_ALLOWED
+INSTANCE_REBOOT_EXIT = _constants.INSTANCE_REBOOT_EXIT
+REBOOT_BEHAVIORS = _constants.REBOOT_BEHAVIORS
+
+VTYPE_STRING = _constants.VTYPE_STRING
+VTYPE_MAYBE_STRING = _constants.VTYPE_MAYBE_STRING
+VTYPE_BOOL = _constants.VTYPE_BOOL
+VTYPE_SIZE = _constants.VTYPE_SIZE
+VTYPE_INT = _constants.VTYPE_INT
+ENFORCEABLE_TYPES = _constants.ENFORCEABLE_TYPES
+
+IFACE_NO_IP_VERSION_SPECIFIED = _constants.IFACE_NO_IP_VERSION_SPECIFIED
+
+VALID_SERIAL_SPEEDS = _constants.VALID_SERIAL_SPEEDS
+
+HV_BOOT_ORDER = _constants.HV_BOOT_ORDER
+HV_CDROM_IMAGE_PATH = _constants.HV_CDROM_IMAGE_PATH
+HV_KVM_CDROM2_IMAGE_PATH = _constants.HV_KVM_CDROM2_IMAGE_PATH
+HV_KVM_FLOPPY_IMAGE_PATH = _constants.HV_KVM_FLOPPY_IMAGE_PATH
+HV_NIC_TYPE = _constants.HV_NIC_TYPE
+HV_DISK_TYPE = _constants.HV_DISK_TYPE
+HV_KVM_CDROM_DISK_TYPE = _constants.HV_KVM_CDROM_DISK_TYPE
+HV_VNC_BIND_ADDRESS = _constants.HV_VNC_BIND_ADDRESS
+HV_VNC_PASSWORD_FILE = _constants.HV_VNC_PASSWORD_FILE
+HV_VNC_TLS = _constants.HV_VNC_TLS
+HV_VNC_X509 = _constants.HV_VNC_X509
+HV_VNC_X509_VERIFY = _constants.HV_VNC_X509_VERIFY
+HV_KVM_SPICE_BIND = _constants.HV_KVM_SPICE_BIND
+HV_KVM_SPICE_IP_VERSION = _constants.HV_KVM_SPICE_IP_VERSION
+HV_KVM_SPICE_PASSWORD_FILE = _constants.HV_KVM_SPICE_PASSWORD_FILE
+HV_KVM_SPICE_LOSSLESS_IMG_COMPR = _constants.HV_KVM_SPICE_LOSSLESS_IMG_COMPR
+HV_KVM_SPICE_JPEG_IMG_COMPR = _constants.HV_KVM_SPICE_JPEG_IMG_COMPR
+HV_KVM_SPICE_ZLIB_GLZ_IMG_COMPR = _constants.HV_KVM_SPICE_ZLIB_GLZ_IMG_COMPR
+HV_KVM_SPICE_STREAMING_VIDEO_DETECTION = \
+  _constants.HV_KVM_SPICE_STREAMING_VIDEO_DETECTION
+HV_KVM_SPICE_AUDIO_COMPR = _constants.HV_KVM_SPICE_AUDIO_COMPR
+HV_KVM_SPICE_USE_TLS = _constants.HV_KVM_SPICE_USE_TLS
+HV_KVM_SPICE_TLS_CIPHERS = _constants.HV_KVM_SPICE_TLS_CIPHERS
+HV_KVM_SPICE_USE_VDAGENT = _constants.HV_KVM_SPICE_USE_VDAGENT
+HV_ACPI = _constants.HV_ACPI
+HV_PAE = _constants.HV_PAE
+HV_USE_BOOTLOADER = _constants.HV_USE_BOOTLOADER
+HV_BOOTLOADER_ARGS = _constants.HV_BOOTLOADER_ARGS
+HV_BOOTLOADER_PATH = _constants.HV_BOOTLOADER_PATH
+HV_KERNEL_ARGS = _constants.HV_KERNEL_ARGS
+HV_KERNEL_PATH = _constants.HV_KERNEL_PATH
+HV_INITRD_PATH = _constants.HV_INITRD_PATH
+HV_ROOT_PATH = _constants.HV_ROOT_PATH
+HV_SERIAL_CONSOLE = _constants.HV_SERIAL_CONSOLE
+HV_SERIAL_SPEED = _constants.HV_SERIAL_SPEED
+HV_USB_MOUSE = _constants.HV_USB_MOUSE
+HV_KEYMAP = _constants.HV_KEYMAP
+HV_DEVICE_MODEL = _constants.HV_DEVICE_MODEL
+HV_INIT_SCRIPT = _constants.HV_INIT_SCRIPT
+HV_MIGRATION_PORT = _constants.HV_MIGRATION_PORT
+HV_MIGRATION_BANDWIDTH = _constants.HV_MIGRATION_BANDWIDTH
+HV_MIGRATION_DOWNTIME = _constants.HV_MIGRATION_DOWNTIME
+HV_MIGRATION_MODE = _constants.HV_MIGRATION_MODE
+HV_USE_LOCALTIME = _constants.HV_USE_LOCALTIME
+HV_DISK_CACHE = _constants.HV_DISK_CACHE
+HV_SECURITY_MODEL = _constants.HV_SECURITY_MODEL
+HV_SECURITY_DOMAIN = _constants.HV_SECURITY_DOMAIN
+HV_KVM_FLAG = _constants.HV_KVM_FLAG
+HV_VHOST_NET = _constants.HV_VHOST_NET
+HV_KVM_USE_CHROOT = _constants.HV_KVM_USE_CHROOT
+HV_CPU_MASK = _constants.HV_CPU_MASK
+HV_MEM_PATH = _constants.HV_MEM_PATH
+HV_PASSTHROUGH = _constants.HV_PASSTHROUGH
+HV_BLOCKDEV_PREFIX = _constants.HV_BLOCKDEV_PREFIX
+HV_REBOOT_BEHAVIOR = _constants.HV_REBOOT_BEHAVIOR
+HV_CPU_TYPE = _constants.HV_CPU_TYPE
+HV_CPU_CAP = _constants.HV_CPU_CAP
+HV_CPU_WEIGHT = _constants.HV_CPU_WEIGHT
+HV_CPU_CORES = _constants.HV_CPU_CORES
+HV_CPU_THREADS = _constants.HV_CPU_THREADS
+HV_CPU_SOCKETS = _constants.HV_CPU_SOCKETS
+HV_SOUNDHW = _constants.HV_SOUNDHW
+HV_USB_DEVICES = _constants.HV_USB_DEVICES
+HV_VGA = _constants.HV_VGA
+HV_KVM_EXTRA = _constants.HV_KVM_EXTRA
+HV_KVM_MACHINE_VERSION = _constants.HV_KVM_MACHINE_VERSION
+HV_KVM_PATH = _constants.HV_KVM_PATH
+HV_VIF_TYPE = _constants.HV_VIF_TYPE
+HV_VIF_SCRIPT = _constants.HV_VIF_SCRIPT
+HV_XEN_CMD = _constants.HV_XEN_CMD
+HV_XEN_CPUID = _constants.HV_XEN_CPUID
+HV_VNET_HDR = _constants.HV_VNET_HDR
+HV_VIRIDIAN = _constants.HV_VIRIDIAN
 
 HVS_PARAMETER_TYPES = {
   HV_KVM_PATH: VTYPE_STRING,
@@ -1016,1514 +621,732 @@ HVS_PARAMETER_TYPES = {
   HV_VIF_TYPE: VTYPE_STRING,
   HV_VIF_SCRIPT: VTYPE_STRING,
   HV_XEN_CMD: VTYPE_STRING,
+  HV_XEN_CPUID: VTYPE_STRING,
   HV_VNET_HDR: VTYPE_BOOL,
   HV_VIRIDIAN: VTYPE_BOOL,
   }
 
 HVS_PARAMETERS = frozenset(HVS_PARAMETER_TYPES.keys())
 
-HVS_PARAMETER_TITLES = {
-  HV_ACPI: "ACPI",
-  HV_BOOT_ORDER: "Boot_order",
-  HV_CDROM_IMAGE_PATH: "CDROM_image_path",
-  HV_DISK_TYPE: "Disk_type",
-  HV_INITRD_PATH: "Initrd_path",
-  HV_KERNEL_PATH: "Kernel_path",
-  HV_NIC_TYPE: "NIC_type",
-  HV_PAE: "PAE",
-  HV_VNC_BIND_ADDRESS: "VNC_bind_address",
-  HV_PASSTHROUGH: "pci_pass",
-  HV_CPU_TYPE: "cpu_type",
-  }
+HVS_PARAMETER_TITLES = _constants.HVS_PARAMETER_TITLES
+
+HV_MIGRATION_COMPLETED = _constants.HV_MIGRATION_COMPLETED
+HV_MIGRATION_ACTIVE = _constants.HV_MIGRATION_ACTIVE
+HV_MIGRATION_FAILED = _constants.HV_MIGRATION_FAILED
+HV_MIGRATION_CANCELLED = _constants.HV_MIGRATION_CANCELLED
+HV_MIGRATION_VALID_STATUSES = _constants.HV_MIGRATION_VALID_STATUSES
+HV_MIGRATION_FAILED_STATUSES = _constants.HV_MIGRATION_FAILED_STATUSES
+HV_KVM_MIGRATION_VALID_STATUSES = _constants.HV_KVM_MIGRATION_VALID_STATUSES
+
+HV_NODEINFO_KEY_VERSION = _constants.HV_NODEINFO_KEY_VERSION
+
+HVST_MEMORY_TOTAL = _constants.HVST_MEMORY_TOTAL
+HVST_MEMORY_NODE = _constants.HVST_MEMORY_NODE
+HVST_MEMORY_HV = _constants.HVST_MEMORY_HV
+HVST_CPU_TOTAL = _constants.HVST_CPU_TOTAL
+HVST_CPU_NODE = _constants.HVST_CPU_NODE
+HVSTS_PARAMETERS = _constants.HVSTS_PARAMETERS
+HVST_DEFAULTS = _constants.HVST_DEFAULTS
+HVSTS_PARAMETER_TYPES = _constants.HVSTS_PARAMETER_TYPES
+
+DS_DISK_TOTAL = _constants.DS_DISK_TOTAL
+DS_DISK_RESERVED = _constants.DS_DISK_RESERVED
+DS_DISK_OVERHEAD = _constants.DS_DISK_OVERHEAD
+DS_DEFAULTS = _constants.DS_DEFAULTS
+DSS_PARAMETER_TYPES = _constants.DSS_PARAMETER_TYPES
+DSS_PARAMETERS = _constants.DSS_PARAMETERS
+DS_VALID_TYPES = _constants.DS_VALID_TYPES
+
+BE_MEMORY = _constants.BE_MEMORY
+BE_MAXMEM = _constants.BE_MAXMEM
+BE_MINMEM = _constants.BE_MINMEM
+BE_VCPUS = _constants.BE_VCPUS
+BE_AUTO_BALANCE = _constants.BE_AUTO_BALANCE
+BE_ALWAYS_FAILOVER = _constants.BE_ALWAYS_FAILOVER
+BE_SPINDLE_USE = _constants.BE_SPINDLE_USE
+BES_PARAMETER_TYPES = _constants.BES_PARAMETER_TYPES
+BES_PARAMETER_TITLES = _constants.BES_PARAMETER_TITLES
+BES_PARAMETER_COMPAT = _constants.BES_PARAMETER_COMPAT
+BES_PARAMETERS = _constants.BES_PARAMETERS
+
+ISPEC_MEM_SIZE = _constants.ISPEC_MEM_SIZE
+ISPEC_CPU_COUNT = _constants.ISPEC_CPU_COUNT
+ISPEC_DISK_COUNT = _constants.ISPEC_DISK_COUNT
+ISPEC_DISK_SIZE = _constants.ISPEC_DISK_SIZE
+ISPEC_NIC_COUNT = _constants.ISPEC_NIC_COUNT
+ISPEC_SPINDLE_USE = _constants.ISPEC_SPINDLE_USE
+ISPECS_PARAMETER_TYPES = _constants.ISPECS_PARAMETER_TYPES
+ISPECS_PARAMETERS = _constants.ISPECS_PARAMETERS
+
+ISPECS_MINMAX = _constants.ISPECS_MINMAX
+ISPECS_MIN = _constants.ISPECS_MIN
+ISPECS_MAX = _constants.ISPECS_MAX
+ISPECS_STD = _constants.ISPECS_STD
+IPOLICY_DTS = _constants.IPOLICY_DTS
+IPOLICY_VCPU_RATIO = _constants.IPOLICY_VCPU_RATIO
+IPOLICY_SPINDLE_RATIO = _constants.IPOLICY_SPINDLE_RATIO
+ISPECS_MINMAX_KEYS = _constants.ISPECS_MINMAX_KEYS
+IPOLICY_PARAMETERS = _constants.IPOLICY_PARAMETERS
+IPOLICY_ALL_KEYS = _constants.IPOLICY_ALL_KEYS
+
+ND_OOB_PROGRAM = _constants.ND_OOB_PROGRAM
+ND_SPINDLE_COUNT = _constants.ND_SPINDLE_COUNT
+ND_EXCLUSIVE_STORAGE = _constants.ND_EXCLUSIVE_STORAGE
+ND_OVS = _constants.ND_OVS
+ND_OVS_NAME = _constants.ND_OVS_NAME
+ND_OVS_LINK = _constants.ND_OVS_LINK
+
+NDS_PARAMETER_TYPES = _constants.NDS_PARAMETER_TYPES
+NDS_PARAMETERS = _constants.NDS_PARAMETERS
+NDS_PARAMETER_TITLES = _constants.NDS_PARAMETER_TITLES
+
+LDP_RESYNC_RATE = _constants.LDP_RESYNC_RATE
+LDP_STRIPES = _constants.LDP_STRIPES
+LDP_BARRIERS = _constants.LDP_BARRIERS
+LDP_NO_META_FLUSH = _constants.LDP_NO_META_FLUSH
+LDP_DEFAULT_METAVG = _constants.LDP_DEFAULT_METAVG
+LDP_DISK_CUSTOM = _constants.LDP_DISK_CUSTOM
+LDP_NET_CUSTOM = _constants.LDP_NET_CUSTOM
+LDP_PROTOCOL = _constants.LDP_PROTOCOL
+LDP_DYNAMIC_RESYNC = _constants.LDP_DYNAMIC_RESYNC
+LDP_PLAN_AHEAD = _constants.LDP_PLAN_AHEAD
+LDP_FILL_TARGET = _constants.LDP_FILL_TARGET
+LDP_DELAY_TARGET = _constants.LDP_DELAY_TARGET
+LDP_MAX_RATE = _constants.LDP_MAX_RATE
+LDP_MIN_RATE = _constants.LDP_MIN_RATE
+LDP_POOL = _constants.LDP_POOL
+LDP_ACCESS = _constants.LDP_ACCESS
+DISK_LD_TYPES = _constants.DISK_LD_TYPES
+DISK_LD_PARAMETERS = _constants.DISK_LD_PARAMETERS
+
+DRBD_RESYNC_RATE = _constants.DRBD_RESYNC_RATE
+DRBD_DATA_STRIPES = _constants.DRBD_DATA_STRIPES
+DRBD_META_STRIPES = _constants.DRBD_META_STRIPES
+DRBD_DISK_BARRIERS = _constants.DRBD_DISK_BARRIERS
+DRBD_META_BARRIERS = _constants.DRBD_META_BARRIERS
+DRBD_DEFAULT_METAVG = _constants.DRBD_DEFAULT_METAVG
+DRBD_DISK_CUSTOM = _constants.DRBD_DISK_CUSTOM
+DRBD_NET_CUSTOM = _constants.DRBD_NET_CUSTOM
+DRBD_PROTOCOL = _constants.DRBD_PROTOCOL
+DRBD_DYNAMIC_RESYNC = _constants.DRBD_DYNAMIC_RESYNC
+DRBD_PLAN_AHEAD = _constants.DRBD_PLAN_AHEAD
+DRBD_FILL_TARGET = _constants.DRBD_FILL_TARGET
+DRBD_DELAY_TARGET = _constants.DRBD_DELAY_TARGET
+DRBD_MAX_RATE = _constants.DRBD_MAX_RATE
+DRBD_MIN_RATE = _constants.DRBD_MIN_RATE
+LV_STRIPES = _constants.LV_STRIPES
+RBD_ACCESS = _constants.RBD_ACCESS
+RBD_POOL = _constants.RBD_POOL
+DISK_DT_TYPES = _constants.DISK_DT_TYPES
+DISK_DT_PARAMETERS = _constants.DISK_DT_PARAMETERS
+
+DDP_LOCAL_IP = _constants.DDP_LOCAL_IP
+DDP_REMOTE_IP = _constants.DDP_REMOTE_IP
+DDP_PORT = _constants.DDP_PORT
+DDP_LOCAL_MINOR = _constants.DDP_LOCAL_MINOR
+DDP_REMOTE_MINOR = _constants.DDP_REMOTE_MINOR
+
+OOB_POWER_ON = _constants.OOB_POWER_ON
+OOB_POWER_OFF = _constants.OOB_POWER_OFF
+OOB_POWER_CYCLE = _constants.OOB_POWER_CYCLE
+OOB_POWER_STATUS = _constants.OOB_POWER_STATUS
+OOB_HEALTH = _constants.OOB_HEALTH
+OOB_COMMANDS = _constants.OOB_COMMANDS
+
+OOB_POWER_STATUS_POWERED = _constants.OOB_POWER_STATUS_POWERED
+
+OOB_TIMEOUT = _constants.OOB_TIMEOUT
+OOB_POWER_DELAY = _constants.OOB_POWER_DELAY
+
+OOB_STATUS_OK = _constants.OOB_STATUS_OK
+OOB_STATUS_WARNING = _constants.OOB_STATUS_WARNING
+OOB_STATUS_CRITICAL = _constants.OOB_STATUS_CRITICAL
+OOB_STATUS_UNKNOWN = _constants.OOB_STATUS_UNKNOWN
+OOB_STATUSES = _constants.OOB_STATUSES
+
+PP_DEFAULT = _constants.PP_DEFAULT
+
+NIC_MODE = _constants.NIC_MODE
+NIC_LINK = _constants.NIC_LINK
+NIC_VLAN = _constants.NIC_VLAN
+NICS_PARAMETER_TYPES = _constants.NICS_PARAMETER_TYPES
+NICS_PARAMETERS = _constants.NICS_PARAMETERS
+
+NIC_MODE_BRIDGED = _constants.NIC_MODE_BRIDGED
+NIC_MODE_ROUTED = _constants.NIC_MODE_ROUTED
+NIC_MODE_OVS = _constants.NIC_MODE_OVS
+NIC_IP_POOL = _constants.NIC_IP_POOL
+NIC_VALID_MODES = _constants.NIC_VALID_MODES
+
+RESERVE_ACTION = _constants.RESERVE_ACTION
+RELEASE_ACTION = _constants.RELEASE_ACTION
+
+IDISK_SIZE = _constants.IDISK_SIZE
+IDISK_SPINDLES = _constants.IDISK_SPINDLES
+IDISK_MODE = _constants.IDISK_MODE
+IDISK_ADOPT = _constants.IDISK_ADOPT
+IDISK_VG = _constants.IDISK_VG
+IDISK_METAVG = _constants.IDISK_METAVG
+IDISK_PROVIDER = _constants.IDISK_PROVIDER
+IDISK_NAME = _constants.IDISK_NAME
+IDISK_PARAMS_TYPES = _constants.IDISK_PARAMS_TYPES
+IDISK_PARAMS = _constants.IDISK_PARAMS
+
+INIC_MAC = _constants.INIC_MAC
+INIC_IP = _constants.INIC_IP
+INIC_MODE = _constants.INIC_MODE
+INIC_LINK = _constants.INIC_LINK
+INIC_NETWORK = _constants.INIC_NETWORK
+INIC_NAME = _constants.INIC_NAME
+INIC_VLAN = _constants.INIC_VLAN
+INIC_BRIDGE = _constants.INIC_BRIDGE
+INIC_PARAMS_TYPES = _constants.INIC_PARAMS_TYPES
+INIC_PARAMS = _constants.INIC_PARAMS
+
+HT_XEN_PVM = _constants.HT_XEN_PVM
+HT_FAKE = _constants.HT_FAKE
+HT_XEN_HVM = _constants.HT_XEN_HVM
+HT_KVM = _constants.HT_KVM
+HT_CHROOT = _constants.HT_CHROOT
+HT_LXC = _constants.HT_LXC
+HYPER_TYPES = _constants.HYPER_TYPES
+HTS_REQ_PORT = _constants.HTS_REQ_PORT
+
+VNC_BASE_PORT = _constants.VNC_BASE_PORT
+VNC_DEFAULT_BIND_ADDRESS = _constants.VNC_DEFAULT_BIND_ADDRESS
+
+HT_NIC_RTL8139 = _constants.HT_NIC_RTL8139
+HT_NIC_NE2K_PCI = _constants.HT_NIC_NE2K_PCI
+HT_NIC_NE2K_ISA = _constants.HT_NIC_NE2K_ISA
+HT_NIC_I82551 = _constants.HT_NIC_I82551
+HT_NIC_I85557B = _constants.HT_NIC_I85557B
+HT_NIC_I8259ER = _constants.HT_NIC_I8259ER
+HT_NIC_PCNET = _constants.HT_NIC_PCNET
+HT_NIC_E1000 = _constants.HT_NIC_E1000
+HT_NIC_PARAVIRTUAL = _constants.HT_NIC_PARAVIRTUAL
+HT_HVM_VALID_NIC_TYPES = _constants.HT_HVM_VALID_NIC_TYPES
+HT_KVM_VALID_NIC_TYPES = _constants.HT_KVM_VALID_NIC_TYPES
+
+HT_HVM_VIF_IOEMU = _constants.HT_HVM_VIF_IOEMU
+HT_HVM_VIF_VIF = _constants.HT_HVM_VIF_VIF
+HT_HVM_VALID_VIF_TYPES = _constants.HT_HVM_VALID_VIF_TYPES
+
+HT_DISK_IOEMU = _constants.HT_DISK_IOEMU
+HT_DISK_IDE = _constants.HT_DISK_IDE
+HT_DISK_SCSI = _constants.HT_DISK_SCSI
+HT_DISK_SD = _constants.HT_DISK_SD
+HT_DISK_MTD = _constants.HT_DISK_MTD
+HT_DISK_PFLASH = _constants.HT_DISK_PFLASH
+HT_DISK_PARAVIRTUAL = _constants.HT_DISK_PARAVIRTUAL
+HT_HVM_VALID_DISK_TYPES = _constants.HT_HVM_VALID_DISK_TYPES
+HT_KVM_VALID_DISK_TYPES = _constants.HT_KVM_VALID_DISK_TYPES
+
+HT_CACHE_DEFAULT = _constants.HT_CACHE_DEFAULT
+HT_CACHE_NONE = _constants.HT_CACHE_NONE
+HT_CACHE_WTHROUGH = _constants.HT_CACHE_WTHROUGH
+HT_CACHE_WBACK = _constants.HT_CACHE_WBACK
+HT_VALID_CACHE_TYPES = _constants.HT_VALID_CACHE_TYPES
+
+HT_MOUSE_MOUSE = _constants.HT_MOUSE_MOUSE
+HT_MOUSE_TABLET = _constants.HT_MOUSE_TABLET
+HT_KVM_VALID_MOUSE_TYPES = _constants.HT_KVM_VALID_MOUSE_TYPES
+
+HT_BO_FLOPPY = _constants.HT_BO_FLOPPY
+HT_BO_CDROM = _constants.HT_BO_CDROM
+HT_BO_DISK = _constants.HT_BO_DISK
+HT_BO_NETWORK = _constants.HT_BO_NETWORK
+HT_KVM_VALID_BO_TYPES = _constants.HT_KVM_VALID_BO_TYPES
+
+HT_KVM_SPICE_LOSSLESS_IMG_COMPR_AUTO_GLZ = \
+  _constants.HT_KVM_SPICE_LOSSLESS_IMG_COMPR_AUTO_GLZ
+HT_KVM_SPICE_LOSSLESS_IMG_COMPR_AUTO_LZ = \
+  _constants.HT_KVM_SPICE_LOSSLESS_IMG_COMPR_AUTO_LZ
+HT_KVM_SPICE_LOSSLESS_IMG_COMPR_QUIC = \
+  _constants.HT_KVM_SPICE_LOSSLESS_IMG_COMPR_QUIC
+HT_KVM_SPICE_LOSSLESS_IMG_COMPR_GLZ = \
+  _constants.HT_KVM_SPICE_LOSSLESS_IMG_COMPR_GLZ
+HT_KVM_SPICE_LOSSLESS_IMG_COMPR_LZ = \
+  _constants.HT_KVM_SPICE_LOSSLESS_IMG_COMPR_LZ
+HT_KVM_SPICE_LOSSLESS_IMG_COMPR_OFF = \
+  _constants.HT_KVM_SPICE_LOSSLESS_IMG_COMPR_OFF
+HT_KVM_SPICE_VALID_LOSSLESS_IMG_COMPR_OPTIONS = \
+  _constants.HT_KVM_SPICE_VALID_LOSSLESS_IMG_COMPR_OPTIONS
+
+HT_KVM_SPICE_LOSSY_IMG_COMPR_AUTO = _constants.HT_KVM_SPICE_LOSSY_IMG_COMPR_AUTO
+HT_KVM_SPICE_LOSSY_IMG_COMPR_NEVER = \
+  _constants.HT_KVM_SPICE_LOSSY_IMG_COMPR_NEVER
+HT_KVM_SPICE_LOSSY_IMG_COMPR_ALWAYS = \
+  _constants.HT_KVM_SPICE_LOSSY_IMG_COMPR_ALWAYS
+
+HT_KVM_SPICE_VALID_LOSSY_IMG_COMPR_OPTIONS = \
+  _constants.HT_KVM_SPICE_VALID_LOSSY_IMG_COMPR_OPTIONS
+
+HT_KVM_SPICE_VIDEO_STREAM_DETECTION_OFF = \
+  _constants.HT_KVM_SPICE_VIDEO_STREAM_DETECTION_OFF
+HT_KVM_SPICE_VIDEO_STREAM_DETECTION_ALL = \
+  _constants.HT_KVM_SPICE_VIDEO_STREAM_DETECTION_ALL
+HT_KVM_SPICE_VIDEO_STREAM_DETECTION_FILTER = \
+  _constants.HT_KVM_SPICE_VIDEO_STREAM_DETECTION_FILTER
+HT_KVM_SPICE_VALID_VIDEO_STREAM_DETECTION_OPTIONS = \
+  _constants.HT_KVM_SPICE_VALID_VIDEO_STREAM_DETECTION_OPTIONS
+
+HT_SM_NONE = _constants.HT_SM_NONE
+HT_SM_USER = _constants.HT_SM_USER
+HT_SM_POOL = _constants.HT_SM_POOL
+HT_KVM_VALID_SM_TYPES = _constants.HT_KVM_VALID_SM_TYPES
+
+HT_KVM_ENABLED = _constants.HT_KVM_ENABLED
+HT_KVM_DISABLED = _constants.HT_KVM_DISABLED
+HT_KVM_FLAG_VALUES = _constants.HT_KVM_FLAG_VALUES
+
+HT_MIGRATION_LIVE = _constants.HT_MIGRATION_LIVE
+HT_MIGRATION_NONLIVE = _constants.HT_MIGRATION_NONLIVE
+HT_MIGRATION_MODES = _constants.HT_MIGRATION_MODES
+
+VERIFY_NPLUSONE_MEM = _constants.VERIFY_NPLUSONE_MEM
+VERIFY_OPTIONAL_CHECKS = _constants.VERIFY_OPTIONAL_CHECKS
+
+CV_TCLUSTER = _constants.CV_TCLUSTER
+CV_TGROUP = _constants.CV_TGROUP
+CV_TNODE = _constants.CV_TNODE
+CV_TINSTANCE = _constants.CV_TINSTANCE
+
+CV_ECLUSTERCFG = _constants.CV_ECLUSTERCFG
+CV_ECLUSTERCERT = _constants.CV_ECLUSTERCERT
+CV_ECLUSTERFILECHECK = _constants.CV_ECLUSTERFILECHECK
+CV_ECLUSTERDANGLINGNODES = _constants.CV_ECLUSTERDANGLINGNODES
+CV_ECLUSTERDANGLINGINST = _constants.CV_ECLUSTERDANGLINGINST
+CV_EGROUPDIFFERENTPVSIZE = _constants.CV_EGROUPDIFFERENTPVSIZE
+CV_EINSTANCEBADNODE = _constants.CV_EINSTANCEBADNODE
+CV_EINSTANCEDOWN = _constants.CV_EINSTANCEDOWN
+CV_EINSTANCELAYOUT = _constants.CV_EINSTANCELAYOUT
+CV_EINSTANCEMISSINGDISK = _constants.CV_EINSTANCEMISSINGDISK
+CV_EINSTANCEFAULTYDISK = _constants.CV_EINSTANCEFAULTYDISK
+CV_EINSTANCEWRONGNODE = _constants.CV_EINSTANCEWRONGNODE
+CV_EINSTANCESPLITGROUPS = _constants.CV_EINSTANCESPLITGROUPS
+CV_EINSTANCEPOLICY = _constants.CV_EINSTANCEPOLICY
+CV_EINSTANCEUNSUITABLENODE = _constants.CV_EINSTANCEUNSUITABLENODE
+CV_EINSTANCEMISSINGCFGPARAMETER = _constants.CV_EINSTANCEMISSINGCFGPARAMETER
+CV_ENODEDRBD = _constants.CV_ENODEDRBD
+CV_ENODEDRBDVERSION = _constants.CV_ENODEDRBDVERSION
+CV_ENODEDRBDHELPER = _constants.CV_ENODEDRBDHELPER
+CV_ENODEFILECHECK = _constants.CV_ENODEFILECHECK
+CV_ENODEHOOKS = _constants.CV_ENODEHOOKS
+CV_ENODEHV = _constants.CV_ENODEHV
+CV_ENODELVM = _constants.CV_ENODELVM
+CV_ENODEN1 = _constants.CV_ENODEN1
+CV_ENODENET = _constants.CV_ENODENET
+CV_ENODEOS = _constants.CV_ENODEOS
+CV_ENODEORPHANINSTANCE = _constants.CV_ENODEORPHANINSTANCE
+CV_ENODEORPHANLV = _constants.CV_ENODEORPHANLV
+CV_ENODERPC = _constants.CV_ENODERPC
+CV_ENODESSH = _constants.CV_ENODESSH
+CV_ENODEVERSION = _constants.CV_ENODEVERSION
+CV_ENODESETUP = _constants.CV_ENODESETUP
+CV_ENODETIME = _constants.CV_ENODETIME
+CV_ENODEOOBPATH = _constants.CV_ENODEOOBPATH
+CV_ENODEUSERSCRIPTS = _constants.CV_ENODEUSERSCRIPTS
+CV_ENODEFILESTORAGEPATHS = _constants.CV_ENODEFILESTORAGEPATHS
+CV_ENODEFILESTORAGEPATHUNUSABLE = _constants.CV_ENODEFILESTORAGEPATHUNUSABLE
+CV_ENODESHAREDFILESTORAGEPATHUNUSABLE = \
+  _constants.CV_ENODESHAREDFILESTORAGEPATHUNUSABLE
+
+CV_ALL_ECODES = _constants.CV_ALL_ECODES
+CV_ALL_ECODES_STRINGS = _constants.CV_ALL_ECODES_STRINGS
+
+NV_BRIDGES = _constants.NV_BRIDGES
+NV_DRBDHELPER = _constants.NV_DRBDHELPER
+NV_DRBDVERSION = _constants.NV_DRBDVERSION
+NV_DRBDLIST = _constants.NV_DRBDLIST
+NV_EXCLUSIVEPVS = _constants.NV_EXCLUSIVEPVS
+NV_FILELIST = _constants.NV_FILELIST
+NV_ACCEPTED_STORAGE_PATHS = _constants.NV_ACCEPTED_STORAGE_PATHS
+NV_FILE_STORAGE_PATH = _constants.NV_FILE_STORAGE_PATH
+NV_SHARED_FILE_STORAGE_PATH = _constants.NV_SHARED_FILE_STORAGE_PATH
+NV_HVINFO = _constants.NV_HVINFO
+NV_HVPARAMS = _constants.NV_HVPARAMS
+NV_HYPERVISOR = _constants.NV_HYPERVISOR
+NV_INSTANCELIST = _constants.NV_INSTANCELIST
+NV_LVLIST = _constants.NV_LVLIST
+NV_MASTERIP = _constants.NV_MASTERIP
+NV_NODELIST = _constants.NV_NODELIST
+NV_NODENETTEST = _constants.NV_NODENETTEST
+NV_NODESETUP = _constants.NV_NODESETUP
+NV_OOB_PATHS = _constants.NV_OOB_PATHS
+NV_OSLIST = _constants.NV_OSLIST
+NV_PVLIST = _constants.NV_PVLIST
+NV_TIME = _constants.NV_TIME
+NV_USERSCRIPTS = _constants.NV_USERSCRIPTS
+NV_VERSION = _constants.NV_VERSION
+NV_VGLIST = _constants.NV_VGLIST
+NV_VMNODES = _constants.NV_VMNODES
+
+INSTST_RUNNING = _constants.INSTST_RUNNING
+INSTST_ADMINDOWN = _constants.INSTST_ADMINDOWN
+INSTST_ADMINOFFLINE = _constants.INSTST_ADMINOFFLINE
+INSTST_NODEOFFLINE = _constants.INSTST_NODEOFFLINE
+INSTST_NODEDOWN = _constants.INSTST_NODEDOWN
+INSTST_WRONGNODE = _constants.INSTST_WRONGNODE
+INSTST_ERRORUP = _constants.INSTST_ERRORUP
+INSTST_ERRORDOWN = _constants.INSTST_ERRORDOWN
+INSTST_ALL = _constants.INSTST_ALL
+
+ADMINST_UP = _constants.ADMINST_UP
+ADMINST_DOWN = _constants.ADMINST_DOWN
+ADMINST_OFFLINE = _constants.ADMINST_OFFLINE
+ADMINST_ALL = _constants.ADMINST_ALL
+
+NR_REGULAR = _constants.NR_REGULAR
+NR_MASTER = _constants.NR_MASTER
+NR_MCANDIDATE = _constants.NR_MCANDIDATE
+NR_DRAINED = _constants.NR_DRAINED
+NR_OFFLINE = _constants.NR_OFFLINE
+NR_ALL = _constants.NR_ALL
+
+SSL_CERT_EXPIRATION_WARN = _constants.SSL_CERT_EXPIRATION_WARN
+SSL_CERT_EXPIRATION_ERROR = _constants.SSL_CERT_EXPIRATION_ERROR
+
+IALLOCATOR_VERSION = _constants.IALLOCATOR_VERSION
+IALLOCATOR_DIR_IN = _constants.IALLOCATOR_DIR_IN
+IALLOCATOR_DIR_OUT = _constants.IALLOCATOR_DIR_OUT
+VALID_IALLOCATOR_DIRECTIONS = _constants.VALID_IALLOCATOR_DIRECTIONS
+
+IALLOCATOR_MODE_ALLOC = _constants.IALLOCATOR_MODE_ALLOC
+IALLOCATOR_MODE_RELOC = _constants.IALLOCATOR_MODE_RELOC
+IALLOCATOR_MODE_CHG_GROUP = _constants.IALLOCATOR_MODE_CHG_GROUP
+IALLOCATOR_MODE_NODE_EVAC = _constants.IALLOCATOR_MODE_NODE_EVAC
+IALLOCATOR_MODE_MULTI_ALLOC = _constants.IALLOCATOR_MODE_MULTI_ALLOC
+VALID_IALLOCATOR_MODES = _constants.VALID_IALLOCATOR_MODES
+
+IALLOCATOR_SEARCH_PATH = _constants.IALLOCATOR_SEARCH_PATH
+DEFAULT_IALLOCATOR_SHORTCUT = _constants.DEFAULT_IALLOCATOR_SHORTCUT
+
+NODE_EVAC_PRI = _constants.NODE_EVAC_PRI
+NODE_EVAC_SEC = _constants.NODE_EVAC_SEC
+NODE_EVAC_ALL = _constants.NODE_EVAC_ALL
+NODE_EVAC_MODES = _constants.NODE_EVAC_MODES
+
+JOB_QUEUE_VERSION = _constants.JOB_QUEUE_VERSION
+JOB_QUEUE_SIZE_HARD_LIMIT = _constants.JOB_QUEUE_SIZE_HARD_LIMIT
+JOB_QUEUE_FILES_PERMS = _constants.JOB_QUEUE_FILES_PERMS
 
-# Migration statuses
-HV_MIGRATION_COMPLETED = "completed"
-HV_MIGRATION_ACTIVE = "active"
-HV_MIGRATION_FAILED = "failed"
-HV_MIGRATION_CANCELLED = "cancelled"
-
-HV_MIGRATION_VALID_STATUSES = compat.UniqueFrozenset([
-  HV_MIGRATION_COMPLETED,
-  HV_MIGRATION_ACTIVE,
-  HV_MIGRATION_FAILED,
-  HV_MIGRATION_CANCELLED,
-  ])
-
-HV_MIGRATION_FAILED_STATUSES = compat.UniqueFrozenset([
-  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,
-  }
+JOB_ID_TEMPLATE = r"\d+"
+JOB_FILE_RE = re.compile(r"^job-(%s)$" % JOB_ID_TEMPLATE)
 
-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,
-  }
+JOB_NOTCHANGED = _constants.JOB_NOTCHANGED
+
+JOB_STATUS_QUEUED = _constants.JOB_STATUS_QUEUED
+JOB_STATUS_WAITING = _constants.JOB_STATUS_WAITING
+JOB_STATUS_CANCELING = _constants.JOB_STATUS_CANCELING
+JOB_STATUS_RUNNING = _constants.JOB_STATUS_RUNNING
+JOB_STATUS_CANCELED = _constants.JOB_STATUS_CANCELED
+JOB_STATUS_SUCCESS = _constants.JOB_STATUS_SUCCESS
+JOB_STATUS_ERROR = _constants.JOB_STATUS_ERROR
+JOBS_PENDING = _constants.JOBS_PENDING
+JOBS_FINALIZED = _constants.JOBS_FINALIZED
+JOB_STATUS_ALL = _constants.JOB_STATUS_ALL
+
+OP_STATUS_QUEUED = _constants.OP_STATUS_QUEUED
+OP_STATUS_WAITING = _constants.OP_STATUS_WAITING
+OP_STATUS_CANCELING = _constants.OP_STATUS_CANCELING
+OP_STATUS_RUNNING = _constants.OP_STATUS_RUNNING
+OP_STATUS_CANCELED = _constants.OP_STATUS_CANCELED
+OP_STATUS_SUCCESS = _constants.OP_STATUS_SUCCESS
+OP_STATUS_ERROR = _constants.OP_STATUS_ERROR
+OPS_FINALIZED = _constants.OPS_FINALIZED
+
+OP_PRIO_LOWEST = _constants.OP_PRIO_LOWEST
+OP_PRIO_HIGHEST = _constants.OP_PRIO_HIGHEST
+OP_PRIO_LOW = _constants.OP_PRIO_LOW
+OP_PRIO_NORMAL = _constants.OP_PRIO_NORMAL
+OP_PRIO_HIGH = _constants.OP_PRIO_HIGH
+OP_PRIO_SUBMIT_VALID = _constants.OP_PRIO_SUBMIT_VALID
+OP_PRIO_DEFAULT = _constants.OP_PRIO_DEFAULT
+
+LOCKS_REPLACE = _constants.LOCKS_REPLACE
+LOCKS_APPEND = _constants.LOCKS_APPEND
+
+LOCK_ATTEMPTS_TIMEOUT = _constants.LOCK_ATTEMPTS_TIMEOUT
+LOCK_ATTEMPTS_MAXWAIT = _constants.LOCK_ATTEMPTS_MAXWAIT
+LOCK_ATTEMPTS_MINWAIT = _constants.LOCK_ATTEMPTS_MINWAIT
+
+ELOG_MESSAGE = _constants.ELOG_MESSAGE
+ELOG_REMOTE_IMPORT = _constants.ELOG_REMOTE_IMPORT
+ELOG_JQUEUE_TEST = _constants.ELOG_JQUEUE_TEST
+
+ETC_HOSTS_ADD = _constants.ETC_HOSTS_ADD
+ETC_HOSTS_REMOVE = _constants.ETC_HOSTS_REMOVE
+
+JQT_MSGPREFIX = _constants.JQT_MSGPREFIX
+JQT_EXPANDNAMES = _constants.JQT_EXPANDNAMES
+JQT_EXEC = _constants.JQT_EXEC
+JQT_LOGMSG = _constants.JQT_LOGMSG
+JQT_STARTMSG = _constants.JQT_STARTMSG
+JQT_ALL = _constants.JQT_ALL
+
+QR_CLUSTER = _constants.QR_CLUSTER
+QR_INSTANCE = _constants.QR_INSTANCE
+QR_NODE = _constants.QR_NODE
+QR_LOCK = _constants.QR_LOCK
+QR_GROUP = _constants.QR_GROUP
+QR_OS = _constants.QR_OS
+QR_JOB = _constants.QR_JOB
+QR_EXPORT = _constants.QR_EXPORT
+QR_NETWORK = _constants.QR_NETWORK
+QR_EXTSTORAGE = _constants.QR_EXTSTORAGE
+QR_VIA_OP = _constants.QR_VIA_OP
+QR_VIA_LUXI = _constants.QR_VIA_LUXI
+QR_VIA_RAPI = _constants.QR_VIA_RAPI
+
+QFT_UNKNOWN = _constants.QFT_UNKNOWN
+QFT_TEXT = _constants.QFT_TEXT
+QFT_BOOL = _constants.QFT_BOOL
+QFT_NUMBER = _constants.QFT_NUMBER
+QFT_UNIT = _constants.QFT_UNIT
+QFT_TIMESTAMP = _constants.QFT_TIMESTAMP
+QFT_OTHER = _constants.QFT_OTHER
+QFT_ALL = _constants.QFT_ALL
+
+RS_NORMAL = _constants.RS_NORMAL
+RS_UNKNOWN = _constants.RS_UNKNOWN
+RS_NODATA = _constants.RS_NODATA
+RS_UNAVAIL = _constants.RS_UNAVAIL
+RS_OFFLINE = _constants.RS_OFFLINE
+RS_ALL = _constants.RS_ALL
+RSS_DESCRIPTION = _constants.RSS_DESCRIPTION
+
+MAX_NICS = _constants.MAX_NICS
+MAX_DISKS = _constants.MAX_DISKS
+
+SSCONF_FILEPREFIX = _constants.SSCONF_FILEPREFIX
+
+SS_CLUSTER_NAME = _constants.SS_CLUSTER_NAME
+SS_CLUSTER_TAGS = _constants.SS_CLUSTER_TAGS
+SS_FILE_STORAGE_DIR = _constants.SS_FILE_STORAGE_DIR
+SS_SHARED_FILE_STORAGE_DIR = _constants.SS_SHARED_FILE_STORAGE_DIR
+SS_MASTER_CANDIDATES = _constants.SS_MASTER_CANDIDATES
+SS_MASTER_CANDIDATES_IPS = _constants.SS_MASTER_CANDIDATES_IPS
+SS_MASTER_IP = _constants.SS_MASTER_IP
+SS_MASTER_NETDEV = _constants.SS_MASTER_NETDEV
+SS_MASTER_NETMASK = _constants.SS_MASTER_NETMASK
+SS_MASTER_NODE = _constants.SS_MASTER_NODE
+SS_NODE_LIST = _constants.SS_NODE_LIST
+SS_NODE_PRIMARY_IPS = _constants.SS_NODE_PRIMARY_IPS
+SS_NODE_SECONDARY_IPS = _constants.SS_NODE_SECONDARY_IPS
+SS_OFFLINE_NODES = _constants.SS_OFFLINE_NODES
+SS_ONLINE_NODES = _constants.SS_ONLINE_NODES
+SS_PRIMARY_IP_FAMILY = _constants.SS_PRIMARY_IP_FAMILY
+SS_INSTANCE_LIST = _constants.SS_INSTANCE_LIST
+SS_RELEASE_VERSION = _constants.SS_RELEASE_VERSION
+SS_HYPERVISOR_LIST = _constants.SS_HYPERVISOR_LIST
+SS_MAINTAIN_NODE_HEALTH = _constants.SS_MAINTAIN_NODE_HEALTH
+SS_UID_POOL = _constants.SS_UID_POOL
+SS_NODEGROUPS = _constants.SS_NODEGROUPS
+SS_NETWORKS = _constants.SS_NETWORKS
+
+SS_HVPARAMS_PREF = _constants.SS_HVPARAMS_PREF
+
+SS_HVPARAMS_XEN_PVM = _constants.SS_HVPARAMS_XEN_PVM
+SS_HVPARAMS_XEN_FAKE = _constants.SS_HVPARAMS_XEN_FAKE
+SS_HVPARAMS_XEN_HVM = _constants.SS_HVPARAMS_XEN_HVM
+SS_HVPARAMS_XEN_KVM = _constants.SS_HVPARAMS_XEN_KVM
+SS_HVPARAMS_XEN_CHROOT = _constants.SS_HVPARAMS_XEN_CHROOT
+SS_HVPARAMS_XEN_LXC = _constants.SS_HVPARAMS_XEN_LXC
+VALID_SS_HVPARAMS_KEYS = _constants.VALID_SS_HVPARAMS_KEYS
+
+SS_FILE_PERMS = _constants.SS_FILE_PERMS
+
+DEFAULT_ENABLED_HYPERVISOR = _constants.DEFAULT_ENABLED_HYPERVISOR
+
+# HVC_DEFAULTS contains one value 'HV_VNC_PASSWORD_FILE' which is not
+# a constant because it depends on an environment variable that is
+# used for VClusters.  Therefore, it cannot be automatically generated
+# by Haskell at compilation time (given that this environment variable
+# might be different at runtime).
+HVC_DEFAULTS = _constants.HVC_DEFAULTS
+HVC_DEFAULTS[HT_XEN_HVM][HV_VNC_PASSWORD_FILE] = pathutils.VNC_PASSWORD_FILE
+
+HVC_GLOBALS = _constants.HVC_GLOBALS
+
+BEC_DEFAULTS = _constants.BEC_DEFAULTS
+
+NDC_DEFAULTS = _constants.NDC_DEFAULTS
+NDC_GLOBALS = _constants.NDC_GLOBALS
+
+DISK_LD_DEFAULTS = _constants.DISK_LD_DEFAULTS
+DISK_DT_DEFAULTS = _constants.DISK_DT_DEFAULTS
+
+NICC_DEFAULTS = _constants.NICC_DEFAULTS
+ISPECS_MINMAX_DEFAULTS = _constants.ISPECS_MINMAX_DEFAULTS
+IPOLICY_DEFAULTS = _constants.IPOLICY_DEFAULTS
+
+MASTER_POOL_SIZE_DEFAULT = _constants.MASTER_POOL_SIZE_DEFAULT
+
+PART_MARGIN = _constants.PART_MARGIN
+PART_RESERVED = _constants.PART_RESERVED
+
+CONFD_PROTOCOL_VERSION = _constants.CONFD_PROTOCOL_VERSION
+
+CONFD_REQ_PING = _constants.CONFD_REQ_PING
+CONFD_REQ_NODE_ROLE_BYNAME = _constants.CONFD_REQ_NODE_ROLE_BYNAME
+CONFD_REQ_NODE_PIP_BY_INSTANCE_IP = _constants.CONFD_REQ_NODE_PIP_BY_INSTANCE_IP
+CONFD_REQ_CLUSTER_MASTER = _constants.CONFD_REQ_CLUSTER_MASTER
+CONFD_REQ_NODE_PIP_LIST = _constants.CONFD_REQ_NODE_PIP_LIST
+CONFD_REQ_MC_PIP_LIST = _constants.CONFD_REQ_MC_PIP_LIST
+CONFD_REQ_INSTANCES_IPS_LIST = _constants.CONFD_REQ_INSTANCES_IPS_LIST
+CONFD_REQ_NODE_DRBD = _constants.CONFD_REQ_NODE_DRBD
+CONFD_REQ_NODE_INSTANCES = _constants.CONFD_REQ_NODE_INSTANCES
+CONFD_REQS = _constants.CONFD_REQS
+
+CONFD_REQQ_LINK = _constants.CONFD_REQQ_LINK
+CONFD_REQQ_IP = _constants.CONFD_REQQ_IP
+CONFD_REQQ_IPLIST = _constants.CONFD_REQQ_IPLIST
+CONFD_REQQ_FIELDS = _constants.CONFD_REQQ_FIELDS
+
+# FIXME: perhaps update code that uses these constants to deal with
+# integers instead of strings
+CONFD_REQFIELD_NAME = str(_constants.CONFD_REQFIELD_NAME)
+CONFD_REQFIELD_IP = str(_constants.CONFD_REQFIELD_IP)
+CONFD_REQFIELD_MNODE_PIP = str(_constants.CONFD_REQFIELD_MNODE_PIP)
+
+CONFD_REPL_STATUS_OK = _constants.CONFD_REPL_STATUS_OK
+CONFD_REPL_STATUS_ERROR = _constants.CONFD_REPL_STATUS_ERROR
+CONFD_REPL_STATUS_NOTIMPLEMENTED = _constants.CONFD_REPL_STATUS_NOTIMPLEMENTED
+CONFD_REPL_STATUSES = _constants.CONFD_REPL_STATUSES
 
-HVSTS_PARAMETERS = frozenset(HVSTS_PARAMETER_TYPES.keys())
+CONFD_NODE_ROLE_MASTER = _constants.CONFD_NODE_ROLE_MASTER
+CONFD_NODE_ROLE_CANDIDATE = _constants.CONFD_NODE_ROLE_CANDIDATE
+CONFD_NODE_ROLE_OFFLINE = _constants.CONFD_NODE_ROLE_OFFLINE
+CONFD_NODE_ROLE_DRAINED = _constants.CONFD_NODE_ROLE_DRAINED
+CONFD_NODE_ROLE_REGULAR = _constants.CONFD_NODE_ROLE_REGULAR
 
-# Disk state
-DS_DISK_TOTAL = "disk_total"
-DS_DISK_RESERVED = "disk_reserved"
-DS_DISK_OVERHEAD = "disk_overhead"
+CONFD_ERROR_UNKNOWN_ENTRY = _constants.CONFD_ERROR_UNKNOWN_ENTRY
+CONFD_ERROR_INTERNAL = _constants.CONFD_ERROR_INTERNAL
+CONFD_ERROR_ARGUMENT = _constants.CONFD_ERROR_ARGUMENT
 
-DS_DEFAULTS = {
-  DS_DISK_TOTAL: 0,
-  DS_DISK_RESERVED: 0,
-  DS_DISK_OVERHEAD: 0,
-  }
+CONFD_MAX_CLOCK_SKEW = _constants.CONFD_MAX_CLOCK_SKEW
 
-DSS_PARAMETER_TYPES = {
-  DS_DISK_TOTAL: VTYPE_INT,
-  DS_DISK_RESERVED: VTYPE_INT,
-  DS_DISK_OVERHEAD: VTYPE_INT,
-  }
+CONFD_CONFIG_RELOAD_TIMEOUT = _constants.CONFD_CONFIG_RELOAD_TIMEOUT
+CONFD_CONFIG_RELOAD_RATELIMIT = _constants.CONFD_CONFIG_RELOAD_RATELIMIT
 
-DSS_PARAMETERS = frozenset(DSS_PARAMETER_TYPES.keys())
-DS_VALID_TYPES = compat.UniqueFrozenset([DT_PLAIN])
-
-# Backend parameter names
-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_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,
-  }
+CONFD_MAGIC_FOURCC = _constants.CONFD_MAGIC_FOURCC
 
-BES_PARAMETER_TITLES = {
-  BE_AUTO_BALANCE: "Auto_balance",
-  BE_MAXMEM: "ConfigMaxMem",
-  BE_MINMEM: "ConfigMinMem",
-  BE_VCPUS: "ConfigVCPUs",
-  }
+CONFD_DEFAULT_REQ_COVERAGE = _constants.CONFD_DEFAULT_REQ_COVERAGE
 
-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,
-  }
+CONFD_CLIENT_EXPIRE_TIMEOUT = _constants.CONFD_CLIENT_EXPIRE_TIMEOUT
 
-ISPECS_PARAMETERS = frozenset(ISPECS_PARAMETER_TYPES.keys())
-
-ISPECS_MINMAX = "minmax"
-ISPECS_MIN = "min"
-ISPECS_MAX = "max"
-ISPECS_STD = "std"
-IPOLICY_DTS = "disk-templates"
-IPOLICY_VCPU_RATIO = "vcpu-ratio"
-IPOLICY_SPINDLE_RATIO = "spindle-ratio"
-
-ISPECS_MINMAX_KEYS = compat.UniqueFrozenset([
-  ISPECS_MIN,
-  ISPECS_MAX,
-  ])
-
-IPOLICY_PARAMETERS = compat.UniqueFrozenset([
-  IPOLICY_VCPU_RATIO,
-  IPOLICY_SPINDLE_RATIO,
-  ])
-
-IPOLICY_ALL_KEYS = (IPOLICY_PARAMETERS |
-                    frozenset([ISPECS_MINMAX, ISPECS_STD, IPOLICY_DTS]))
-
-# Node parameter names
-ND_OOB_PROGRAM = "oob_program"
-ND_SPINDLE_COUNT = "spindle_count"
-ND_EXCLUSIVE_STORAGE = "exclusive_storage"
-
-NDS_PARAMETER_TYPES = {
-  ND_OOB_PROGRAM: VTYPE_STRING,
-  ND_SPINDLE_COUNT: VTYPE_INT,
-  ND_EXCLUSIVE_STORAGE: VTYPE_BOOL,
-  }
+MAX_UDP_DATA_SIZE = _constants.MAX_UDP_DATA_SIZE
 
-NDS_PARAMETERS = frozenset(NDS_PARAMETER_TYPES.keys())
+UIDPOOL_UID_MIN = _constants.UIDPOOL_UID_MIN
+UIDPOOL_UID_MAX = _constants.UIDPOOL_UID_MAX
 
-NDS_PARAMETER_TITLES = {
-  ND_OOB_PROGRAM: "OutOfBandProgram",
-  ND_SPINDLE_COUNT: "SpindleCount",
-  ND_EXCLUSIVE_STORAGE: "ExclusiveStorage",
-  }
+PGREP = _constants.PGREP
 
-# 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_PROTOCOL = "protocol"
-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_PROTOCOL: 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_PROTOCOL = "protocol"
-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_PROTOCOL: 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,
-  }
+INITIAL_NODE_GROUP_NAME = _constants.INITIAL_NODE_GROUP_NAME
 
-DISK_DT_PARAMETERS = frozenset(DISK_DT_TYPES.keys())
-
-# OOB supported commands
-OOB_POWER_ON = "power-on"
-OOB_POWER_OFF = "power-off"
-OOB_POWER_CYCLE = "power-cycle"
-OOB_POWER_STATUS = "power-status"
-OOB_HEALTH = "health"
-
-OOB_COMMANDS = compat.UniqueFrozenset([
-  OOB_POWER_ON,
-  OOB_POWER_OFF,
-  OOB_POWER_CYCLE,
-  OOB_POWER_STATUS,
-  OOB_HEALTH,
-  ])
-
-OOB_POWER_STATUS_POWERED = "powered"
-
-OOB_TIMEOUT = 60 # 60 seconds
-OOB_POWER_DELAY = 2.0 # 2 seconds
-
-OOB_STATUS_OK = "OK"
-OOB_STATUS_WARNING = "WARNING"
-OOB_STATUS_CRITICAL = "CRITICAL"
-OOB_STATUS_UNKNOWN = "UNKNOWN"
-
-OOB_STATUSES = compat.UniqueFrozenset([
-  OOB_STATUS_OK,
-  OOB_STATUS_WARNING,
-  OOB_STATUS_CRITICAL,
-  OOB_STATUS_UNKNOWN,
-  ])
-
-# Instance Parameters Profile
-PP_DEFAULT = "default"
-
-# NIC_* constants are used inside the ganeti config
-NIC_MODE = "mode"
-NIC_LINK = "link"
-
-NIC_MODE_BRIDGED = "bridged"
-NIC_MODE_ROUTED = "routed"
-NIC_MODE_OVS = "openvswitch"
-NIC_IP_POOL = "pool"
-
-NIC_VALID_MODES = compat.UniqueFrozenset([
-  NIC_MODE_BRIDGED,
-  NIC_MODE_ROUTED,
-  NIC_MODE_OVS,
-  ])
-
-RESERVE_ACTION = "reserve"
-RELEASE_ACTION = "release"
-
-NICS_PARAMETER_TYPES = {
-  NIC_MODE: VTYPE_STRING,
-  NIC_LINK: VTYPE_STRING,
-  }
+ALLOC_POLICY_PREFERRED = _constants.ALLOC_POLICY_PREFERRED
+ALLOC_POLICY_LAST_RESORT = _constants.ALLOC_POLICY_LAST_RESORT
+ALLOC_POLICY_UNALLOCABLE = _constants.ALLOC_POLICY_UNALLOCABLE
+VALID_ALLOC_POLICIES = _constants.VALID_ALLOC_POLICIES
 
-NICS_PARAMETERS = frozenset(NICS_PARAMETER_TYPES.keys())
-
-# IDISK_* constants are used in opcodes, to create/change disks
-IDISK_SIZE = "size"
-IDISK_SPINDLES = "spindles"
-IDISK_MODE = "mode"
-IDISK_ADOPT = "adopt"
-IDISK_VG = "vg"
-IDISK_METAVG = "metavg"
-IDISK_PROVIDER = "provider"
-IDISK_NAME = "name"
-IDISK_PARAMS_TYPES = {
-  IDISK_SIZE: VTYPE_SIZE,
-  IDISK_SPINDLES: VTYPE_INT,
-  IDISK_MODE: VTYPE_STRING,
-  IDISK_ADOPT: VTYPE_STRING,
-  IDISK_VG: VTYPE_STRING,
-  IDISK_METAVG: VTYPE_STRING,
-  IDISK_PROVIDER: VTYPE_STRING,
-  IDISK_NAME: VTYPE_MAYBE_STRING,
-  }
-IDISK_PARAMS = frozenset(IDISK_PARAMS_TYPES.keys())
-
-# INIC_* constants are used in opcodes, to create/change nics
-INIC_MAC = "mac"
-INIC_IP = "ip"
-INIC_MODE = "mode"
-INIC_LINK = "link"
-INIC_NETWORK = "network"
-INIC_NAME = "name"
-INIC_PARAMS_TYPES = {
-  INIC_IP: VTYPE_MAYBE_STRING,
-  INIC_LINK: VTYPE_STRING,
-  INIC_MAC: VTYPE_STRING,
-  INIC_MODE: VTYPE_STRING,
-  INIC_NETWORK: VTYPE_MAYBE_STRING,
-  INIC_NAME: VTYPE_MAYBE_STRING,
-  }
-INIC_PARAMS = frozenset(INIC_PARAMS_TYPES.keys())
-
-# Hypervisor constants
-HT_XEN_PVM = "xen-pvm"
-HT_FAKE = "fake"
-HT_XEN_HVM = "xen-hvm"
-HT_KVM = "kvm"
-HT_CHROOT = "chroot"
-HT_LXC = "lxc"
-HYPER_TYPES = compat.UniqueFrozenset([
-  HT_XEN_PVM,
-  HT_FAKE,
-  HT_XEN_HVM,
-  HT_KVM,
-  HT_CHROOT,
-  HT_LXC,
-  ])
-HTS_REQ_PORT = compat.UniqueFrozenset([HT_XEN_HVM, HT_KVM])
-
-VNC_BASE_PORT = 5900
-VNC_DEFAULT_BIND_ADDRESS = IP4_ADDRESS_ANY
-
-# NIC types
-HT_NIC_RTL8139 = "rtl8139"
-HT_NIC_NE2K_PCI = "ne2k_pci"
-HT_NIC_NE2K_ISA = "ne2k_isa"
-HT_NIC_I82551 = "i82551"
-HT_NIC_I85557B = "i82557b"
-HT_NIC_I8259ER = "i82559er"
-HT_NIC_PCNET = "pcnet"
-HT_NIC_E1000 = "e1000"
-HT_NIC_PARAVIRTUAL = HT_DISK_PARAVIRTUAL = "paravirtual"
-
-HT_HVM_VALID_NIC_TYPES = compat.UniqueFrozenset([
-  HT_NIC_RTL8139,
-  HT_NIC_NE2K_PCI,
-  HT_NIC_E1000,
-  HT_NIC_NE2K_ISA,
-  HT_NIC_PARAVIRTUAL,
-  ])
-HT_KVM_VALID_NIC_TYPES = compat.UniqueFrozenset([
-  HT_NIC_RTL8139,
-  HT_NIC_NE2K_PCI,
-  HT_NIC_NE2K_ISA,
-  HT_NIC_I82551,
-  HT_NIC_I85557B,
-  HT_NIC_I8259ER,
-  HT_NIC_PCNET,
-  HT_NIC_E1000,
-  HT_NIC_PARAVIRTUAL,
-  ])
-
-# Vif types
-# default vif type in xen-hvm
-HT_HVM_VIF_IOEMU = "ioemu"
-HT_HVM_VIF_VIF = "vif"
-HT_HVM_VALID_VIF_TYPES = compat.UniqueFrozenset([
-  HT_HVM_VIF_IOEMU,
-  HT_HVM_VIF_VIF,
-  ])
-
-# Disk types
-HT_DISK_IOEMU = "ioemu"
-HT_DISK_IDE = "ide"
-HT_DISK_SCSI = "scsi"
-HT_DISK_SD = "sd"
-HT_DISK_MTD = "mtd"
-HT_DISK_PFLASH = "pflash"
-
-HT_CACHE_DEFAULT = "default"
-HT_CACHE_NONE = "none"
-HT_CACHE_WTHROUGH = "writethrough"
-HT_CACHE_WBACK = "writeback"
-HT_VALID_CACHE_TYPES = compat.UniqueFrozenset([
-  HT_CACHE_DEFAULT,
-  HT_CACHE_NONE,
-  HT_CACHE_WTHROUGH,
-  HT_CACHE_WBACK,
-  ])
-
-HT_HVM_VALID_DISK_TYPES = compat.UniqueFrozenset([
-  HT_DISK_PARAVIRTUAL,
-  HT_DISK_IOEMU,
-  ])
-HT_KVM_VALID_DISK_TYPES = compat.UniqueFrozenset([
-  HT_DISK_PARAVIRTUAL,
-  HT_DISK_IDE,
-  HT_DISK_SCSI,
-  HT_DISK_SD,
-  HT_DISK_MTD,
-  HT_DISK_PFLASH,
-  ])
-
-# Mouse types:
-HT_MOUSE_MOUSE = "mouse"
-HT_MOUSE_TABLET = "tablet"
-
-HT_KVM_VALID_MOUSE_TYPES = compat.UniqueFrozenset([
-  HT_MOUSE_MOUSE,
-  HT_MOUSE_TABLET,
-  ])
-
-# Boot order
-HT_BO_FLOPPY = "floppy"
-HT_BO_CDROM = "cdrom"
-HT_BO_DISK = "disk"
-HT_BO_NETWORK = "network"
-
-HT_KVM_VALID_BO_TYPES = compat.UniqueFrozenset([
-  HT_BO_FLOPPY,
-  HT_BO_CDROM,
-  HT_BO_DISK,
-  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 = compat.UniqueFrozenset([
-  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 = compat.UniqueFrozenset([
-  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 = compat.UniqueFrozenset([
-  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"
-HT_SM_POOL = "pool"
-
-HT_KVM_VALID_SM_TYPES = compat.UniqueFrozenset([
-  HT_SM_NONE,
-  HT_SM_USER,
-  HT_SM_POOL,
-  ])
-
-# Kvm flag values
-HT_KVM_ENABLED = "enabled"
-HT_KVM_DISABLED = "disabled"
-
-HT_KVM_FLAG_VALUES = compat.UniqueFrozenset([HT_KVM_ENABLED, HT_KVM_DISABLED])
-
-# Migration type
-HT_MIGRATION_LIVE = "live"
-HT_MIGRATION_NONLIVE = "non-live"
-HT_MIGRATION_MODES = compat.UniqueFrozenset([
-  HT_MIGRATION_LIVE,
-  HT_MIGRATION_NONLIVE,
-  ])
-
-# Cluster Verify steps
-VERIFY_NPLUSONE_MEM = "nplusone_mem"
-VERIFY_OPTIONAL_CHECKS = compat.UniqueFrozenset([VERIFY_NPLUSONE_MEM])
-
-# Cluster Verify error classes
-CV_TCLUSTER = "cluster"
-CV_TGROUP = "group"
-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_EGROUPDIFFERENTPVSIZE = \
-  (CV_TGROUP, "EGROUPDIFFERENTPVSIZE", "PVs in the group have different sizes")
-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_EINSTANCEUNSUITABLENODE = \
-  (CV_TINSTANCE, "EINSTANCEUNSUITABLENODE",
-   "Instance running on nodes that are not suitable for it")
-CV_EINSTANCEMISSINGCFGPARAMETER = \
-  (CV_TINSTANCE, "EINSTANCEMISSINGCFGPARAMETER",
-   "A configuration parameter for an instance is missing")
-CV_ENODEDRBD = \
-  (CV_TNODE, "ENODEDRBD", "Error parsing the DRBD status file")
-CV_ENODEDRBDVERSION = \
-  (CV_TNODE, "ENODEDRBDVERSION", "DRBD version mismatch within a node group")
-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_ENODEFILESTORAGEPATHS = \
-  (CV_TNODE, "ENODEFILESTORAGEPATHS", "Detected bad file storage paths")
-CV_ENODEFILESTORAGEPATHUNUSABLE = \
-  (CV_TNODE, "ENODEFILESTORAGEPATHUNUSABLE", "File storage path unusable")
-CV_ENODESHAREDFILESTORAGEPATHUNUSABLE = \
-  (CV_TNODE, "ENODESHAREDFILESTORAGEPATHUNUSABLE",
-      "Shared file storage path unusable")
-
-CV_ALL_ECODES = compat.UniqueFrozenset([
-  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_ENODEFILESTORAGEPATHS,
-  CV_ENODEFILESTORAGEPATHUNUSABLE,
-  CV_ENODESHAREDFILESTORAGEPATHUNUSABLE,
-  ])
-
-CV_ALL_ECODES_STRINGS = \
-  compat.UniqueFrozenset(estr for (_, estr, _) in CV_ALL_ECODES)
-
-# Node verify constants
-NV_BRIDGES = "bridges"
-NV_DRBDHELPER = "drbd-helper"
-NV_DRBDVERSION = "drbd-version"
-NV_DRBDLIST = "drbd-list"
-NV_EXCLUSIVEPVS = "exclusive-pvs"
-NV_FILELIST = "filelist"
-NV_ACCEPTED_STORAGE_PATHS = "allowed-file-storage-paths"
-NV_FILE_STORAGE_PATH = "file-storage-path"
-NV_SHARED_FILE_STORAGE_PATH = "shared-file-storage-path"
-NV_HVINFO = "hvinfo"
-NV_HVPARAMS = "hvparms"
-NV_HYPERVISOR = "hypervisor"
-NV_INSTANCELIST = "instancelist"
-NV_LVLIST = "lvlist"
-NV_MASTERIP = "master-ip"
-NV_NODELIST = "nodelist"
-NV_NODENETTEST = "node-net-test"
-NV_NODESETUP = "nodesetup"
-NV_OOB_PATHS = "oob-paths"
-NV_OSLIST = "oslist"
-NV_PVLIST = "pvlist"
-NV_TIME = "time"
-NV_USERSCRIPTS = "user-scripts"
-NV_VERSION = "version"
-NV_VGLIST = "vglist"
-NV_VMNODES = "vmnodes"
-
-# 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"
-INSTST_ERRORUP = "ERROR_up"
-INSTST_ERRORDOWN = "ERROR_down"
-INSTST_ALL = compat.UniqueFrozenset([
-  INSTST_RUNNING,
-  INSTST_ADMINDOWN,
-  INSTST_ADMINOFFLINE,
-  INSTST_NODEOFFLINE,
-  INSTST_NODEDOWN,
-  INSTST_WRONGNODE,
-  INSTST_ERRORUP,
-  INSTST_ERRORDOWN,
-  ])
-
-# Admin states
-ADMINST_UP = "up"
-ADMINST_DOWN = "down"
-ADMINST_OFFLINE = "offline"
-ADMINST_ALL = compat.UniqueFrozenset([
-  ADMINST_UP,
-  ADMINST_DOWN,
-  ADMINST_OFFLINE,
-  ])
-
-# Node roles
-NR_REGULAR = "R"
-NR_MASTER = "M"
-NR_MCANDIDATE = "C"
-NR_DRAINED = "D"
-NR_OFFLINE = "O"
-NR_ALL = compat.UniqueFrozenset([
-  NR_REGULAR,
-  NR_MASTER,
-  NR_MCANDIDATE,
-  NR_DRAINED,
-  NR_OFFLINE,
-  ])
-
-# SSL certificate check constants (in days)
-SSL_CERT_EXPIRATION_WARN = 30
-SSL_CERT_EXPIRATION_ERROR = 7
-
-# Allocator framework constants
-IALLOCATOR_VERSION = 2
-IALLOCATOR_DIR_IN = "in"
-IALLOCATOR_DIR_OUT = "out"
-VALID_IALLOCATOR_DIRECTIONS = compat.UniqueFrozenset([
-  IALLOCATOR_DIR_IN,
-  IALLOCATOR_DIR_OUT,
-  ])
-IALLOCATOR_MODE_ALLOC = "allocate"
-IALLOCATOR_MODE_RELOC = "relocate"
-IALLOCATOR_MODE_CHG_GROUP = "change-group"
-IALLOCATOR_MODE_NODE_EVAC = "node-evacuate"
-IALLOCATOR_MODE_MULTI_ALLOC = "multi-allocate"
-VALID_IALLOCATOR_MODES = compat.UniqueFrozenset([
-  IALLOCATOR_MODE_ALLOC,
-  IALLOCATOR_MODE_RELOC,
-  IALLOCATOR_MODE_CHG_GROUP,
-  IALLOCATOR_MODE_NODE_EVAC,
-  IALLOCATOR_MODE_MULTI_ALLOC,
-  ])
-IALLOCATOR_SEARCH_PATH = _autoconf.IALLOCATOR_SEARCH_PATH
-DEFAULT_IALLOCATOR_SHORTCUT = "."
-
-IALLOCATOR_NEVAC_PRI = "primary-only"
-IALLOCATOR_NEVAC_SEC = "secondary-only"
-IALLOCATOR_NEVAC_ALL = "all"
-IALLOCATOR_NEVAC_MODES = compat.UniqueFrozenset([
-  IALLOCATOR_NEVAC_PRI,
-  IALLOCATOR_NEVAC_SEC,
-  IALLOCATOR_NEVAC_ALL,
-  ])
-
-# Node evacuation
-NODE_EVAC_PRI = "primary-only"
-NODE_EVAC_SEC = "secondary-only"
-NODE_EVAC_ALL = "all"
-NODE_EVAC_MODES = compat.UniqueFrozenset([
-  NODE_EVAC_PRI,
-  NODE_EVAC_SEC,
-  NODE_EVAC_ALL,
-  ])
-
-# Job queue
-JOB_QUEUE_VERSION = 1
-JOB_QUEUE_SIZE_HARD_LIMIT = 5000
-JOB_QUEUE_FILES_PERMS = 0640
+BLOCKDEV_DRIVER_MANUAL = _constants.BLOCKDEV_DRIVER_MANUAL
 
-JOB_ID_TEMPLATE = r"\d+"
-JOB_FILE_RE = re.compile(r"^job-(%s)$" % JOB_ID_TEMPLATE)
+QEMUIMG_PATH = _constants.QEMUIMG_PATH
 
-# unchanged job return
-JOB_NOTCHANGED = "nochange"
-
-# Job status
-JOB_STATUS_QUEUED = "queued"
-JOB_STATUS_WAITING = "waiting"
-JOB_STATUS_CANCELING = "canceling"
-JOB_STATUS_RUNNING = "running"
-JOB_STATUS_CANCELED = "canceled"
-JOB_STATUS_SUCCESS = "success"
-JOB_STATUS_ERROR = "error"
-JOBS_PENDING = compat.UniqueFrozenset([
-  JOB_STATUS_QUEUED,
-  JOB_STATUS_WAITING,
-  JOB_STATUS_CANCELING,
-  ])
-JOBS_FINALIZED = compat.UniqueFrozenset([
-  JOB_STATUS_CANCELED,
-  JOB_STATUS_SUCCESS,
-  JOB_STATUS_ERROR,
-  ])
-JOB_STATUS_ALL = compat.UniqueFrozenset([
-  JOB_STATUS_RUNNING,
-  ]) | JOBS_PENDING | JOBS_FINALIZED
-
-# OpCode status
-# not yet finalized
-OP_STATUS_QUEUED = "queued"
-OP_STATUS_WAITING = "waiting"
-OP_STATUS_CANCELING = "canceling"
-OP_STATUS_RUNNING = "running"
-# finalized
-OP_STATUS_CANCELED = "canceled"
-OP_STATUS_SUCCESS = "success"
-OP_STATUS_ERROR = "error"
-OPS_FINALIZED = compat.UniqueFrozenset([
-  OP_STATUS_CANCELED,
-  OP_STATUS_SUCCESS,
-  OP_STATUS_ERROR,
-  ])
-
-# OpCode priority
-OP_PRIO_LOWEST = +19
-OP_PRIO_HIGHEST = -20
-
-OP_PRIO_LOW = +10
-OP_PRIO_NORMAL = 0
-OP_PRIO_HIGH = -10
-
-OP_PRIO_SUBMIT_VALID = compat.UniqueFrozenset([
-  OP_PRIO_LOW,
-  OP_PRIO_NORMAL,
-  OP_PRIO_HIGH,
-  ])
-
-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_REMOTE_IMPORT = "remote-import"
-ELOG_JQUEUE_TEST = "jqueue-test"
-
-# /etc/hosts modification
-ETC_HOSTS_ADD = "add"
-ETC_HOSTS_REMOVE = "remove"
-
-# Job queue test
-JQT_MSGPREFIX = "TESTMSG="
-JQT_EXPANDNAMES = "expandnames"
-JQT_EXEC = "exec"
-JQT_LOGMSG = "logmsg"
-JQT_STARTMSG = "startmsg"
-JQT_ALL = compat.UniqueFrozenset([
-  JQT_EXPANDNAMES,
-  JQT_EXEC,
-  JQT_LOGMSG,
-  JQT_STARTMSG,
-  ])
-
-# 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"
-QR_NETWORK = "network"
-QR_EXTSTORAGE = "extstorage"
-
-#: List of resources which can be queried using L{opcodes.OpQuery}
-QR_VIA_OP = compat.UniqueFrozenset([
-  QR_CLUSTER,
-  QR_INSTANCE,
-  QR_NODE,
-  QR_GROUP,
-  QR_OS,
-  QR_EXPORT,
-  QR_NETWORK,
-  QR_EXTSTORAGE,
-  ])
-
-#: 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
-QR_VIA_RAPI = QR_VIA_LUXI
-
-# Query field types
-QFT_UNKNOWN = "unknown"
-QFT_TEXT = "text"
-QFT_BOOL = "bool"
-QFT_NUMBER = "number"
-QFT_UNIT = "unit"
-QFT_TIMESTAMP = "timestamp"
-QFT_OTHER = "other"
-
-#: All query field types
-QFT_ALL = compat.UniqueFrozenset([
-  QFT_UNKNOWN,
-  QFT_TEXT,
-  QFT_BOOL,
-  QFT_NUMBER,
-  QFT_UNIT,
-  QFT_TIMESTAMP,
-  QFT_OTHER,
-  ])
-
-# Query result field status (don't change or reuse values as they're used by
-# clients)
-#: Normal field status
-RS_NORMAL = 0
-#: Unknown field
-RS_UNKNOWN = 1
-#: No data (e.g. RPC error), can be used instead of L{RS_OFFLINE}
-RS_NODATA = 2
-#: Value unavailable/unsupported for item; if this field is supported
-#: but we cannot get the data for the moment, RS_NODATA or
-#: RS_OFFLINE should be used
-RS_UNAVAIL = 3
-#: Resource marked offline
-RS_OFFLINE = 4
-
-RS_ALL = compat.UniqueFrozenset([
-  RS_NORMAL,
-  RS_UNKNOWN,
-  RS_NODATA,
-  RS_UNAVAIL,
-  RS_OFFLINE,
-  ])
-
-#: Dictionary with special field cases and their verbose/terse formatting
-RSS_DESCRIPTION = {
-  RS_UNKNOWN: ("(unknown)", "??"),
-  RS_NODATA: ("(nodata)", "?"),
-  RS_OFFLINE: ("(offline)", "*"),
-  RS_UNAVAIL: ("(unavail)", "-"),
-  }
+HTOOLS = _constants.HTOOLS
+IALLOC_HAIL = _constants.IALLOC_HAIL
 
-# max dynamic devices
-MAX_NICS = 8
-MAX_DISKS = 16
-
-# SSCONF file prefix
-SSCONF_FILEPREFIX = "ssconf_"
-# SSCONF keys
-SS_CLUSTER_NAME = "cluster_name"
-SS_CLUSTER_TAGS = "cluster_tags"
-SS_FILE_STORAGE_DIR = "file_storage_dir"
-SS_SHARED_FILE_STORAGE_DIR = "shared_file_storage_dir"
-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"
-SS_NODE_SECONDARY_IPS = "node_secondary_ips"
-SS_OFFLINE_NODES = "offline_nodes"
-SS_ONLINE_NODES = "online_nodes"
-SS_PRIMARY_IP_FAMILY = "primary_ip_family"
-SS_INSTANCE_LIST = "instance_list"
-SS_RELEASE_VERSION = "release_version"
-SS_HYPERVISOR_LIST = "hypervisor_list"
-SS_MAINTAIN_NODE_HEALTH = "maintain_node_health"
-SS_UID_POOL = "uid_pool"
-SS_NODEGROUPS = "nodegroups"
-SS_NETWORKS = "networks"
-
-# This is not a complete SSCONF key, but the prefix for the hypervisor keys
-SS_HVPARAMS_PREF = "hvparams_"
-
-# Hvparams keys:
-SS_HVPARAMS_XEN_PVM = SS_HVPARAMS_PREF + HT_XEN_PVM
-SS_HVPARAMS_XEN_FAKE = SS_HVPARAMS_PREF + HT_FAKE
-SS_HVPARAMS_XEN_HVM = SS_HVPARAMS_PREF + HT_XEN_HVM
-SS_HVPARAMS_XEN_KVM = SS_HVPARAMS_PREF + HT_KVM
-SS_HVPARAMS_XEN_CHROOT = SS_HVPARAMS_PREF + HT_CHROOT
-SS_HVPARAMS_XEN_LXC = SS_HVPARAMS_PREF + HT_LXC
-
-VALID_SS_HVPARAMS_KEYS = compat.UniqueFrozenset([
-  SS_HVPARAMS_XEN_PVM,
-  SS_HVPARAMS_XEN_FAKE,
-  SS_HVPARAMS_XEN_HVM,
-  SS_HVPARAMS_XEN_KVM,
-  SS_HVPARAMS_XEN_CHROOT,
-  SS_HVPARAMS_XEN_LXC,
-  ])
-
-SS_FILE_PERMS = 0444
-
-# cluster wide default parameters
-DEFAULT_ENABLED_HYPERVISOR = HT_XEN_PVM
-
-HVC_DEFAULTS = {
-  HT_XEN_PVM: {
-    HV_USE_BOOTLOADER: False,
-    HV_BOOTLOADER_PATH: XEN_BOOTLOADER,
-    HV_BOOTLOADER_ARGS: "",
-    HV_KERNEL_PATH: XEN_KERNEL,
-    HV_INITRD_PATH: "",
-    HV_ROOT_PATH: "/dev/xvda1",
-    HV_KERNEL_ARGS: "ro",
-    HV_MIGRATION_PORT: 8002,
-    HV_MIGRATION_MODE: HT_MIGRATION_LIVE,
-    HV_BLOCKDEV_PREFIX: "sd",
-    HV_REBOOT_BEHAVIOR: INSTANCE_REBOOT_ALLOWED,
-    HV_CPU_MASK: CPU_PINNING_ALL,
-    HV_CPU_CAP: 0,
-    HV_CPU_WEIGHT: 256,
-    HV_VIF_SCRIPT: "",
-    HV_XEN_CMD: XEN_CMD_XM,
-    },
-  HT_XEN_HVM: {
-    HV_BOOT_ORDER: "cd",
-    HV_CDROM_IMAGE_PATH: "",
-    HV_NIC_TYPE: HT_NIC_RTL8139,
-    HV_DISK_TYPE: HT_DISK_PARAVIRTUAL,
-    HV_VNC_BIND_ADDRESS: IP4_ADDRESS_ANY,
-    HV_VNC_PASSWORD_FILE: pathutils.VNC_PASSWORD_FILE,
-    HV_ACPI: True,
-    HV_PAE: True,
-    HV_KERNEL_PATH: "/usr/lib/xen/boot/hvmloader",
-    HV_DEVICE_MODEL: "/usr/lib/xen/bin/qemu-dm",
-    HV_MIGRATION_PORT: 8002,
-    HV_MIGRATION_MODE: HT_MIGRATION_NONLIVE,
-    HV_USE_LOCALTIME: False,
-    HV_BLOCKDEV_PREFIX: "hd",
-    HV_PASSTHROUGH: "",
-    HV_REBOOT_BEHAVIOR: INSTANCE_REBOOT_ALLOWED,
-    HV_CPU_MASK: CPU_PINNING_ALL,
-    HV_CPU_CAP: 0,
-    HV_CPU_WEIGHT: 256,
-    HV_VIF_TYPE: HT_HVM_VIF_IOEMU,
-    HV_VIF_SCRIPT: "",
-    HV_VIRIDIAN: False,
-    HV_XEN_CMD: XEN_CMD_XM,
-    },
-  HT_KVM: {
-    HV_KVM_PATH: KVM_PATH,
-    HV_KERNEL_PATH: KVM_KERNEL,
-    HV_INITRD_PATH: "",
-    HV_KERNEL_ARGS: "ro",
-    HV_ROOT_PATH: "/dev/vda1",
-    HV_ACPI: True,
-    HV_SERIAL_CONSOLE: True,
-    HV_SERIAL_SPEED: 38400,
-    HV_VNC_BIND_ADDRESS: "",
-    HV_VNC_TLS: False,
-    HV_VNC_X509: "",
-    HV_VNC_X509_VERIFY: False,
-    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: "",
-    HV_BOOT_ORDER: HT_BO_DISK,
-    HV_NIC_TYPE: HT_NIC_PARAVIRTUAL,
-    HV_DISK_TYPE: HT_DISK_PARAVIRTUAL,
-    HV_KVM_CDROM_DISK_TYPE: "",
-    HV_USB_MOUSE: "",
-    HV_KEYMAP: "",
-    HV_MIGRATION_PORT: 8102,
-    HV_MIGRATION_BANDWIDTH: 32, # MiB/s
-    HV_MIGRATION_DOWNTIME: 30,  # ms
-    HV_MIGRATION_MODE: HT_MIGRATION_LIVE,
-    HV_USE_LOCALTIME: False,
-    HV_DISK_CACHE: HT_CACHE_DEFAULT,
-    HV_SECURITY_MODEL: HT_SM_NONE,
-    HV_SECURITY_DOMAIN: "",
-    HV_KVM_FLAG: "",
-    HV_VHOST_NET: False,
-    HV_KVM_USE_CHROOT: False,
-    HV_MEM_PATH: "",
-    HV_REBOOT_BEHAVIOR: INSTANCE_REBOOT_ALLOWED,
-    HV_CPU_MASK: CPU_PINNING_ALL,
-    HV_CPU_TYPE: "",
-    HV_CPU_CORES: 0,
-    HV_CPU_THREADS: 0,
-    HV_CPU_SOCKETS: 0,
-    HV_SOUNDHW: "",
-    HV_USB_DEVICES: "",
-    HV_VGA: "",
-    HV_KVM_EXTRA: "",
-    HV_KVM_MACHINE_VERSION: "",
-    HV_VNET_HDR: True,
-    },
-  HT_FAKE: {
-    HV_MIGRATION_MODE: HT_MIGRATION_LIVE,
-  },
-  HT_CHROOT: {
-    HV_INIT_SCRIPT: "/ganeti-chroot",
-    },
-  HT_LXC: {
-    HV_CPU_MASK: "",
-    },
-  }
+FAKE_OP_MASTER_TURNUP = _constants.FAKE_OP_MASTER_TURNUP
+FAKE_OP_MASTER_TURNDOWN = _constants.FAKE_OP_MASTER_TURNDOWN
 
-HVC_GLOBALS = compat.UniqueFrozenset([
-  HV_MIGRATION_PORT,
-  HV_MIGRATION_BANDWIDTH,
-  HV_MIGRATION_MODE,
-  HV_XEN_CMD,
-  ])
-
-BEC_DEFAULTS = {
-  BE_MINMEM: 128,
-  BE_MAXMEM: 128,
-  BE_VCPUS: 1,
-  BE_AUTO_BALANCE: True,
-  BE_ALWAYS_FAILOVER: False,
-  BE_SPINDLE_USE: 1,
-  }
+SSHK_RSA = _constants.SSHK_RSA
+SSHK_DSA = _constants.SSHK_DSA
+SSHK_ALL = _constants.SSHK_ALL
 
-NDC_DEFAULTS = {
-  ND_OOB_PROGRAM: "",
-  ND_SPINDLE_COUNT: 1,
-  ND_EXCLUSIVE_STORAGE: False,
-  }
+SSHAK_RSA = _constants.SSHAK_RSA
+SSHAK_DSS = _constants.SSHAK_DSS
+SSHAK_ALL = _constants.SSHAK_ALL
 
-NDC_GLOBALS = compat.UniqueFrozenset([
-  ND_EXCLUSIVE_STORAGE,
-  ])
-
-DISK_LD_DEFAULTS = {
-  DT_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_PROTOCOL: DRBD_DEFAULT_NET_PROTOCOL,
-    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
-    },
-  DT_PLAIN: {
-    LDP_STRIPES: _autoconf.LVM_STRIPECOUNT
-    },
-  DT_FILE: {},
-  DT_SHARED_FILE: {},
-  DT_BLOCK: {},
-  DT_RBD: {
-    LDP_POOL: "rbd"
-    },
-  DT_EXT: {},
-  }
+SSHS_CLUSTER_NAME = _constants.SSHS_CLUSTER_NAME
+SSHS_SSH_HOST_KEY = _constants.SSHS_SSH_HOST_KEY
+SSHS_SSH_ROOT_KEY = _constants.SSHS_SSH_ROOT_KEY
+SSHS_NODE_DAEMON_CERTIFICATE = _constants.SSHS_NODE_DAEMON_CERTIFICATE
+SSH_DAEMON_KEYFILES = _constants.SSH_DAEMON_KEYFILES
 
-# readability shortcuts
-_LV_DEFAULTS = DISK_LD_DEFAULTS[DT_PLAIN]
-_DRBD_DEFAULTS = DISK_LD_DEFAULTS[DT_DRBD8]
-
-DISK_DT_DEFAULTS = {
-  DT_PLAIN: {
-    LV_STRIPES: DISK_LD_DEFAULTS[DT_PLAIN][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_PROTOCOL: _DRBD_DEFAULTS[LDP_PROTOCOL],
-    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[DT_RBD][LDP_POOL]
-    },
-  DT_EXT: {},
-  }
+NDS_CLUSTER_NAME = _constants.NDS_CLUSTER_NAME
+NDS_NODE_DAEMON_CERTIFICATE = _constants.NDS_NODE_DAEMON_CERTIFICATE
+NDS_SSCONF = _constants.NDS_SSCONF
+NDS_START_NODE_DAEMON = _constants.NDS_START_NODE_DAEMON
 
-# we don't want to export the shortcuts
-del _LV_DEFAULTS, _DRBD_DEFAULTS
+RANDOM_UUID_FILE = _constants.RANDOM_UUID_FILE
 
-NICC_DEFAULTS = {
-  NIC_MODE: NIC_MODE_BRIDGED,
-  NIC_LINK: DEFAULT_BRIDGE,
-  }
+# Regex string for verifying a UUID
+UUID_REGEX = "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$"
 
-# All of the following values are quite arbitrarily - there are no
-# "good" defaults, these must be customised per-site
-ISPECS_MINMAX_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,
-    },
-  }
-IPOLICY_DEFAULTS = {
-  ISPECS_MINMAX: [ISPECS_MINMAX_DEFAULTS],
-  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: list(DISK_TEMPLATES),
-  IPOLICY_VCPU_RATIO: 4.0,
-  IPOLICY_SPINDLE_RATIO: 32.0,
-  }
+AUTO_REPAIR_TAG_PREFIX = _constants.AUTO_REPAIR_TAG_PREFIX
+AUTO_REPAIR_TAG_ENABLED = _constants.AUTO_REPAIR_TAG_ENABLED
+AUTO_REPAIR_TAG_SUSPENDED = _constants.AUTO_REPAIR_TAG_SUSPENDED
+AUTO_REPAIR_TAG_PENDING = _constants.AUTO_REPAIR_TAG_PENDING
+AUTO_REPAIR_TAG_RESULT = _constants.AUTO_REPAIR_TAG_RESULT
 
-MASTER_POOL_SIZE_DEFAULT = 10
-
-# Exclusive storage:
-# Error margin used to compare physical disks
-PART_MARGIN = .01
-# Space reserved when creating instance disks
-PART_RESERVED = .02
-
-CONFD_PROTOCOL_VERSION = 1
-
-CONFD_REQ_PING = 0
-CONFD_REQ_NODE_ROLE_BYNAME = 1
-CONFD_REQ_NODE_PIP_BY_INSTANCE_IP = 2
-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_REQ_NODE_INSTANCES = 8
-
-# 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.
-CONFD_REQQ_LINK = "0"
-CONFD_REQQ_IP = "1"
-CONFD_REQQ_IPLIST = "2"
-CONFD_REQQ_FIELDS = "3"
-
-CONFD_REQFIELD_NAME = "0"
-CONFD_REQFIELD_IP = "1"
-CONFD_REQFIELD_MNODE_PIP = "2"
-
-CONFD_REQS = compat.UniqueFrozenset([
-  CONFD_REQ_PING,
-  CONFD_REQ_NODE_ROLE_BYNAME,
-  CONFD_REQ_NODE_PIP_BY_INSTANCE_IP,
-  CONFD_REQ_CLUSTER_MASTER,
-  CONFD_REQ_NODE_PIP_LIST,
-  CONFD_REQ_MC_PIP_LIST,
-  CONFD_REQ_INSTANCES_IPS_LIST,
-  CONFD_REQ_NODE_DRBD,
-  ])
-
-CONFD_REPL_STATUS_OK = 0
-CONFD_REPL_STATUS_ERROR = 1
-CONFD_REPL_STATUS_NOTIMPLEMENTED = 2
-
-CONFD_REPL_STATUSES = compat.UniqueFrozenset([
-  CONFD_REPL_STATUS_OK,
-  CONFD_REPL_STATUS_ERROR,
-  CONFD_REPL_STATUS_NOTIMPLEMENTED,
-  ])
-
-(CONFD_NODE_ROLE_MASTER,
- CONFD_NODE_ROLE_CANDIDATE,
- CONFD_NODE_ROLE_OFFLINE,
- CONFD_NODE_ROLE_DRAINED,
- CONFD_NODE_ROLE_REGULAR,
- ) = range(5)
-
-# A few common errors for confd
-CONFD_ERROR_UNKNOWN_ENTRY = 1
-CONFD_ERROR_INTERNAL = 2
-CONFD_ERROR_ARGUMENT = 3
-
-# Each request is "salted" by the current timestamp.
-# This constants decides how many seconds of skew to accept.
-# TODO: make this a default and allow the value to be more configurable
-CONFD_MAX_CLOCK_SKEW = 2 * NODE_MAX_CLOCK_SKEW
-
-# When we haven't reloaded the config for more than this amount of
-# seconds, we force a test to see if inotify is betraying us. Using a
-# prime number to ensure we get less chance of 'same wakeup' with
-# other processes.
-CONFD_CONFIG_RELOAD_TIMEOUT = 17
-
-# If we receive more than one update in this amount of microseconds,
-# we move to polling every RATELIMIT seconds, rather than relying on
-# inotify, to be able to serve more requests.
-CONFD_CONFIG_RELOAD_RATELIMIT = 250000
-
-# Magic number prepended to all confd queries.
-# This allows us to distinguish different types of confd protocols and handle
-# them. For example by changing this we can move the whole payload to be
-# compressed, or move away from json.
-CONFD_MAGIC_FOURCC = "plj0"
-
-# By default a confd request is sent to the minimum between this number and all
-# MCs. 6 was chosen because even in the case of a disastrous 50% response rate,
-# we should have enough answers to be able to compare more than one.
-CONFD_DEFAULT_REQ_COVERAGE = 6
-
-# Timeout in seconds to expire pending query request in the confd client
-# library. We don't actually expect any answer more than 10 seconds after we
-# sent a request.
-CONFD_CLIENT_EXPIRE_TIMEOUT = 10
-
-# Maximum UDP datagram size.
-# On IPv4: 64K - 20 (ip header size) - 8 (udp header size) = 65507
-# On IPv6: 64K - 40 (ip6 header size) - 8 (udp header size) = 65487
-#   (assuming we can't use jumbo frames)
-# We just set this to 60K, which should be enough
-MAX_UDP_DATA_SIZE = 61440
-
-# User-id pool minimum/maximum acceptable user-ids.
-UIDPOOL_UID_MIN = 0
-UIDPOOL_UID_MAX = 2 ** 32 - 1 # Assuming 32 bit user-ids
-
-# Name or path of the pgrep command
-PGREP = "pgrep"
-
-# Name of the node group that gets created at cluster init or upgrade
-INITIAL_NODE_GROUP_NAME = "default"
-
-# Possible values for NodeGroup.alloc_policy
-ALLOC_POLICY_PREFERRED = "preferred"
-ALLOC_POLICY_LAST_RESORT = "last_resort"
-ALLOC_POLICY_UNALLOCABLE = "unallocable"
-VALID_ALLOC_POLICIES = [
-  ALLOC_POLICY_PREFERRED,
-  ALLOC_POLICY_LAST_RESORT,
-  ALLOC_POLICY_UNALLOCABLE,
-  ]
-
-# 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"
-
-# SSH key types
-SSHK_RSA = "rsa"
-SSHK_DSA = "dsa"
-SSHK_ALL = compat.UniqueFrozenset([SSHK_RSA, SSHK_DSA])
-
-# SSH authorized key types
-SSHAK_RSA = "ssh-rsa"
-SSHAK_DSS = "ssh-dss"
-SSHAK_ALL = compat.UniqueFrozenset([SSHAK_RSA, SSHAK_DSS])
-
-# SSH setup
-SSHS_CLUSTER_NAME = "cluster_name"
-SSHS_SSH_HOST_KEY = "ssh_host_key"
-SSHS_SSH_ROOT_KEY = "ssh_root_key"
-SSHS_NODE_DAEMON_CERTIFICATE = "node_daemon_certificate"
-
-#: Key files for SSH daemon
-SSH_DAEMON_KEYFILES = {
-  SSHK_RSA: (pathutils.SSH_HOST_RSA_PRIV, pathutils.SSH_HOST_RSA_PUB),
-  SSHK_DSA: (pathutils.SSH_HOST_DSA_PRIV, pathutils.SSH_HOST_DSA_PUB),
-  }
+AUTO_REPAIR_FIX_STORAGE = _constants.AUTO_REPAIR_FIX_STORAGE
+AUTO_REPAIR_MIGRATE = _constants.AUTO_REPAIR_MIGRATE
+AUTO_REPAIR_FAILOVER = _constants.AUTO_REPAIR_FAILOVER
+AUTO_REPAIR_REINSTALL = _constants.AUTO_REPAIR_REINSTALL
+AUTO_REPAIR_ALL_TYPES = _constants.AUTO_REPAIR_ALL_TYPES
 
-# Node daemon setup
-NDS_CLUSTER_NAME = "cluster_name"
-NDS_NODE_DAEMON_CERTIFICATE = "node_daemon_certificate"
-NDS_SSCONF = "ssconf"
-NDS_START_NODE_DAEMON = "start_node_daemon"
+AUTO_REPAIR_SUCCESS = _constants.AUTO_REPAIR_SUCCESS
+AUTO_REPAIR_FAILURE = _constants.AUTO_REPAIR_FAILURE
+AUTO_REPAIR_ENOPERM = _constants.AUTO_REPAIR_ENOPERM
+AUTO_REPAIR_ALL_RESULTS = _constants.AUTO_REPAIR_ALL_RESULTS
 
-# Path generating random UUID
-RANDOM_UUID_FILE = "/proc/sys/kernel/random/uuid"
+BUILTIN_DATA_COLLECTOR_VERSION = _constants.BUILTIN_DATA_COLLECTOR_VERSION
 
-# Regex string for verifying a UUID
-UUID_REGEX = "^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$"
+OPCODE_REASON = _constants.OPCODE_REASON
+
+OPCODE_REASON_SRC_CLIENT = _constants.OPCODE_REASON_SRC_CLIENT
+OPCODE_REASON_SRC_NODED = _constants.OPCODE_REASON_SRC_NODED
+OPCODE_REASON_SRC_OPCODE = _constants.OPCODE_REASON_SRC_OPCODE
+OPCODE_REASON_SRC_RLIB2 = _constants.OPCODE_REASON_SRC_RLIB2
+OPCODE_REASON_SRC_USER = _constants.OPCODE_REASON_SRC_USER
+OPCODE_REASON_SOURCES = _constants.OPCODE_REASON_SOURCES
+
+DISKSTATS_FILE = _constants.DISKSTATS_FILE
+
+STAT_FILE = _constants.STAT_FILE
+CPUAVGLOAD_BUFFER_SIZE = _constants.CPUAVGLOAD_BUFFER_SIZE
+CPUAVGLOAD_WINDOW_SIZE = _constants.CPUAVGLOAD_WINDOW_SIZE
+
+MOND_TIME_INTERVAL = _constants.MOND_TIME_INTERVAL
+MOND_LATEST_API_VERSION = _constants.MOND_LATEST_API_VERSION
 
-# Auto-repair tag prefixes
-AUTO_REPAIR_TAG_PREFIX = "ganeti:watcher:autorepair:"
-AUTO_REPAIR_TAG_ENABLED = AUTO_REPAIR_TAG_PREFIX
-AUTO_REPAIR_TAG_SUSPENDED = AUTO_REPAIR_TAG_ENABLED + "suspend:"
-AUTO_REPAIR_TAG_PENDING = AUTO_REPAIR_TAG_PREFIX + "pending:"
-AUTO_REPAIR_TAG_RESULT = AUTO_REPAIR_TAG_PREFIX + "result:"
-
-# Auto-repair levels
-AUTO_REPAIR_FIX_STORAGE = "fix-storage"
-AUTO_REPAIR_MIGRATE = "migrate"
-AUTO_REPAIR_FAILOVER = "failover"
-AUTO_REPAIR_REINSTALL = "reinstall"
-AUTO_REPAIR_ALL_TYPES = [
-  AUTO_REPAIR_FIX_STORAGE,
-  AUTO_REPAIR_MIGRATE,
-  AUTO_REPAIR_FAILOVER,
-  AUTO_REPAIR_REINSTALL,
-]
-
-# Auto-repair results
-AUTO_REPAIR_SUCCESS = "success"
-AUTO_REPAIR_FAILURE = "failure"
-AUTO_REPAIR_ENOPERM = "enoperm"
-AUTO_REPAIR_ALL_RESULTS = frozenset([
-    AUTO_REPAIR_SUCCESS,
-    AUTO_REPAIR_FAILURE,
-    AUTO_REPAIR_ENOPERM,
-])
-
-# The version identifier for builtin data collectors
-BUILTIN_DATA_COLLECTOR_VERSION = "B"
-
-# The reason trail opcode parameter name
-OPCODE_REASON = "reason"
-
-# The source reasons for the execution of an OpCode
-OPCODE_REASON_SRC_CLIENT = "gnt:client"
-OPCODE_REASON_SRC_NODED = "gnt:daemon:noded"
-OPCODE_REASON_SRC_OPCODE = "gnt:opcode"
-OPCODE_REASON_SRC_RLIB2 = "gnt:library:rlib2"
-OPCODE_REASON_SRC_USER = "gnt:user"
-
-OPCODE_REASON_SOURCES = compat.UniqueFrozenset([
-  OPCODE_REASON_SRC_CLIENT,
-  OPCODE_REASON_SRC_NODED,
-  OPCODE_REASON_SRC_OPCODE,
-  OPCODE_REASON_SRC_RLIB2,
-  OPCODE_REASON_SRC_USER,
-  ])
-
-DISKSTATS_FILE = "/proc/diskstats"
+# Timeouts for upgrades
+
+UPGRADE_QUEUE_DRAIN_TIMEOUT = _constants.UPGRADE_QUEUE_DRAIN_TIMEOUT
+UPGRADE_QUEUE_POLL_INTERVAL = _constants.UPGRADE_QUEUE_POLL_INTERVAL
+
+# device types to hotplug
+HOTPLUG_TARGET_DISK = _constants.HOTPLUG_TARGET_DISK
+HOTPLUG_TARGET_NIC = _constants.HOTPLUG_TARGET_NIC
+HOTPLUG_ALL_TARGETS = _constants.HOTPLUG_ALL_TARGETS
+
+# hotplug actions
+HOTPLUG_ACTION_ADD = _constants.HOTPLUG_ACTION_ADD
+HOTPLUG_ACTION_REMOVE = _constants.HOTPLUG_ACTION_REMOVE
+HOTPLUG_ACTION_MODIFY = _constants.HOTPLUG_ACTION_MODIFY
+HOTPLUG_ALL_ACTIONS = _constants.HOTPLUG_ALL_ACTIONS
+
+# disk removal timeouts
+DISK_REMOVE_RETRY_INTERVAL = _constants.DISK_REMOVE_RETRY_INTERVAL
+DISK_REMOVE_RETRY_TIMEOUT = _constants.DISK_REMOVE_RETRY_TIMEOUT
+
+# other constants
+
+HAS_GNU_LN = _constants.HAS_GNU_LN
 
 # Do not re-export imported modules
-del re, _vcsversion, _autoconf, socket, pathutils, compat
+del re, _vcsversion, _constants, socket, pathutils, compat
+
+
+ALLOCATABLE_KEY = "allocatable"
+FAILED_KEY = "failed"
index 22b7502..f205f4a 100644 (file)
@@ -102,6 +102,15 @@ class HypervisorError(GenericError):
   """
 
 
+class HotplugError(HypervisorError):
+  """Hotplug-related exception.
+
+  This is raised in case a hotplug action fails or is not supported.
+  It is currently used only by KVM hypervisor.
+
+  """
+
+
 class ProgrammerError(GenericError):
   """Programming-related error.
 
index a452239..d6171ac 100644 (file)
--- a/lib/ht.py
+++ b/lib/ht.py
 
 import re
 import operator
+import ipaddr
 
 from ganeti import compat
 from ganeti import utils
 from ganeti import constants
+from ganeti import objects
 
 
 _PAREN_RE = re.compile("^[a-zA-Z0-9_-]+$")
@@ -158,10 +160,6 @@ def EmptyDict():
 NoDefault = object()
 
 
-#: The no-type (value too complex to check it in the type system)
-NoType = object()
-
-
 # Some basic types
 @WithDesc("Anything")
 def TAny(_):
@@ -359,13 +357,24 @@ TMaybeDict = TMaybe(TDict)
 #: Maybe a list (list or None)
 TMaybeList = TMaybe(TList)
 
+
+#: a non-negative number (value > 0)
+# val_type should be TInt, TDouble (== TFloat), or TNumber
+def TNonNegative(val_type):
+  return WithDesc("EqualOrGreaterThanZero")(TAnd(val_type, lambda v: v >= 0))
+
+
+#: a positive number (value >= 0)
+# val_type should be TInt, TDouble (== TFloat), or TNumber
+def TPositive(val_type):
+  return WithDesc("GreaterThanZero")(TAnd(val_type, lambda v: v > 0))
+
+
 #: a non-negative integer (value >= 0)
-TNonNegativeInt = \
-  TAnd(TInt, WithDesc("EqualOrGreaterThanZero")(lambda v: v >= 0))
+TNonNegativeInt = TNonNegative(TInt)
 
 #: a positive integer (value > 0)
-TPositiveInt = \
-  TAnd(TInt, WithDesc("GreaterThanZero")(lambda v: v > 0))
+TPositiveInt = TPositive(TInt)
 
 #: a maybe positive integer (positive integer or None)
 TMaybePositiveInt = TMaybe(TPositiveInt)
@@ -383,6 +392,9 @@ TJobId = WithDesc("JobId")(TOr(TNonNegativeInt,
                                TRegex(re.compile("^%s$" %
                                                  constants.JOB_ID_TEMPLATE))))
 
+#: Double (== Float)
+TDouble = TFloat
+
 #: Number
 TNumber = TOr(TInt, TFloat)
 
@@ -415,6 +427,24 @@ def TListOf(my_type):
 TMaybeListOf = lambda item_type: TMaybe(TListOf(item_type))
 
 
+def TTupleOf(*val_types):
+  """Checks if a given value is a list with the proper size and its
+     elements match the given types.
+
+  """
+  desc = WithDesc("Tuple of %s" % (Parens(val_types), ))
+  return desc(TAnd(TIsLength(len(val_types)), TItems(val_types)))
+
+
+def TSetOf(val_type):
+  """Checks if a given value is a list with all elements of the same
+     type and eliminates duplicated elements.
+
+  """
+  desc = WithDesc("Set of %s" % (Parens(val_type), ))
+  return desc(lambda st: TListOf(val_type)(list(set(st))))
+
+
 def TDictOf(key_type, val_type):
   """Checks a dict type for the type of its key/values.
 
@@ -497,3 +527,197 @@ def TItems(items):
 
   return desc(lambda value: compat.all(check(i)
                                        for (check, i) in zip(items, value)))
+
+
+TAllocPolicy = TElemOf(constants.VALID_ALLOC_POLICIES)
+TCVErrorCode = TElemOf(constants.CV_ALL_ECODES_STRINGS)
+TQueryResultCode = TElemOf(constants.RS_ALL)
+TExportTarget = TOr(TNonEmptyString, TList)
+TExportMode = TElemOf(constants.EXPORT_MODES)
+TDiskIndex = TAnd(TNonNegativeInt, lambda val: val < constants.MAX_DISKS)
+TReplaceDisksMode = TElemOf(constants.REPLACE_MODES)
+TDiskTemplate = TElemOf(constants.DISK_TEMPLATES)
+TEvacMode = TElemOf(constants.NODE_EVAC_MODES)
+TIAllocatorTestDir = TElemOf(constants.VALID_IALLOCATOR_DIRECTIONS)
+TIAllocatorMode = TElemOf(constants.VALID_IALLOCATOR_MODES)
+
+
+def TSetParamsMods(fn):
+  """Generates a check for modification lists.
+
+  """
+  # Old format
+  # TODO: Remove in version 2.11 including support in LUInstanceSetParams
+  old_mod_item_fn = \
+    TAnd(TIsLength(2),
+         TItems([TOr(TElemOf(constants.DDMS_VALUES), TNonNegativeInt), fn]))
+
+  # New format, supporting adding/removing disks/NICs at arbitrary indices
+  mod_item_fn = \
+      TAnd(TIsLength(3), TItems([
+        TElemOf(constants.DDMS_VALUES_WITH_MODIFY),
+        Comment("Device index, can be negative, e.g. -1 for last disk")
+                 (TOr(TInt, TString)),
+        fn,
+        ]))
+
+  return TOr(Comment("Recommended")(TListOf(mod_item_fn)),
+             Comment("Deprecated")(TListOf(old_mod_item_fn)))
+
+
+TINicParams = \
+    Comment("NIC parameters")(TDictOf(TElemOf(constants.INIC_PARAMS),
+                                      TMaybe(TString)))
+
+TIDiskParams = \
+    Comment("Disk parameters")(TDictOf(TElemOf(constants.IDISK_PARAMS),
+                                       TOr(TNonEmptyString, TInt)))
+
+THypervisor = TElemOf(constants.HYPER_TYPES)
+TMigrationMode = TElemOf(constants.HT_MIGRATION_MODES)
+TNICMode = TElemOf(constants.NIC_VALID_MODES)
+TInstCreateMode = TElemOf(constants.INSTANCE_CREATE_MODES)
+TRebootType = TElemOf(constants.REBOOT_TYPES)
+TFileDriver = TElemOf(constants.FILE_DRIVER)
+TOobCommand = TElemOf(constants.OOB_COMMANDS)
+TQueryTypeOp = TElemOf(constants.QR_VIA_OP)
+
+TDiskParams = \
+    Comment("Disk parameters")(TDictOf(TNonEmptyString,
+                                       TOr(TNonEmptyString, TInt)))
+
+TDiskChanges = \
+    TAnd(TIsLength(2),
+         TItems([Comment("Disk index")(TNonNegativeInt),
+                 Comment("Parameters")(TDiskParams)]))
+
+TRecreateDisksInfo = TOr(TListOf(TNonNegativeInt), TListOf(TDiskChanges))
+
+
+def TStorageType(val):
+  """Builds a function that checks if a given value is a valid storage
+  type.
+
+  """
+  return (val in constants.STORAGE_TYPES)
+
+
+TTagKind = TElemOf(constants.VALID_TAG_TYPES)
+TDdmSimple = TElemOf(constants.DDMS_VALUES)
+TVerifyOptionalChecks = TElemOf(constants.VERIFY_OPTIONAL_CHECKS)
+
+
+@WithDesc("IPv4 network")
+def _CheckCIDRNetNotation(value):
+  """Ensure a given CIDR notation type is valid.
+
+  """
+  try:
+    ipaddr.IPv4Network(value)
+  except ipaddr.AddressValueError:
+    return False
+  return True
+
+
+@WithDesc("IPv4 address")
+def _CheckCIDRAddrNotation(value):
+  """Ensure a given CIDR notation type is valid.
+
+  """
+  try:
+    ipaddr.IPv4Address(value)
+  except ipaddr.AddressValueError:
+    return False
+  return True
+
+
+@WithDesc("IPv6 address")
+def _CheckCIDR6AddrNotation(value):
+  """Ensure a given CIDR notation type is valid.
+
+  """
+  try:
+    ipaddr.IPv6Address(value)
+  except ipaddr.AddressValueError:
+    return False
+  return True
+
+
+@WithDesc("IPv6 network")
+def _CheckCIDR6NetNotation(value):
+  """Ensure a given CIDR notation type is valid.
+
+  """
+  try:
+    ipaddr.IPv6Network(value)
+  except ipaddr.AddressValueError:
+    return False
+  return True
+
+
+TIPv4Address = TAnd(TString, _CheckCIDRAddrNotation)
+TIPv6Address = TAnd(TString, _CheckCIDR6AddrNotation)
+TIPv4Network = TAnd(TString, _CheckCIDRNetNotation)
+TIPv6Network = TAnd(TString, _CheckCIDR6NetNotation)
+
+
+def TObject(val_type):
+  return TDictOf(TAny, val_type)
+
+
+def TObjectCheck(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 TStrictDict(True, True, fields_types)
+
+
+TQueryFieldDef = \
+    TObjectCheck(objects.QueryFieldDefinition, {
+        "name": TNonEmptyString,
+        "title": TNonEmptyString,
+        "kind": TElemOf(constants.QFT_ALL),
+        "doc": TNonEmptyString
+    })
+
+TQueryRow = \
+    TListOf(TAnd(TIsLength(2),
+                 TItems([TElemOf(constants.RS_ALL), TAny])))
+
+TQueryResult = TListOf(TQueryRow)
+
+TQueryResponse = \
+    TObjectCheck(objects.QueryResponse, {
+        "fields": TListOf(TQueryFieldDef),
+        "data": TQueryResult
+    })
+
+TQueryFieldsResponse = \
+    TObjectCheck(objects.QueryFieldsResponse, {
+        "fields": TListOf(TQueryFieldDef)
+    })
+
+TJobIdListItem = \
+    TAnd(TIsLength(2),
+         TItems([Comment("success")(TBool),
+                 Comment("Job ID if successful, error message"
+                         " otherwise")(TOr(TString, TJobId))]))
+
+TJobIdList = TListOf(TJobIdListItem)
+
+TJobIdListOnly = TStrictDict(True, True, {
+  constants.JOB_IDS_KEY: Comment("List of submitted jobs")(TJobIdList)
+  })
+
+TInstanceMultiAllocResponse = \
+    TStrictDict(True, True, {
+      constants.JOB_IDS_KEY: Comment("List of submitted jobs")(TJobIdList),
+      constants.ALLOCATABLE_KEY: TListOf(TNonEmptyString),
+      constants.FAILED_KEY: TListOf(TNonEmptyString)
+    })
index 54764ab..d124eeb 100644 (file)
@@ -563,3 +563,42 @@ class BaseHypervisor(object):
       return "; ".join(msgs)
     else:
       return None
+
+  # pylint: disable=R0201,W0613
+  def HotAddDevice(self, instance, dev_type, device, extra, seq):
+    """Hot-add a device.
+
+    """
+    raise errors.HotplugError("Hotplug is not supported by this hypervisor")
+
+  # pylint: disable=R0201,W0613
+  def HotDelDevice(self, instance, dev_type, device, extra, seq):
+    """Hot-del a device.
+
+    """
+    raise errors.HotplugError("Hotplug is not supported by this hypervisor")
+
+  # pylint: disable=R0201,W0613
+  def HotModDevice(self, instance, dev_type, device, extra, seq):
+    """Hot-mod a device.
+
+    """
+    raise errors.HotplugError("Hotplug is not supported by this hypervisor")
+
+  # pylint: disable=R0201,W0613
+  def VerifyHotplugSupport(self, instance, action, dev_type):
+    """Verifies that hotplug is supported.
+
+    Hotplug is not supported by default. If a hypervisor wants to support
+    it it should override this method.
+
+    @type instance: L{objects.Instance}
+    @param instance: the instance object
+    @type action: string
+    @param action: one of the supported hotplug commands
+    @type dev_type: string
+    @param dev_type: one of the supported device types to hotplug
+    @raise errors.HotplugError: if hotplugging is not supported
+
+    """
+    raise errors.HotplugError("Hotplug is not supported by this hypervisor")
index 91a3b35..d499222 100644 (file)
@@ -37,10 +37,15 @@ import shutil
 import socket
 import stat
 import StringIO
+from bitarray import bitarray
 try:
   import affinity   # pylint: disable=F0401
 except ImportError:
   affinity = None
+try:
+  import fdsend   # pylint: disable=F0401
+except ImportError:
+  fdsend = None
 
 from ganeti import utils
 from ganeti import constants
@@ -79,6 +84,132 @@ _SPICE_ADDITIONAL_PARAMS = frozenset([
   constants.HV_KVM_SPICE_USE_TLS,
   ])
 
+# Constant bitarray that reflects to a free pci slot
+# Use it with bitarray.search()
+_AVAILABLE_PCI_SLOT = bitarray("0")
+
+# below constants show the format of runtime file
+# the nics are in second possition, while the disks in 4th (last)
+# moreover disk entries are stored as a list of in tuples
+# (L{objects.Disk}, link_name, uri)
+_KVM_NICS_RUNTIME_INDEX = 1
+_KVM_DISKS_RUNTIME_INDEX = 3
+_DEVICE_RUNTIME_INDEX = {
+  constants.HOTPLUG_TARGET_DISK: _KVM_DISKS_RUNTIME_INDEX,
+  constants.HOTPLUG_TARGET_NIC: _KVM_NICS_RUNTIME_INDEX
+  }
+_FIND_RUNTIME_ENTRY = {
+  constants.HOTPLUG_TARGET_NIC:
+    lambda nic, kvm_nics: [n for n in kvm_nics if n.uuid == nic.uuid],
+  constants.HOTPLUG_TARGET_DISK:
+    lambda disk, kvm_disks: [(d, l, u) for (d, l, u) in kvm_disks
+                             if d.uuid == disk.uuid]
+  }
+_RUNTIME_DEVICE = {
+  constants.HOTPLUG_TARGET_NIC: lambda d: d,
+  constants.HOTPLUG_TARGET_DISK: lambda (d, e, _): d
+  }
+_RUNTIME_ENTRY = {
+  constants.HOTPLUG_TARGET_NIC: lambda d, e: d,
+  constants.HOTPLUG_TARGET_DISK: lambda d, e: (d, e, None)
+  }
+
+
+def _GenerateDeviceKVMId(dev_type, dev):
+  """Helper function to generate a unique device name used by KVM
+
+  QEMU monitor commands use names to identify devices. Here we use their pci
+  slot and a part of their UUID to name them. dev.pci might be None for old
+  devices in the cluster.
+
+  @type dev_type: sting
+  @param dev_type: device type of param dev
+  @type dev: L{objects.Disk} or L{objects.NIC}
+  @param dev: the device object for which we generate a kvm name
+  @raise errors.HotplugError: in case a device has no pci slot (old devices)
+
+  """
+
+  if not dev.pci:
+    raise errors.HotplugError("Hotplug is not supported for %s with UUID %s" %
+                              (dev_type, dev.uuid))
+
+  return "%s-%s-pci-%d" % (dev_type.lower(), dev.uuid.split("-")[0], dev.pci)
+
+
+def _UpdatePCISlots(dev, pci_reservations):
+  """Update pci configuration for a stopped instance
+
+  If dev has a pci slot then reserve it, else find first available
+  in pci_reservations bitarray. It acts on the same objects passed
+  as params so there is no need to return anything.
+
+  @type dev: L{objects.Disk} or L{objects.NIC}
+  @param dev: the device object for which we update its pci slot
+  @type pci_reservations: bitarray
+  @param pci_reservations: existing pci reservations for an instance
+  @raise errors.HotplugError: in case an instance has all its slot occupied
+
+  """
+  if dev.pci:
+    free = dev.pci
+  else: # pylint: disable=E1103
+    [free] = pci_reservations.search(_AVAILABLE_PCI_SLOT, 1)
+    if not free:
+      raise errors.HypervisorError("All PCI slots occupied")
+    dev.pci = int(free)
+
+  pci_reservations[free] = True
+
+
+def _GetExistingDeviceInfo(dev_type, device, runtime):
+  """Helper function to get an existing device inside the runtime file
+
+  Used when an instance is running. Load kvm runtime file and search
+  for a device based on its type and uuid.
+
+  @type dev_type: sting
+  @param dev_type: device type of param dev
+  @type device: L{objects.Disk} or L{objects.NIC}
+  @param device: the device object for which we generate a kvm name
+  @type runtime: tuple (cmd, nics, hvparams, disks)
+  @param runtime: the runtime data to search for the device
+  @raise errors.HotplugError: in case the requested device does not
+    exist (e.g. device has been added without --hotplug option) or
+    device info has not pci slot (e.g. old devices in the cluster)
+
+  """
+  index = _DEVICE_RUNTIME_INDEX[dev_type]
+  found = _FIND_RUNTIME_ENTRY[dev_type](device, runtime[index])
+  if not found:
+    raise errors.HotplugError("Cannot find runtime info for %s with UUID %s" %
+                              (dev_type, device.uuid))
+
+  return found[0]
+
+
+def _AnalyzeSerializedRuntime(serialized_runtime):
+  """Return runtime entries for a serialized runtime file
+
+  @type serialized_runtime: string
+  @param serialized_runtime: raw text data read from actual runtime file
+  @return: (cmd, nics, hvparams, bdevs)
+  @rtype: list
+
+  """
+  loaded_runtime = serializer.Load(serialized_runtime)
+  if len(loaded_runtime) == 3:
+    serialized_disks = []
+    kvm_cmd, serialized_nics, hvparams = loaded_runtime
+  else:
+    kvm_cmd, serialized_nics, hvparams, serialized_disks = loaded_runtime
+
+  kvm_nics = [objects.NIC.FromDict(snic) for snic in serialized_nics]
+  kvm_disks = [(objects.Disk.FromDict(sdisk), link, uri)
+               for sdisk, link, uri in serialized_disks]
+
+  return (kvm_cmd, kvm_nics, hvparams, kvm_disks)
+
 
 def _GetTunFeatures(fd, _ioctl=fcntl.ioctl):
   """Retrieves supported TUN features from file descriptor.
@@ -230,30 +361,15 @@ class QmpMessage:
     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"
+class MonitorSocket(object):
   _SOCKET_TIMEOUT = 5
 
   def __init__(self, monitor_filename):
-    """Instantiates the QmpConnection object.
+    """Instantiates the MonitorSocket object.
 
     @type monitor_filename: string
     @param monitor_filename: the filename of the UNIX raw socket on which the
-                             QMP monitor is listening
+                             monitor (QMP or simple one) is listening
 
     """
     self.monitor_filename = monitor_filename
@@ -262,7 +378,6 @@ class QmpConnection:
     # in a reasonable amount of time
     self.sock.settimeout(self._SOCKET_TIMEOUT)
     self._connected = False
-    self._buf = ""
 
   def _check_socket(self):
     sock_stat = None
@@ -270,29 +385,27 @@ class QmpConnection:
       sock_stat = os.stat(self.monitor_filename)
     except EnvironmentError, err:
       if err.errno == errno.ENOENT:
-        raise errors.HypervisorError("No qmp socket found")
+        raise errors.HypervisorError("No monitor socket found")
       else:
-        raise errors.HypervisorError("Error checking qmp socket: %s",
+        raise errors.HypervisorError("Error checking monitor socket: %s",
                                      utils.ErrnoOrStr(err))
     if not stat.S_ISSOCK(sock_stat.st_mode):
-      raise errors.HypervisorError("Qmp socket is not a socket")
+      raise errors.HypervisorError("Monitor 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"
+      raise errors.ProgrammerError("To use a MonitorSocket you need to first"
                                    " invoke connect() on it")
 
   def connect(self):
-    """Connects to the QMP monitor.
+    """Connects to the monitor.
 
-    Connects to the UNIX socket and makes sure that we can actually send and
-    receive data to the kvm instance via QMP.
+    Connects to the UNIX socket
 
     @raise errors.HypervisorError: when there are communication errors
-    @raise errors.ProgrammerError: when there are data serialization errors
 
     """
     if self._connected:
@@ -307,6 +420,47 @@ class QmpConnection:
       raise errors.HypervisorError("Can't connect to qmp socket")
     self._connected = True
 
+  def close(self):
+    """Closes the socket
+
+    It cannot be used after this call.
+
+    """
+    self.sock.close()
+
+
+class QmpConnection(MonitorSocket):
+  """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"
+
+  def __init__(self, monitor_filename):
+    super(QmpConnection, self).__init__(monitor_filename)
+    self._buf = ""
+
+  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
+
+    """
+    super(QmpConnection, self).connect()
     # 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()
@@ -546,6 +700,7 @@ class KVMHypervisor(hv_base.BaseHypervisor):
 
   _VIRTIO = "virtio"
   _VIRTIO_NET_PCI = "virtio-net-pci"
+  _VIRTIO_BLK_PCI = "virtio-blk-pci"
 
   _MIGRATION_STATUS_RE = re.compile(r"Migration\s+status:\s+(\w+)",
                                     re.M | re.I)
@@ -575,13 +730,22 @@ class KVMHypervisor(hv_base.BaseHypervisor):
   _NETDEV_RE = re.compile(r"^-netdev\s", re.M)
   _DISPLAY_RE = re.compile(r"^-display\s", re.M)
   _MACHINE_RE = re.compile(r"^-machine\s", re.M)
-  _NEW_VIRTIO_RE = re.compile(r"^name \"%s\"" % _VIRTIO_NET_PCI, re.M)
+  _VIRTIO_NET_RE = re.compile(r"^name \"%s\"" % _VIRTIO_NET_PCI, re.M)
+  _VIRTIO_BLK_RE = re.compile(r"^name \"%s\"" % _VIRTIO_BLK_PCI, re.M)
   # match  -drive.*boot=on|off on different lines, but in between accept only
   # dashes not preceeded by a new line (which would mean another option
   # different than -drive is starting)
   _BOOT_RE = re.compile(r"^-drive\s([^-]|(?<!^)-)*,boot=on\|off", re.M | re.S)
   _UUID_RE = re.compile(r"^-uuid\s", re.M)
 
+  _INFO_PCI_RE = re.compile(r'Bus.*device[ ]*(\d+).*')
+  _INFO_PCI_CMD = "info pci"
+  _INFO_VERSION_RE = \
+    re.compile(r'^QEMU (\d+)\.(\d+)(\.(\d+))?.*monitor.*', re.M)
+  _INFO_VERSION_CMD = "info version"
+
+  _DEFAULT_PCI_RESERVATIONS = "11110000000000000000000000000000"
+
   ANCILLARY_FILES = [
     _KVM_NETWORK_SCRIPT,
     ]
@@ -1034,6 +1198,98 @@ class KVMHypervisor(hv_base.BaseHypervisor):
         data.append(info)
     return data
 
+  def _GenerateKVMBlockDevicesOptions(self, instance, kvm_disks,
+                                      kvmhelp, devlist):
+    """Generate KVM options regarding instance's block devices.
+
+    @type instance: L{objects.Instance}
+    @param instance: the instance object
+    @type kvm_disks: list of tuples
+    @param kvm_disks: list of tuples [(disk, link_name, uri)..]
+    @type kvmhelp: string
+    @param kvmhelp: output of kvm --help
+    @type devlist: string
+    @param devlist: output of kvm -device ?
+    @rtype: list
+    @return: list of command line options eventually used by kvm executable
+
+    """
+    hvp = instance.hvparams
+    kernel_path = hvp[constants.HV_KERNEL_PATH]
+    if kernel_path:
+      boot_disk = False
+    else:
+      boot_disk = hvp[constants.HV_BOOT_ORDER] == constants.HT_BO_DISK
+
+    # whether this is an older KVM version that uses the boot=on flag
+    # on devices
+    needs_boot_flag = self._BOOT_RE.search(kvmhelp)
+
+    dev_opts = []
+    device_driver = None
+    disk_type = hvp[constants.HV_DISK_TYPE]
+    if disk_type == constants.HT_DISK_PARAVIRTUAL:
+      if_val = ",if=%s" % self._VIRTIO
+      try:
+        if self._VIRTIO_BLK_RE.search(devlist):
+          if_val = ",if=none"
+          # will be passed in -device option as driver
+          device_driver = self._VIRTIO_BLK_PCI
+      except errors.HypervisorError, _:
+        pass
+    else:
+      if_val = ",if=%s" % disk_type
+    # Cache mode
+    disk_cache = hvp[constants.HV_DISK_CACHE]
+    if instance.disk_template in constants.DTS_EXT_MIRROR:
+      if disk_cache != "none":
+        # TODO: make this a hard error, instead of a silent overwrite
+        logging.warning("KVM: overriding disk_cache setting '%s' with 'none'"
+                        " to prevent shared storage corruption on migration",
+                        disk_cache)
+      cache_val = ",cache=none"
+    elif disk_cache != constants.HT_CACHE_DEFAULT:
+      cache_val = ",cache=%s" % disk_cache
+    else:
+      cache_val = ""
+    for cfdev, link_name, uri in kvm_disks:
+      if cfdev.mode != constants.DISK_RDWR:
+        raise errors.HypervisorError("Instance has read-only disks which"
+                                     " are not supported by KVM")
+      # TODO: handle FD_LOOP and FD_BLKTAP (?)
+      boot_val = ""
+      if boot_disk:
+        dev_opts.extend(["-boot", "c"])
+        boot_disk = False
+        if needs_boot_flag and disk_type != constants.HT_DISK_IDE:
+          boot_val = ",boot=on"
+
+      access_mode = cfdev.params.get(constants.LDP_ACCESS,
+                                     constants.DISK_KERNELSPACE)
+      if (uri and access_mode == constants.DISK_USERSPACE):
+        drive_uri = uri
+      else:
+        drive_uri = link_name
+
+      drive_val = "file=%s,format=raw%s%s%s" % \
+                  (drive_uri, if_val, boot_val, cache_val)
+
+      if device_driver:
+        # kvm_disks are the 4th entry of runtime file that did not exist in
+        # the past. That means that cfdev should always have pci slot and
+        # _GenerateDeviceKVMId() will not raise a exception.
+        kvm_devid = _GenerateDeviceKVMId(constants.HOTPLUG_TARGET_DISK, cfdev)
+        drive_val += (",id=%s" % kvm_devid)
+        drive_val += (",bus=0,unit=%d" % cfdev.pci)
+        dev_val = ("%s,drive=%s,id=%s" %
+                   (device_driver, kvm_devid, kvm_devid))
+        dev_val += ",bus=pci.0,addr=%s" % hex(cfdev.pci)
+        dev_opts.extend(["-device", dev_val])
+
+      dev_opts.extend(["-drive", drive_val])
+
+    return dev_opts
+
   def _GenerateKVMRuntime(self, instance, block_devices, startup_paused,
                           kvmhelp):
     """Generate KVM information to start an instance.
@@ -1102,9 +1358,8 @@ class KVMHypervisor(hv_base.BaseHypervisor):
 
     kernel_path = hvp[constants.HV_KERNEL_PATH]
     if kernel_path:
-      boot_disk = boot_cdrom = boot_floppy = boot_network = False
+      boot_cdrom = boot_floppy = boot_network = False
     else:
-      boot_disk = hvp[constants.HV_BOOT_ORDER] == constants.HT_BO_DISK
       boot_cdrom = hvp[constants.HV_BOOT_ORDER] == constants.HT_BO_CDROM
       boot_floppy = hvp[constants.HV_BOOT_ORDER] == constants.HT_BO_FLOPPY
       boot_network = hvp[constants.HV_BOOT_ORDER] == constants.HT_BO_NETWORK
@@ -1120,38 +1375,6 @@ class KVMHypervisor(hv_base.BaseHypervisor):
     needs_boot_flag = self._BOOT_RE.search(kvmhelp)
 
     disk_type = hvp[constants.HV_DISK_TYPE]
-    if disk_type == constants.HT_DISK_PARAVIRTUAL:
-      if_val = ",if=virtio"
-    else:
-      if_val = ",if=%s" % disk_type
-    # Cache mode
-    disk_cache = hvp[constants.HV_DISK_CACHE]
-    if instance.disk_template in constants.DTS_EXT_MIRROR:
-      if disk_cache != "none":
-        # TODO: make this a hard error, instead of a silent overwrite
-        logging.warning("KVM: overriding disk_cache setting '%s' with 'none'"
-                        " to prevent shared storage corruption on migration",
-                        disk_cache)
-      cache_val = ",cache=none"
-    elif disk_cache != constants.HT_CACHE_DEFAULT:
-      cache_val = ",cache=%s" % disk_cache
-    else:
-      cache_val = ""
-    for cfdev, dev_path in block_devices:
-      if cfdev.mode != constants.DISK_RDWR:
-        raise errors.HypervisorError("Instance has read-only disks which"
-                                     " are not supported by KVM")
-      # TODO: handle FD_LOOP and FD_BLKTAP (?)
-      boot_val = ""
-      if boot_disk:
-        kvm_cmd.extend(["-boot", "c"])
-        boot_disk = False
-        if needs_boot_flag and disk_type != constants.HT_DISK_IDE:
-          boot_val = ",boot=on"
-
-      drive_val = "file=%s,format=raw%s%s%s" % (dev_path, if_val, boot_val,
-                                                cache_val)
-      kvm_cmd.extend(["-drive", drive_val])
 
     #Now we can specify a different device type for CDROM devices.
     cdrom_disk_type = hvp[constants.HV_KVM_CDROM_DISK_TYPE]
@@ -1415,12 +1638,20 @@ class KVMHypervisor(hv_base.BaseHypervisor):
     if hvp[constants.HV_KVM_EXTRA]:
       kvm_cmd.extend(hvp[constants.HV_KVM_EXTRA].split(" "))
 
-    # Save the current instance nics, but defer their expansion as parameters,
-    # as we'll need to generate executable temp files for them.
-    kvm_nics = instance.nics
+    pci_reservations = bitarray(self._DEFAULT_PCI_RESERVATIONS)
+    kvm_disks = []
+    for disk, link_name, uri in block_devices:
+      _UpdatePCISlots(disk, pci_reservations)
+      kvm_disks.append((disk, link_name, uri))
+
+    kvm_nics = []
+    for nic in instance.nics:
+      _UpdatePCISlots(nic, pci_reservations)
+      kvm_nics.append(nic)
+
     hvparams = hvp
 
-    return (kvm_cmd, kvm_nics, hvparams)
+    return (kvm_cmd, kvm_nics, hvparams, kvm_disks)
 
   def _WriteKVMRuntime(self, instance_name, data):
     """Write an instance's KVM runtime
@@ -1446,9 +1677,14 @@ class KVMHypervisor(hv_base.BaseHypervisor):
     """Save an instance's KVM runtime
 
     """
-    kvm_cmd, kvm_nics, hvparams = kvm_runtime
+    kvm_cmd, kvm_nics, hvparams, kvm_disks = kvm_runtime
+
     serialized_nics = [nic.ToDict() for nic in kvm_nics]
-    serialized_form = serializer.Dump((kvm_cmd, serialized_nics, hvparams))
+    serialized_disks = [(blk.ToDict(), link, uri)
+                        for blk, link, uri in kvm_disks]
+    serialized_form = serializer.Dump((kvm_cmd, serialized_nics, hvparams,
+                                      serialized_disks))
+
     self._WriteKVMRuntime(instance.name, serialized_form)
 
   def _LoadKVMRuntime(self, instance, serialized_runtime=None):
@@ -1457,10 +1693,8 @@ class KVMHypervisor(hv_base.BaseHypervisor):
     """
     if not serialized_runtime:
       serialized_runtime = self._ReadKVMRuntime(instance.name)
-    loaded_runtime = serializer.Load(serialized_runtime)
-    kvm_cmd, serialized_nics, hvparams = loaded_runtime
-    kvm_nics = [objects.NIC.FromDict(snic) for snic in serialized_nics]
-    return (kvm_cmd, kvm_nics, hvparams)
+
+    return _AnalyzeSerializedRuntime(serialized_runtime)
 
   def _RunKVMCmd(self, name, kvm_cmd, tap_fds=None):
     """Run the KVM cmd and check for errors
@@ -1485,6 +1719,8 @@ class KVMHypervisor(hv_base.BaseHypervisor):
     if not self._InstancePidAlive(name)[2]:
       raise errors.HypervisorError("Failed to start instance %s" % name)
 
+  # too many local variables
+  # pylint: disable=R0914
   def _ExecuteKVMRuntime(self, instance, kvm_runtime, kvmhelp, incoming=None):
     """Execute a KVM cmd, after completing it with some last minute data.
 
@@ -1508,7 +1744,7 @@ class KVMHypervisor(hv_base.BaseHypervisor):
 
     temp_files = []
 
-    kvm_cmd, kvm_nics, up_hvp = kvm_runtime
+    kvm_cmd, kvm_nics, up_hvp, kvm_disks = kvm_runtime
     # the first element of kvm_cmd is always the path to the kvm binary
     kvm_path = kvm_cmd[0]
     up_hvp = objects.FillDict(conf_hvp, up_hvp)
@@ -1534,6 +1770,7 @@ class KVMHypervisor(hv_base.BaseHypervisor):
     # related parameters we'll use up_hvp
     tapfds = []
     taps = []
+    devlist = self._GetKVMOutput(kvm_path, self._KVMOPT_DEVICELIST)
     if not kvm_nics:
       kvm_cmd.extend(["-net", "none"])
     else:
@@ -1543,8 +1780,7 @@ class KVMHypervisor(hv_base.BaseHypervisor):
       if nic_type == constants.HT_NIC_PARAVIRTUAL:
         nic_model = self._VIRTIO
         try:
-          devlist = self._GetKVMOutput(kvm_path, self._KVMOPT_DEVICELIST)
-          if self._NEW_VIRTIO_RE.search(devlist):
+          if self._VIRTIO_NET_RE.search(devlist):
             nic_model = self._VIRTIO_NET_PCI
             vnet_hdr = up_hvp[constants.HV_VNET_HDR]
         except errors.HypervisorError, _:
@@ -1569,8 +1805,18 @@ class KVMHypervisor(hv_base.BaseHypervisor):
         tapfds.append(tapfd)
         taps.append(tapname)
         if kvm_supports_netdev:
-          nic_val = "%s,mac=%s,netdev=netdev%s" % (nic_model, nic.mac, nic_seq)
-          tap_val = "type=tap,id=netdev%s,fd=%d%s" % (nic_seq, tapfd, tap_extra)
+          nic_val = "%s,mac=%s" % (nic_model, nic.mac)
+          try:
+            # kvm_nics already exist in old runtime files and thus there might
+            # be some entries without pci slot (therefore try: except:)
+            kvm_devid = _GenerateDeviceKVMId(constants.HOTPLUG_TARGET_NIC, nic)
+            netdev = kvm_devid
+            nic_val += (",id=%s,bus=pci.0,addr=%s" % (kvm_devid, hex(nic.pci)))
+          except errors.HotplugError:
+            netdev = "netdev%d" % nic_seq
+          nic_val += (",netdev=%s" % netdev)
+          tap_val = ("type=tap,id=%s,fd=%d%s" %
+                     (netdev, tapfd, tap_extra))
           kvm_cmd.extend(["-netdev", tap_val, "-device", nic_val])
         else:
           nic_val = "nic,vlan=%s,macaddr=%s,model=%s" % (nic_seq,
@@ -1612,6 +1858,11 @@ class KVMHypervisor(hv_base.BaseHypervisor):
         continue
       self._ConfigureNIC(instance, nic_seq, nic, taps[nic_seq])
 
+    bdev_opts = self._GenerateKVMBlockDevicesOptions(instance,
+                                                     kvm_disks,
+                                                     kvmhelp,
+                                                     devlist)
+    kvm_cmd.extend(bdev_opts)
     # 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.
@@ -1729,6 +1980,157 @@ class KVMHypervisor(hv_base.BaseHypervisor):
 
     return result
 
+  def _GetFreePCISlot(self, instance, dev):
+    """Get the first available pci slot of a runnung instance.
+
+    """
+    slots = bitarray(32)
+    slots.setall(False) # pylint: disable=E1101
+    output = self._CallMonitorCommand(instance.name, self._INFO_PCI_CMD)
+    for line in output.stdout.splitlines():
+      match = self._INFO_PCI_RE.search(line)
+      if match:
+        slot = int(match.group(1))
+        slots[slot] = True
+
+    [free] = slots.search(_AVAILABLE_PCI_SLOT, 1) # pylint: disable=E1101
+    if not free:
+      raise errors.HypervisorError("All PCI slots occupied")
+
+    dev.pci = int(free)
+
+  def VerifyHotplugSupport(self, instance, action, dev_type):
+    """Verifies that hotplug is supported.
+
+    Hotplug is *not* supported in case of:
+     - qemu versions < 1.0
+     - security models and chroot (disk hotplug)
+     - fdsend module is missing (nic hot-add)
+
+    @raise errors.HypervisorError: in one of the previous cases
+
+    """
+    output = self._CallMonitorCommand(instance.name, self._INFO_VERSION_CMD)
+    # TODO: search for netdev_add, drive_add, device_add.....
+    match = self._INFO_VERSION_RE.search(output.stdout)
+    if not match:
+      raise errors.HotplugError("Try hotplug only in running instances.")
+    v_major, v_min, _, _ = match.groups()
+    if (int(v_major), int(v_min)) < (1, 0):
+      raise errors.HotplugError("Hotplug not supported for qemu versions < 1.0")
+
+    if dev_type == constants.HOTPLUG_TARGET_DISK:
+      hvp = instance.hvparams
+      security_model = hvp[constants.HV_SECURITY_MODEL]
+      use_chroot = hvp[constants.HV_KVM_USE_CHROOT]
+      if use_chroot:
+        raise errors.HotplugError("Disk hotplug is not supported"
+                                  " in case of chroot.")
+      if security_model != constants.HT_SM_NONE:
+        raise errors.HotplugError("Disk Hotplug is not supported in case"
+                                  " security models are used.")
+
+    if (dev_type == constants.HOTPLUG_TARGET_NIC and
+        action == constants.HOTPLUG_ACTION_ADD and not fdsend):
+      raise errors.HotplugError("Cannot hot-add NIC."
+                                " fdsend python module is missing.")
+
+  def _CallHotplugCommand(self, name, cmd):
+    output = self._CallMonitorCommand(name, cmd)
+    # TODO: parse output and check if succeeded
+    for line in output.stdout.splitlines():
+      logging.info("%s", line)
+
+  def HotAddDevice(self, instance, dev_type, device, extra, seq):
+    """ Helper method to hot-add a new device
+
+    It gets free pci slot generates the device name and invokes the
+    device specific method.
+
+    """
+    # in case of hot-mod this is given
+    if device.pci is None:
+      self._GetFreePCISlot(instance, device)
+    kvm_devid = _GenerateDeviceKVMId(dev_type, device)
+    runtime = self._LoadKVMRuntime(instance)
+    if dev_type == constants.HOTPLUG_TARGET_DISK:
+      command = "drive_add dummy file=%s,if=none,id=%s,format=raw\n" % \
+                 (extra, kvm_devid)
+      command += ("device_add virtio-blk-pci,bus=pci.0,addr=%s,drive=%s,id=%s" %
+                  (hex(device.pci), kvm_devid, kvm_devid))
+    elif dev_type == constants.HOTPLUG_TARGET_NIC:
+      (tap, fd) = _OpenTap()
+      self._ConfigureNIC(instance, seq, device, tap)
+      self._PassTapFd(instance, fd, device)
+      command = "netdev_add tap,id=%s,fd=%s\n" % (kvm_devid, kvm_devid)
+      args = "virtio-net-pci,bus=pci.0,addr=%s,mac=%s,netdev=%s,id=%s" % \
+               (hex(device.pci), device.mac, kvm_devid, kvm_devid)
+      command += "device_add %s" % args
+      utils.WriteFile(self._InstanceNICFile(instance.name, seq), data=tap)
+
+    self._CallHotplugCommand(instance.name, command)
+    # update relevant entries in runtime file
+    index = _DEVICE_RUNTIME_INDEX[dev_type]
+    entry = _RUNTIME_ENTRY[dev_type](device, extra)
+    runtime[index].append(entry)
+    self._SaveKVMRuntime(instance, runtime)
+
+  def HotDelDevice(self, instance, dev_type, device, _, seq):
+    """ Helper method for hot-del device
+
+    It gets device info from runtime file, generates the device name and
+    invokes the device specific method.
+
+    """
+    runtime = self._LoadKVMRuntime(instance)
+    entry = _GetExistingDeviceInfo(dev_type, device, runtime)
+    kvm_device = _RUNTIME_DEVICE[dev_type](entry)
+    kvm_devid = _GenerateDeviceKVMId(dev_type, kvm_device)
+    if dev_type == constants.HOTPLUG_TARGET_DISK:
+      command = "device_del %s\n" % kvm_devid
+      command += "drive_del %s" % kvm_devid
+    elif dev_type == constants.HOTPLUG_TARGET_NIC:
+      command = "device_del %s\n" % kvm_devid
+      command += "netdev_del %s" % kvm_devid
+      utils.RemoveFile(self._InstanceNICFile(instance.name, seq))
+    self._CallHotplugCommand(instance.name, command)
+    index = _DEVICE_RUNTIME_INDEX[dev_type]
+    runtime[index].remove(entry)
+    self._SaveKVMRuntime(instance, runtime)
+
+    return kvm_device.pci
+
+  def HotModDevice(self, instance, dev_type, device, _, seq):
+    """ Helper method for hot-mod device
+
+    It gets device info from runtime file, generates the device name and
+    invokes the device specific method. Currently only NICs support hot-mod
+
+    """
+    if dev_type == constants.HOTPLUG_TARGET_NIC:
+      # putting it back in the same pci slot
+      device.pci = self.HotDelDevice(instance, dev_type, device, _, seq)
+      # TODO: remove sleep when socat gets removed
+      time.sleep(2)
+      self.HotAddDevice(instance, dev_type, device, _, seq)
+
+  def _PassTapFd(self, instance, fd, nic):
+    """Pass file descriptor to kvm process via monitor socket using SCM_RIGHTS
+
+    """
+    # TODO: factor out code related to unix sockets.
+    #       squash common parts between monitor and qmp
+    kvm_devid = _GenerateDeviceKVMId(constants.HOTPLUG_TARGET_NIC, nic)
+    command = "getfd %s\n" % kvm_devid
+    fds = [fd]
+    logging.info("%s", fds)
+    try:
+      monsock = MonitorSocket(self._InstanceMonitor(instance.name))
+      monsock.connect()
+      fdsend.sendfds(monsock.sock, command, fds=fds)
+    finally:
+      monsock.close()
+
   @classmethod
   def _ParseKVMVersion(cls, text):
     """Parse the KVM version from the --help output.
index 8aa940f..742c8be 100644 (file)
@@ -287,7 +287,7 @@ def _GetConfigFileDiskData(block_devices, blockdev_prefix,
 
   disk_data = []
 
-  for sd_suffix, (cfdev, dev_path) in zip(_letters, block_devices):
+  for sd_suffix, (cfdev, dev_path, _) in zip(_letters, block_devices):
     sd_name = blockdev_prefix + sd_suffix
 
     if cfdev.mode == constants.DISK_RDWR:
@@ -296,7 +296,7 @@ def _GetConfigFileDiskData(block_devices, blockdev_prefix,
       mode = "r"
 
     if cfdev.dev_type in [constants.DT_FILE, constants.DT_SHARED_FILE]:
-      driver = _FILE_DRIVER_MAP[cfdev.physical_id[0]]
+      driver = _FILE_DRIVER_MAP[cfdev.logical_id[0]]
     else:
       driver = "phy"
 
@@ -305,6 +305,19 @@ def _GetConfigFileDiskData(block_devices, blockdev_prefix,
   return disk_data
 
 
+def _QuoteCpuidField(data):
+  """Add quotes around the CPUID field only if necessary.
+
+  Xen CPUID fields come in two shapes: LIBXL strings, which need quotes around
+  them, and lists of XEND strings, which don't.
+
+  @param data: Either type of parameter.
+  @return: The quoted version thereof.
+
+  """
+  return "'%s'" % data if data.startswith("host") else data
+
+
 class XenHypervisor(hv_base.BaseHypervisor):
   """Xen generic hypervisor interface
 
@@ -960,6 +973,8 @@ class XenPvmHypervisor(XenHypervisor):
     constants.HV_VIF_SCRIPT: hv_base.OPT_FILE_CHECK,
     constants.HV_XEN_CMD:
       hv_base.ParamInSet(True, constants.KNOWN_XEN_COMMANDS),
+    constants.HV_XEN_CPUID: hv_base.NO_CHECK,
+    constants.HV_SOUNDHW: hv_base.NO_CHECK,
     }
 
   def _GetConfig(self, instance, startup_memory, block_devices):
@@ -1018,6 +1033,10 @@ class XenPvmHypervisor(XenHypervisor):
         nic_str += ", ip=%s" % ip
       if nic.nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED:
         nic_str += ", bridge=%s" % nic.nicparams[constants.NIC_LINK]
+      if nic.nicparams[constants.NIC_MODE] == constants.NIC_MODE_OVS:
+        nic_str += ", bridge=%s" % nic.nicparams[constants.NIC_LINK]
+        if nic.nicparams[constants.NIC_VLAN]:
+          nic_str += "%s" % nic.nicparams[constants.NIC_VLAN]
       if hvp[constants.HV_VIF_SCRIPT]:
         nic_str += ", script=%s" % hvp[constants.HV_VIF_SCRIPT]
       vif_data.append("'%s'" % nic_str)
@@ -1039,6 +1058,13 @@ class XenPvmHypervisor(XenHypervisor):
     config.write("on_crash = 'restart'\n")
     config.write("extra = '%s'\n" % hvp[constants.HV_KERNEL_ARGS])
 
+    cpuid = hvp[constants.HV_XEN_CPUID]
+    if cpuid:
+      config.write("cpuid = %s\n" % _QuoteCpuidField(cpuid))
+
+    if hvp[constants.HV_SOUNDHW]:
+      config.write("soundhw = '%s'\n" % hvp[constants.HV_SOUNDHW])
+
     return config.getvalue()
 
 
@@ -1089,6 +1115,8 @@ class XenHvmHypervisor(XenHypervisor):
     constants.HV_VIRIDIAN: hv_base.NO_CHECK,
     constants.HV_XEN_CMD:
       hv_base.ParamInSet(True, constants.KNOWN_XEN_COMMANDS),
+    constants.HV_XEN_CPUID: hv_base.NO_CHECK,
+    constants.HV_SOUNDHW: hv_base.NO_CHECK,
     }
 
   def _GetConfig(self, instance, startup_memory, block_devices):
@@ -1218,4 +1246,11 @@ class XenHvmHypervisor(XenHypervisor):
       config.write("on_reboot = 'destroy'\n")
     config.write("on_crash = 'restart'\n")
 
+    cpuid = hvp[constants.HV_XEN_CPUID]
+    if cpuid:
+      config.write("cpuid = %s\n" % _QuoteCpuidField(cpuid))
+
+    if hvp[constants.HV_SOUNDHW]:
+      config.write("soundhw = '%s'\n" % hvp[constants.HV_SOUNDHW])
+
     return config.getvalue()
index 110d386..9e9d988 100644 (file)
@@ -49,6 +49,7 @@ from ganeti import serializer
 from ganeti import workerpool
 from ganeti import locking
 from ganeti import opcodes
+from ganeti import opcodes_base
 from ganeti import errors
 from ganeti import mcpu
 from ganeti import utils
@@ -231,7 +232,7 @@ class _QueuedJob(object):
     count = 0
     for queued_op in self.ops:
       op = queued_op.input
-      reason_src = opcodes.NameToReasonSrc(op.__class__.__name__)
+      reason_src = opcodes_base.NameToReasonSrc(op.__class__.__name__)
       reason_text = "job=%d;index=%d" % (self.id, count)
       reason = getattr(op, "reason", [])
       reason.append((reason_src, reason_text, utils.EpochNano()))
@@ -910,7 +911,7 @@ class _OpExecContext:
     self.summary = op.input.Summary()
 
     # Create local copy to modify
-    if getattr(op.input, opcodes.DEPEND_ATTR, None):
+    if getattr(op.input, opcodes_base.DEPEND_ATTR, None):
       self.jobdeps = op.input.depends[:]
     else:
       self.jobdeps = None
@@ -2196,11 +2197,11 @@ class JobQueue(object):
                                   " are %s" % (idx, op.priority, allowed))
 
       # Check job dependencies
-      dependencies = getattr(op.input, opcodes.DEPEND_ATTR, None)
-      if not opcodes.TNoRelativeJobDependencies(dependencies):
+      dependencies = getattr(op.input, opcodes_base.DEPEND_ATTR, None)
+      if not opcodes_base.TNoRelativeJobDependencies(dependencies):
         raise errors.GenericError("Opcode %s has invalid dependencies, must"
                                   " match %s: %s" %
-                                  (idx, opcodes.TNoRelativeJobDependencies,
+                                  (idx, opcodes_base.TNoRelativeJobDependencies,
                                    dependencies))
 
     # Write to disk
@@ -2228,6 +2229,19 @@ class JobQueue(object):
 
   @locking.ssynchronized(_LOCK)
   @_RequireOpenQueue
+  def SubmitJobToDrainedQueue(self, ops):
+    """Forcefully create and store a new job.
+
+    Do so, even if the job queue is drained.
+    @see: L{_SubmitJobUnlocked}
+
+    """
+    (job_id, ) = self._NewSerialsUnlocked(1)
+    self._EnqueueJobsUnlocked([self._SubmitJobUnlocked(job_id, ops)])
+    return job_id
+
+  @locking.ssynchronized(_LOCK)
+  @_RequireOpenQueue
   @_RequireNonDrainedQueue
   def SubmitManyJobs(self, jobs):
     """Create and store multiple jobs.
@@ -2298,7 +2312,7 @@ class JobQueue(object):
 
     for (idx, (job_id, ops)) in enumerate(zip(job_ids, jobs)):
       for op in ops:
-        if getattr(op, opcodes.DEPEND_ATTR, None):
+        if getattr(op, opcodes_base.DEPEND_ATTR, None):
           (status, data) = \
             self._ResolveJobDependencies(compat.partial(resolve_fn, idx),
                                          op.depends)
index 7a23af6..8860685 100644 (file)
@@ -1625,7 +1625,7 @@ BGL = "BGL"
 NAL = "NAL"
 
 
-class GanetiLockManager:
+class GanetiLockManager(object):
   """The Ganeti Locking Library
 
   The purpose of this small library is to manage locking for ganeti clusters
index 0de9185..29f3aef 100644 (file)
@@ -51,6 +51,7 @@ KEY_RESULT = "result"
 KEY_VERSION = "version"
 
 REQ_SUBMIT_JOB = "SubmitJob"
+REQ_SUBMIT_JOB_TO_DRAINED_QUEUE = "SubmitJobToDrainedQueue"
 REQ_SUBMIT_MANY_JOBS = "SubmitManyJobs"
 REQ_WAIT_FOR_JOB_CHANGE = "WaitForJobChange"
 REQ_CANCEL_JOB = "CancelJob"
@@ -91,6 +92,7 @@ REQ_ALL = compat.UniqueFrozenset([
   REQ_SET_DRAIN_FLAG,
   REQ_SET_WATCHER_PAUSE,
   REQ_SUBMIT_JOB,
+  REQ_SUBMIT_JOB_TO_DRAINED_QUEUE,
   REQ_SUBMIT_MANY_JOBS,
   REQ_WAIT_FOR_JOB_CHANGE,
   ])
@@ -481,6 +483,10 @@ class Client(object):
     ops_state = map(lambda op: op.__getstate__(), ops)
     return self.CallMethod(REQ_SUBMIT_JOB, (ops_state, ))
 
+  def SubmitJobToDrainedQueue(self, ops):
+    ops_state = map(lambda op: op.__getstate__(), ops)
+    return self.CallMethod(REQ_SUBMIT_JOB_TO_DRAINED_QUEUE, (ops_state, ))
+
   def SubmitManyJobs(self, jobs):
     jobs_state = []
     for ops in jobs:
index fefcad0..ac00460 100644 (file)
@@ -336,7 +336,7 @@ class IAReqNodeEvac(IARequestBase):
   MODE = constants.IALLOCATOR_MODE_NODE_EVAC
   REQ_PARAMS = [
     ("instances", _STRING_LIST),
-    ("evac_mode", ht.TElemOf(constants.IALLOCATOR_NEVAC_MODES)),
+    ("evac_mode", ht.TEvacMode),
     ]
   REQ_RESULT = _NEVAC_RESULT
 
@@ -399,10 +399,12 @@ class IAllocator(object):
 
     self._BuildInputData(req)
 
-  def _ComputeClusterDataNodeInfo(self, node_list, cluster_info,
-                                   hypervisor_name):
+  def _ComputeClusterDataNodeInfo(self, disk_templates, node_list,
+                                  cluster_info, hypervisor_name):
     """Prepare and execute node info call.
 
+    @type disk_templates: list of string
+    @param disk_templates: the disk templates of the instances to be allocated
     @type node_list: list of strings
     @param node_list: list of nodes' UUIDs
     @type cluster_info: L{objects.Cluster}
@@ -413,17 +415,17 @@ class IAllocator(object):
     @return: the result of the node info RPC call
 
     """
-    storage_units_raw = utils.storage.GetStorageUnitsOfCluster(
-        self.cfg, include_spindles=True)
+    storage_units_raw = utils.storage.GetStorageUnits(self.cfg, disk_templates)
     storage_units = rpc.PrepareStorageUnitsForNodes(self.cfg, storage_units_raw,
                                                     node_list)
     hvspecs = [(hypervisor_name, cluster_info.hvparams[hypervisor_name])]
     return self.rpc.call_node_info(node_list, storage_units, hvspecs)
 
-  def _ComputeClusterData(self):
+  def _ComputeClusterData(self, disk_template=None):
     """Compute the generic allocator input data.
 
-    This is the data that is independent of the actual operation.
+    @type disk_template: list of string
+    @param disk_template: the disk templates of the instances to be allocated
 
     """
     cluster_info = self.cfg.GetClusterInfo()
@@ -452,9 +454,11 @@ class IAllocator(object):
       hypervisor_name = cluster_info.primary_hypervisor
       node_whitelist = None
 
-    has_lvm = utils.storage.IsLvmEnabled(cluster_info.enabled_disk_templates)
-    node_data = self._ComputeClusterDataNodeInfo(node_list, cluster_info,
-                                                 hypervisor_name)
+    if not disk_template:
+      disk_template = cluster_info.enabled_disk_templates[0]
+
+    node_data = self._ComputeClusterDataNodeInfo([disk_template], node_list,
+                                                 cluster_info, hypervisor_name)
 
     node_iinfo = \
       self.rpc.call_all_instances_info(node_list,
@@ -464,8 +468,8 @@ class IAllocator(object):
     data["nodegroups"] = self._ComputeNodeGroupData(self.cfg)
 
     config_ndata = self._ComputeBasicNodeData(self.cfg, ninfo, node_whitelist)
-    data["nodes"] = self._ComputeDynamicNodeData(ninfo, node_data, node_iinfo,
-                                                 i_list, config_ndata, has_lvm)
+    data["nodes"] = self._ComputeDynamicNodeData(
+        ninfo, node_data, node_iinfo, i_list, config_ndata, disk_template)
     assert len(data["nodes"]) == len(ninfo), \
         "Incomplete node data computed"
 
@@ -548,6 +552,46 @@ class IAllocator(object):
     return value
 
   @staticmethod
+  def _ComputeStorageDataFromSpaceInfoByTemplate(
+      space_info, node_name, disk_template):
+    """Extract storage data from node info.
+
+    @type space_info: see result of the RPC call node info
+    @param space_info: the storage reporting part of the result of the RPC call
+      node info
+    @type node_name: string
+    @param node_name: the node's name
+    @type disk_template: string
+    @param disk_template: the disk template to report space for
+    @rtype: 4-tuple of integers
+    @return: tuple of storage info (total_disk, free_disk, total_spindles,
+       free_spindles)
+
+    """
+    storage_type = constants.MAP_DISK_TEMPLATE_STORAGE_TYPE[disk_template]
+    if storage_type not in constants.STS_REPORT:
+      total_disk = total_spindles = 0
+      free_disk = free_spindles = 0
+    else:
+      template_space_info = utils.storage.LookupSpaceInfoByDiskTemplate(
+          space_info, disk_template)
+      if not template_space_info:
+        raise errors.OpExecError("Node '%s' didn't return space info for disk"
+                                   "template '%s'" % (node_name, disk_template))
+      total_disk = template_space_info["storage_size"]
+      free_disk = template_space_info["storage_free"]
+
+      total_spindles = 0
+      free_spindles = 0
+      if disk_template in constants.DTS_LVM:
+        lvm_pv_info = utils.storage.LookupSpaceInfoByStorageType(
+           space_info, constants.ST_LVM_PV)
+        if lvm_pv_info:
+          total_spindles = lvm_pv_info["storage_size"]
+          free_spindles = lvm_pv_info["storage_free"]
+    return (total_disk, free_disk, total_spindles, free_spindles)
+
+  @staticmethod
   def _ComputeStorageDataFromSpaceInfo(space_info, node_name, has_lvm):
     """Extract storage data from node info.
 
@@ -574,7 +618,7 @@ class IAllocator(object):
       free_disk = lvm_vg_info["storage_free"]
       lvm_pv_info = utils.storage.LookupSpaceInfoByStorageType(
          space_info, constants.ST_LVM_PV)
-      if not lvm_vg_info:
+      if not lvm_pv_info:
         raise errors.OpExecError("Node '%s' didn't return LVM pv space info."
                                  % (node_name))
       total_spindles = lvm_pv_info["storage_size"]
@@ -616,7 +660,7 @@ class IAllocator(object):
     return (i_p_mem, i_p_up_mem, mem_free)
 
   def _ComputeDynamicNodeData(self, node_cfg, node_data, node_iinfo, i_list,
-                              node_results, has_lvm):
+                              node_results, disk_template):
     """Compute global node data.
 
     @param node_results: the basic node structures as filled from the config
@@ -642,8 +686,8 @@ class IAllocator(object):
         (i_p_mem, i_p_up_mem, mem_free) = self._ComputeInstanceMemory(
              i_list, node_iinfo, nuuid, mem_free)
         (total_disk, free_disk, total_spindles, free_spindles) = \
-            self._ComputeStorageDataFromSpaceInfo(space_info, ninfo.name,
-                                                  has_lvm)
+            self._ComputeStorageDataFromSpaceInfoByTemplate(
+                space_info, ninfo.name, disk_template)
 
         # compute memory used by instances
         pnr_dyn = {
@@ -715,9 +759,12 @@ class IAllocator(object):
     """Build input data structures.
 
     """
-    self._ComputeClusterData()
-
     request = req.GetRequest(self.cfg)
+    disk_template = None
+    if "disk_template" in request:
+      disk_template = request["disk_template"]
+    self._ComputeClusterData(disk_template=disk_template)
+
     request["type"] = req.MODE
     self.in_data["request"] = request
 
index a4936a4..4d8b8a0 100644 (file)
@@ -1165,10 +1165,11 @@ class ExportInstanceHelper:
 
     instance = self._instance
     src_node = instance.primary_node
+    src_node_name = self._lu.cfg.GetNodeName(src_node)
 
     for idx, disk in enumerate(instance.disks):
       self._feedback_fn("Creating a snapshot of disk/%s on node %s" %
-                        (idx, src_node))
+                        (idx, src_node_name))
 
       # result.payload will be a snapshot of an lvm leaf of the one we
       # passed
@@ -1177,17 +1178,16 @@ class ExportInstanceHelper:
       msg = result.fail_msg
       if msg:
         self._lu.LogWarning("Could not snapshot disk/%s on node %s: %s",
-                            idx, src_node, msg)
+                            idx, src_node_name, msg)
       elif (not isinstance(result.payload, (tuple, list)) or
             len(result.payload) != 2):
         self._lu.LogWarning("Could not snapshot disk/%s on node %s: invalid"
-                            " result '%s'", idx, src_node, result.payload)
+                            " result '%s'", idx, src_node_name, result.payload)
       else:
         disk_id = tuple(result.payload)
         disk_params = constants.DISK_LD_DEFAULTS[constants.DT_PLAIN].copy()
         new_dev = objects.Disk(dev_type=constants.DT_PLAIN, size=disk.size,
-                               logical_id=disk_id, physical_id=disk_id,
-                               iv_name=disk.iv_name,
+                               logical_id=disk_id, iv_name=disk.iv_name,
                                params=disk_params)
 
       self._snap_disks.append(new_dev)
@@ -1205,14 +1205,17 @@ class ExportInstanceHelper:
     disk = self._snap_disks[disk_index]
     if disk and not self._removed_snaps[disk_index]:
       src_node = self._instance.primary_node
+      src_node_name = self._lu.cfg.GetNodeName(src_node)
 
       self._feedback_fn("Removing snapshot of disk/%s on node %s" %
-                        (disk_index, src_node))
+                        (disk_index, src_node_name))
 
-      result = self._lu.rpc.call_blockdev_remove(src_node, disk)
+      result = self._lu.rpc.call_blockdev_remove(src_node,
+                                                 (disk, self._instance))
       if result.fail_msg:
         self._lu.LogWarning("Could not remove snapshot for disk/%d from node"
-                            " %s: %s", disk_index, src_node, result.fail_msg)
+                            " %s: %s", disk_index, src_node_name,
+                            result.fail_msg)
       else:
         self._removed_snaps[disk_index] = True
 
@@ -1236,13 +1239,13 @@ class ExportInstanceHelper:
         continue
 
       path = utils.PathJoin(pathutils.EXPORT_DIR, "%s.new" % instance.name,
-                            dev.physical_id[1])
+                            dev.logical_id[1])
 
       finished_fn = compat.partial(self._TransferFinished, idx)
 
       # FIXME: pass debug option from opcode to backend
       dt = DiskTransfer("snapshot/%s" % idx,
-                        constants.IEIO_SCRIPT, (dev, idx),
+                        constants.IEIO_SCRIPT, ((dev, instance), idx),
                         constants.IEIO_FILE, (path, ),
                         finished_fn)
       transfers.append(dt)
@@ -1300,7 +1303,7 @@ class ExportInstanceHelper:
         finished_fn = compat.partial(self._TransferFinished, idx)
         ieloop.Add(DiskExport(self._lu, instance.primary_node,
                               opts, host, port, instance, "disk%d" % idx,
-                              constants.IEIO_SCRIPT, (dev, idx),
+                              constants.IEIO_SCRIPT, ((dev, instance), idx),
                               timeouts, cbs, private=(idx, finished_fn)))
 
       ieloop.Run()
@@ -1482,7 +1485,7 @@ def RemoteImport(lu, feedback_fn, instance, pnode, source_x509_ca,
 
         ieloop.Add(DiskImport(lu, instance.primary_node, opts, instance,
                               "disk%d" % idx,
-                              constants.IEIO_SCRIPT, (dev, idx),
+                              constants.IEIO_SCRIPT, ((dev, instance), idx),
                               timeouts, cbs, private=(idx, )))
 
       ieloop.Run()
index 4498747..5a04805 100644 (file)
@@ -36,6 +36,7 @@ import itertools
 import traceback
 
 from ganeti import opcodes
+from ganeti import opcodes_base
 from ganeti import constants
 from ganeti import errors
 from ganeti import hooksmaster
@@ -207,7 +208,7 @@ def _SetBaseOpParams(src, defcomment, dst):
       hasattr(src, "priority")):
     dst.priority = src.priority
 
-  if not getattr(dst, opcodes.COMMENT_ATTR, None):
+  if not getattr(dst, opcodes_base.COMMENT_ATTR, None):
     dst.comment = defcomment
 
 
@@ -469,6 +470,22 @@ class Processor(object):
 
     return result
 
+  # pylint: disable=R0201
+  def _CheckLUResult(self, op, result):
+    """Check the LU result against the contract in the opcode.
+
+    """
+    resultcheck_fn = op.OP_RESULT
+    if not (resultcheck_fn is None or resultcheck_fn(result)):
+      logging.error("Expected opcode result matching %s, got %s",
+                    resultcheck_fn, result)
+      if not getattr(op, "dry_run", False):
+        # FIXME: LUs should still behave in dry_run mode, or
+        # alternately we should have OP_DRYRUN_RESULT; in the
+        # meantime, we simply skip the OP_RESULT check in dry-run mode
+        raise errors.OpResultError("Opcode result does not match %s: %s" %
+                                   (resultcheck_fn, utils.Truncate(result, 80)))
+
   def ExecOpCode(self, op, cbs, timeout=None):
     """Execute an opcode.
 
@@ -526,16 +543,7 @@ class Processor(object):
     finally:
       self._cbs = None
 
-    resultcheck_fn = op.OP_RESULT
-    if not (resultcheck_fn is None or resultcheck_fn(result)):
-      logging.error("Expected opcode result matching %s, got %s",
-                    resultcheck_fn, result)
-      if not getattr(op, "dry_run", False):
-        # FIXME: LUs should still behave in dry_run mode, or
-        # alternately we should have OP_DRYRUN_RESULT; in the
-        # meantime, we simply skip the OP_RESULT check in dry-run mode
-        raise errors.OpResultError("Opcode result does not match %s: %s" %
-                                   (resultcheck_fn, utils.Truncate(result, 80)))
+    self._CheckLUResult(op, result)
 
     return result
 
index b4555ef..be427aa 100644 (file)
@@ -266,6 +266,10 @@ class ConfigObject(outils.ValidatedSlots):
     """Implement __repr__ for ConfigObjects."""
     return repr(self.ToDict())
 
+  def __eq__(self, other):
+    """Implement __eq__ for ConfigObjects."""
+    return isinstance(other, self.__class__) and self.ToDict() == other.ToDict()
+
   def UpgradeConfig(self):
     """Fill defaults for missing configuration values.
 
@@ -447,10 +451,7 @@ class ConfigData(ConfigObject):
       InstancePolicy.UpgradeDiskTemplates(
         nodegroup.ipolicy, self.cluster.enabled_disk_templates)
     if self.cluster.drbd_usermode_helper is None:
-      # To decide if we set an helper let's check if at least one instance has
-      # a DRBD disk. This does not cover all the possible scenarios but it
-      # gives a good approximation.
-      if self.HasAnyDiskOfType(constants.DT_DRBD8):
+      if self.cluster.IsDiskTemplateEnabled(constants.DT_DRBD8):
         self.cluster.drbd_usermode_helper = constants.DEFAULT_DRBD_HELPER
     if self.networks is None:
       self.networks = {}
@@ -484,7 +485,8 @@ class ConfigData(ConfigObject):
 
 class NIC(ConfigObject):
   """Config object representing a network card."""
-  __slots__ = ["name", "mac", "ip", "network", "nicparams", "netinfo"] + _UUID
+  __slots__ = ["name", "mac", "ip", "network",
+               "nicparams", "netinfo", "pci"] + _UUID
 
   @classmethod
   def CheckParameterSyntax(cls, nicparams):
@@ -507,9 +509,11 @@ class NIC(ConfigObject):
 
 class Disk(ConfigObject):
   """Config object representing a block device."""
-  __slots__ = (["name", "dev_type", "logical_id", "physical_id",
-                "children", "iv_name", "size", "mode", "params", "spindles"] +
-               _UUID)
+  __slots__ = (["name", "dev_type", "logical_id", "children", "iv_name",
+                "size", "mode", "params", "spindles", "pci"] + _UUID +
+               # dynamic_params is special. It depends on the node this instance
+               # is sent to, and should not be persisted.
+               ["dynamic_params"])
 
   def CreateOnSecondary(self):
     """Test if this device needs to be created on a secondary node."""
@@ -697,51 +701,53 @@ class Disk(ConfigObject):
         child.UnsetSize()
     self.size = 0
 
-  def SetPhysicalID(self, target_node_uuid, nodes_ip):
-    """Convert the logical ID to the physical ID.
-
-    This is used only for drbd, which needs ip/port configuration.
+  def UpdateDynamicDiskParams(self, target_node_uuid, nodes_ip):
+    """Updates the dynamic disk params for the given node.
 
-    The routine descends down and updates its children also, because
-    this helps when the only the top device is passed to the remote
-    node.
+    This is mainly used for drbd, which needs ip/port configuration.
 
     Arguments:
       - target_node_uuid: the node UUID we wish to configure for
       - nodes_ip: a mapping of node name to ip
 
-    The target_node must exist in in nodes_ip, and must be one of the
-    nodes in the logical ID for each of the DRBD devices encountered
-    in the disk tree.
+    The target_node must exist in nodes_ip, and should be one of the
+    nodes in the logical ID if this device is a DRBD device.
 
     """
     if self.children:
       for child in self.children:
-        child.SetPhysicalID(target_node_uuid, nodes_ip)
+        child.UpdateDynamicDiskParams(target_node_uuid, nodes_ip)
 
-    if self.logical_id is None and self.physical_id is not None:
-      return
-    if self.dev_type in constants.DTS_DRBD:
-      pnode_uuid, snode_uuid, port, pminor, sminor, secret = self.logical_id
+    dyn_disk_params = {}
+    if self.logical_id is not None and self.dev_type in constants.DTS_DRBD:
+      pnode_uuid, snode_uuid, _, pminor, sminor, _ = self.logical_id
       if target_node_uuid not in (pnode_uuid, snode_uuid):
-        raise errors.ConfigurationError("DRBD device not knowing node %s" %
-                                        target_node_uuid)
+        # disk object is being sent to neither the primary nor the secondary
+        # node. reset the dynamic parameters, the target node is not
+        # supposed to use them.
+        self.dynamic_params = dyn_disk_params
+        return
+
       pnode_ip = nodes_ip.get(pnode_uuid, None)
       snode_ip = nodes_ip.get(snode_uuid, None)
       if pnode_ip is None or snode_ip is None:
         raise errors.ConfigurationError("Can't find primary or secondary node"
                                         " for %s" % str(self))
-      p_data = (pnode_ip, port)
-      s_data = (snode_ip, port)
       if pnode_uuid == target_node_uuid:
-        self.physical_id = p_data + s_data + (pminor, secret)
+        dyn_disk_params[constants.DDP_LOCAL_IP] = pnode_ip
+        dyn_disk_params[constants.DDP_REMOTE_IP] = snode_ip
+        dyn_disk_params[constants.DDP_LOCAL_MINOR] = pminor
+        dyn_disk_params[constants.DDP_REMOTE_MINOR] = sminor
       else: # it must be secondary, we tested above
-        self.physical_id = s_data + p_data + (sminor, secret)
-    else:
-      self.physical_id = self.logical_id
-    return
+        dyn_disk_params[constants.DDP_LOCAL_IP] = snode_ip
+        dyn_disk_params[constants.DDP_REMOTE_IP] = pnode_ip
+        dyn_disk_params[constants.DDP_LOCAL_MINOR] = sminor
+        dyn_disk_params[constants.DDP_REMOTE_MINOR] = pminor
 
-  def ToDict(self):
+    self.dynamic_params = dyn_disk_params
+
+  # pylint: disable=W0221
+  def ToDict(self, include_dynamic_params=False):
     """Disk-specific conversion to standard python types.
 
     This replaces the children lists of objects with lists of
@@ -749,6 +755,8 @@ class Disk(ConfigObject):
 
     """
     bo = super(Disk, self).ToDict()
+    if not include_dynamic_params and "dynamic_params" in bo:
+      del bo["dynamic_params"]
 
     for attr in ("children",):
       alist = bo.get(attr, None)
@@ -766,8 +774,6 @@ class Disk(ConfigObject):
       obj.children = outils.ContainerFromDicts(obj.children, list, Disk)
     if obj.logical_id and isinstance(obj.logical_id, list):
       obj.logical_id = tuple(obj.logical_id)
-    if obj.physical_id and isinstance(obj.physical_id, list):
-      obj.physical_id = tuple(obj.physical_id)
     if obj.dev_type in constants.DTS_DRBD:
       # we need a tuple of length six here
       if len(obj.logical_id) < 6:
@@ -783,22 +789,16 @@ class Disk(ConfigObject):
     elif self.dev_type in constants.DTS_DRBD:
       node_a, node_b, port, minor_a, minor_b = self.logical_id[:5]
       val = "<DRBD8("
-      if self.physical_id is None:
-        phy = "unconfigured"
-      else:
-        phy = ("configured as %s:%s %s:%s" %
-               (self.physical_id[0], self.physical_id[1],
-                self.physical_id[2], self.physical_id[3]))
 
-      val += ("hosts=%s/%d-%s/%d, port=%s, %s, " %
-              (node_a, minor_a, node_b, minor_b, port, phy))
+      val += ("hosts=%s/%d-%s/%d, port=%s, " %
+              (node_a, minor_a, node_b, minor_b, port))
       if self.children and self.children.count(None) == 0:
         val += "backend=%s, metadev=%s" % (self.children[0], self.children[1])
       else:
         val += "no local storage"
     else:
-      val = ("<Disk(type=%s, logical_id=%s, physical_id=%s, children=%s" %
-             (self.dev_type, self.logical_id, self.physical_id, self.children))
+      val = ("<Disk(type=%s, logical_id=%s, children=%s" %
+             (self.dev_type, self.logical_id, self.children))
     if self.iv_name is None:
       val += ", not visible"
     else:
@@ -900,6 +900,7 @@ class Disk(ConfigObject):
     elif disk_template == constants.DT_RBD:
       result.append(FillDict(constants.DISK_LD_DEFAULTS[constants.DT_RBD], {
         constants.LDP_POOL: dt_params[constants.RBD_POOL],
+        constants.LDP_ACCESS: dt_params[constants.RBD_ACCESS],
         }))
 
     elif disk_template == constants.DT_EXT:
diff --git a/lib/opcodes.py b/lib/opcodes.py
deleted file mode 100644 (file)
index ccdb9aa..0000000
+++ /dev/null
@@ -1,2231 +0,0 @@
-#
-#
-
-# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 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.
-
-
-"""OpCodes module
-
-This module implements the data structures which define the cluster
-operations - the so-called opcodes.
-
-Every operation which modifies the cluster state is expressed via
-opcodes.
-
-"""
-
-# this are practically structures, so disable the message about too
-# few public methods:
-# pylint: disable=R0903
-
-import logging
-import re
-import ipaddr
-
-from ganeti import constants
-from ganeti import errors
-from ganeti import ht
-from ganeti import objects
-from ganeti import outils
-
-
-# Common opcode attributes
-
-#: output fields for a query operation
-_POutputFields = ("output_fields", ht.NoDefault, ht.TListOf(ht.TNonEmptyString),
-                  "Selected output fields")
-
-#: the shutdown timeout
-_PShutdownTimeout = \
-  ("shutdown_timeout", constants.DEFAULT_SHUTDOWN_TIMEOUT, ht.TNonNegativeInt,
-   "How long to wait for instance to shut down")
-
-#: the force parameter
-_PForce = ("force", False, ht.TBool, "Whether to force the operation")
-
-#: a required instance name (for single-instance LUs)
-_PInstanceName = ("instance_name", ht.NoDefault, ht.TNonEmptyString,
-                  "Instance name")
-
-#: a instance UUID (for single-instance LUs)
-_PInstanceUuid = ("instance_uuid", None, ht.TMaybeString,
-                  "Instance UUID")
-
-#: Whether to ignore offline nodes
-_PIgnoreOfflineNodes = ("ignore_offline_nodes", False, ht.TBool,
-                        "Whether to ignore offline nodes")
-
-#: a required node name (for single-node LUs)
-_PNodeName = ("node_name", ht.NoDefault, ht.TNonEmptyString, "Node name")
-
-#: a node UUID (for use with _PNodeName)
-_PNodeUuid = ("node_uuid", None, ht.TMaybeString, "Node UUID")
-
-#: a required node group name (for single-group LUs)
-_PGroupName = ("group_name", ht.NoDefault, ht.TNonEmptyString, "Group name")
-
-#: Migration type (live/non-live)
-_PMigrationMode = ("mode", None,
-                   ht.TMaybe(ht.TElemOf(constants.HT_MIGRATION_MODES)),
-                   "Migration mode")
-
-#: Obsolete 'live' migration mode (boolean)
-_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),
-             "Tag kind")
-
-#: List of tag strings
-_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")
-
-_PWaitForSync = ("wait_for_sync", True, ht.TBool,
-                 "Whether to wait for the disk to synchronize")
-
-_PWaitForSyncFalse = ("wait_for_sync", False, ht.TBool,
-                      "Whether to wait for the disk to synchronize"
-                      " (defaults to false)")
-
-_PIgnoreConsistency = ("ignore_consistency", False, ht.TBool,
-                       "Whether to ignore disk consistency")
-
-_PStorageName = ("name", ht.NoDefault, ht.TMaybeString, "Storage name")
-
-_PUseLocking = ("use_locking", False, ht.TBool,
-                "Whether to use synchronization")
-
-_PNameCheck = ("name_check", True, ht.TBool, "Whether to check name")
-
-_PNodeGroupAllocPolicy = \
-  ("alloc_policy", None,
-   ht.TMaybe(ht.TElemOf(constants.VALID_ALLOC_POLICIES)),
-   "Instance allocation policy")
-
-_PGroupNodeParams = ("ndparams", None, ht.TMaybeDict,
-                     "Default node parameters for group")
-
-_PQueryWhat = ("what", ht.NoDefault, ht.TElemOf(constants.QR_VIA_OP),
-               "Resource(s) to query for")
-
-_PEarlyRelease = ("early_release", False, ht.TBool,
-                  "Whether to release locks as soon as possible")
-
-_PIpCheckDoc = "Whether to ensure instance's IP address is inactive"
-
-#: Do not remember instance state changes
-_PNoRemember = ("no_remember", False, ht.TBool,
-                "Do not remember the state change")
-
-#: Target node for instance migration/failover
-_PMigrationTargetNode = ("target_node", None, ht.TMaybeString,
-                         "Target node for shared-storage instances")
-
-_PMigrationTargetNodeUuid = ("target_node_uuid", None, ht.TMaybeString,
-                             "Target node UUID for shared-storage instances")
-
-_PStartupPaused = ("startup_paused", False, ht.TBool,
-                   "Pause instance at startup")
-
-_PVerbose = ("verbose", False, ht.TBool, "Verbose mode")
-
-# Parameters for cluster verification
-_PDebugSimulateErrors = ("debug_simulate_errors", False, ht.TBool,
-                         "Whether to simulate errors (useful for debugging)")
-_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.TMaybe(ht.TDictOf(ht.TElemOf(constants.DISK_TEMPLATES), ht.TDict)),
-   "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")
-
-#: Opportunistic locking
-_POpportunisticLocking = \
-  ("opportunistic_locking", False, ht.TBool,
-   ("Whether to employ opportunistic locking for nodes, meaning nodes"
-    " already locked by another opcode won't be considered for instance"
-    " allocation (only when an iallocator is used)"))
-
-_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)")
-
-#: IAllocator field builder
-_PIAllocFromDesc = lambda desc: ("iallocator", None, ht.TMaybeString, desc)
-
-#: a required network name
-_PNetworkName = ("network_name", ht.NoDefault, ht.TNonEmptyString,
-                 "Set network name")
-
-_PTargetGroups = \
-  ("target_groups", None, ht.TMaybeListOf(ht.TNonEmptyString),
-   "Destination group names or UUIDs (defaults to \"all but current group\")")
-
-#: OP_ID conversion regular expression
-_OPID_RE = re.compile("([a-z])([A-Z])")
-
-#: Utility function for L{OpClusterSetParams}
-_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.Comment("NIC parameters")(ht.TDictOf(ht.TElemOf(constants.INIC_PARAMS),
-                                          ht.TMaybeString))
-
-_TSetParamsResultItemItems = [
-  ht.Comment("name of changed parameter")(ht.TNonEmptyString),
-  ht.Comment("new value")(ht.TAny),
-  ]
-
-_TSetParamsResult = \
-  ht.TListOf(ht.TAnd(ht.TIsLength(len(_TSetParamsResultItemItems)),
-                     ht.TItems(_TSetParamsResultItemItems)))
-
-# In the disks option we can provide arbitrary parameters too, which
-# we may not be able to validate at this level, so we just check the
-# format of the dict here and the checks concerning IDISK_PARAMS will
-# happen at the LU level
-_TDiskParams = \
-  ht.Comment("Disk parameters")(ht.TDictOf(ht.TNonEmptyString,
-                                           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_",
-  "NODE_": "N_",
-  "INSTANCE_": "I_",
-  }
-
-#: Attribute name for dependencies
-DEPEND_ATTR = "depends"
-
-#: Attribute name for comment
-COMMENT_ATTR = "comment"
-
-
-def _NameComponents(name):
-  """Split an opcode class name into its components
-
-  @type name: string
-  @param name: the class name, as OpXxxYyy
-  @rtype: array of strings
-  @return: the components of the name
-
-  """
-  assert name.startswith("Op")
-  # Note: (?<=[a-z])(?=[A-Z]) would be ideal, since it wouldn't
-  # consume any input, and hence we would just have all the elements
-  # in the list, one by one; but it seems that split doesn't work on
-  # non-consuming input, hence we have to process the input string a
-  # bit
-  name = _OPID_RE.sub(r"\1,\2", name)
-  elems = name.split(",")
-  return elems
-
-
-def _NameToId(name):
-  """Convert an opcode class name to an OP_ID.
-
-  @type name: string
-  @param name: the class name, as OpXxxYyy
-  @rtype: string
-  @return: the name in the OP_XXXX_YYYY format
-
-  """
-  if not name.startswith("Op"):
-    return None
-  return "_".join(n.upper() for n in _NameComponents(name))
-
-
-def NameToReasonSrc(name):
-  """Convert an opcode class name to a source string for the reason trail
-
-  @type name: string
-  @param name: the class name, as OpXxxYyy
-  @rtype: string
-  @return: the name in the OP_XXXX_YYYY format
-
-  """
-  if not name.startswith("Op"):
-    return None
-  return "%s:%s" % (constants.OPCODE_REASON_SRC_OPCODE,
-                    "_".join(n.lower() for n in _NameComponents(name)))
-
-
-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 _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.TMaybe(template_check)
-
-  return template_check
-
-
-def _CheckStorageType(storage_type):
-  """Ensure a given storage type is valid.
-
-  """
-  if storage_type not in constants.STORAGE_TYPES:
-    raise errors.OpPrereqError("Unknown storage type: %s" % storage_type,
-                               errors.ECODE_INVAL)
-  return True
-
-
-#: Storage type parameter
-_PStorageType = ("storage_type", ht.NoDefault, _CheckStorageType,
-                 "Storage type")
-
-
-@ht.WithDesc("IPv4 network")
-def _CheckCIDRNetNotation(value):
-  """Ensure a given CIDR notation type is valid.
-
-  """
-  try:
-    ipaddr.IPv4Network(value)
-  except ipaddr.AddressValueError:
-    return False
-  return True
-
-
-@ht.WithDesc("IPv4 address")
-def _CheckCIDRAddrNotation(value):
-  """Ensure a given CIDR notation type is valid.
-
-  """
-  try:
-    ipaddr.IPv4Address(value)
-  except ipaddr.AddressValueError:
-    return False
-  return True
-
-
-@ht.WithDesc("IPv6 address")
-def _CheckCIDR6AddrNotation(value):
-  """Ensure a given CIDR notation type is valid.
-
-  """
-  try:
-    ipaddr.IPv6Address(value)
-  except ipaddr.AddressValueError:
-    return False
-  return True
-
-
-@ht.WithDesc("IPv6 network")
-def _CheckCIDR6NetNotation(value):
-  """Ensure a given CIDR notation type is valid.
-
-  """
-  try:
-    ipaddr.IPv6Network(value)
-  except ipaddr.AddressValueError:
-    return False
-  return True
-
-
-_TIpAddress4 = ht.TAnd(ht.TString, _CheckCIDRAddrNotation)
-_TIpAddress6 = ht.TAnd(ht.TString, _CheckCIDR6AddrNotation)
-_TIpNetwork4 = ht.TAnd(ht.TString, _CheckCIDRNetNotation)
-_TIpNetwork6 = ht.TAnd(ht.TString, _CheckCIDR6NetNotation)
-_TMaybeAddr4List = ht.TMaybe(ht.TListOf(_TIpAddress4))
-
-
-class _AutoOpParamSlots(outils.AutoSlots):
-  """Meta class for opcode definitions.
-
-  """
-  def __new__(mcs, name, bases, attrs):
-    """Called when a class should be created.
-
-    @param mcs: The meta class
-    @param name: Name of created class
-    @param bases: Base classes
-    @type attrs: dict
-    @param attrs: Class attributes
-
-    """
-    assert "OP_ID" not in attrs, "Class '%s' defining OP_ID" % name
-
-    slots = mcs._GetSlots(attrs)
-    assert "OP_DSC_FIELD" not in attrs or attrs["OP_DSC_FIELD"] in slots, \
-      "Class '%s' uses unknown field in OP_DSC_FIELD" % name
-    assert ("OP_DSC_FORMATTER" not in attrs or
-            callable(attrs["OP_DSC_FORMATTER"])), \
-      ("Class '%s' uses non-callable in OP_DSC_FORMATTER (%s)" %
-       (name, type(attrs["OP_DSC_FORMATTER"])))
-
-    attrs["OP_ID"] = _NameToId(name)
-
-    return outils.AutoSlots.__new__(mcs, name, bases, attrs)
-
-  @classmethod
-  def _GetSlots(mcs, attrs):
-    """Build the slots out of OP_PARAMS.
-
-    """
-    # Always set OP_PARAMS to avoid duplicates in BaseOpCode.GetAllParams
-    params = attrs.setdefault("OP_PARAMS", [])
-
-    # Use parameter names as slots
-    return [pname for (pname, _, _, _) in params]
-
-
-class BaseOpCode(outils.ValidatedSlots):
-  """A simple serializable object.
-
-  This object serves as a parent class for OpCode without any custom
-  field handling.
-
-  """
-  # pylint: disable=E1101
-  # as OP_ID is dynamically defined
-  __metaclass__ = _AutoOpParamSlots
-
-  def __getstate__(self):
-    """Generic serializer.
-
-    This method just returns the contents of the instance as a
-    dictionary.
-
-    @rtype:  C{dict}
-    @return: the instance attributes and their values
-
-    """
-    state = {}
-    for name in self.GetAllSlots():
-      if hasattr(self, name):
-        state[name] = getattr(self, name)
-    return state
-
-  def __setstate__(self, state):
-    """Generic unserializer.
-
-    This method just restores from the serialized state the attributes
-    of the current instance.
-
-    @param state: the serialized opcode data
-    @type state:  C{dict}
-
-    """
-    if not isinstance(state, dict):
-      raise ValueError("Invalid data to __setstate__: expected dict, got %s" %
-                       type(state))
-
-    for name in self.GetAllSlots():
-      if name not in state and hasattr(self, name):
-        delattr(self, name)
-
-    for name in state:
-      setattr(self, name, state[name])
-
-  @classmethod
-  def GetAllParams(cls):
-    """Compute list of all parameters for an opcode.
-
-    """
-    slots = []
-    for parent in cls.__mro__:
-      slots.extend(getattr(parent, "OP_PARAMS", []))
-    return slots
-
-  def Validate(self, set_defaults): # pylint: disable=W0221
-    """Validate opcode parameters, optionally setting default values.
-
-    @type set_defaults: bool
-    @param set_defaults: Whether to set default values
-    @raise errors.OpPrereqError: When a parameter value doesn't match
-                                 requirements
-
-    """
-    for (attr_name, default, test, _) in self.GetAllParams():
-      assert test == ht.NoType or callable(test)
-
-      if not hasattr(self, attr_name):
-        if default == ht.NoDefault:
-          raise errors.OpPrereqError("Required parameter '%s.%s' missing" %
-                                     (self.OP_ID, attr_name),
-                                     errors.ECODE_INVAL)
-        elif set_defaults:
-          if callable(default):
-            dval = default()
-          else:
-            dval = default
-          setattr(self, attr_name, dval)
-
-      if test == ht.NoType:
-        # no tests here
-        continue
-
-      if set_defaults or hasattr(self, attr_name):
-        attr_val = getattr(self, attr_name)
-        if not test(attr_val):
-          logging.error("OpCode %s, parameter %s, has invalid type %s/value"
-                        " '%s' expecting type %s",
-                        self.OP_ID, attr_name, type(attr_val), attr_val, test)
-          raise errors.OpPrereqError("Parameter '%s.%s' fails validation" %
-                                     (self.OP_ID, attr_name),
-                                     errors.ECODE_INVAL)
-
-
-def _BuildJobDepCheck(relative):
-  """Builds check for job dependencies (L{DEPEND_ATTR}).
-
-  @type relative: bool
-  @param relative: Whether to accept relative job IDs (negative)
-  @rtype: callable
-
-  """
-  if relative:
-    job_id = ht.TOr(ht.TJobId, ht.TRelativeJobId)
-  else:
-    job_id = ht.TJobId
-
-  job_dep = \
-    ht.TAnd(ht.TOr(ht.TList, ht.TTuple),
-            ht.TIsLength(2),
-            ht.TItems([job_id,
-                       ht.TListOf(ht.TElemOf(constants.JOBS_FINALIZED))]))
-
-  return ht.TMaybeListOf(job_dep)
-
-
-TNoRelativeJobDependencies = _BuildJobDepCheck(False)
-
-#: List of submission status and job ID as returned by C{SubmitManyJobs}
-_TJobIdListItem = \
-  ht.TAnd(ht.TIsLength(2),
-          ht.TItems([ht.Comment("success")(ht.TBool),
-                     ht.Comment("Job ID if successful, error message"
-                                " otherwise")(ht.TOr(ht.TString,
-                                                     ht.TJobId))]))
-TJobIdList = ht.TListOf(_TJobIdListItem)
-
-#: Result containing only list of submitted jobs
-TJobIdListOnly = ht.TStrictDict(True, True, {
-  constants.JOB_IDS_KEY: ht.Comment("List of submitted jobs")(TJobIdList),
-  })
-
-
-class OpCode(BaseOpCode):
-  """Abstract OpCode.
-
-  This is the root of the actual OpCode hierarchy. All clases derived
-  from this class should override OP_ID.
-
-  @cvar OP_ID: The ID of this opcode. This should be unique amongst all
-               children of this class.
-  @cvar OP_DSC_FIELD: The name of a field whose value will be included in the
-                      string returned by Summary(); see the docstring of that
-                      method for details).
-  @cvar OP_DSC_FORMATTER: A callable that should format the OP_DSC_FIELD; if
-                          not present, then the field will be simply converted
-                          to string
-  @cvar OP_PARAMS: List of opcode attributes, the default values they should
-                   get if not already defined, and types they must match.
-  @cvar OP_RESULT: Callable to verify opcode result
-  @cvar WITH_LU: Boolean that specifies whether this should be included in
-      mcpu's dispatch table
-  @ivar dry_run: Whether the LU should be run in dry-run mode, i.e. just
-                 the check steps
-  @ivar priority: Opcode priority for queue
-
-  """
-  # pylint: disable=E1101
-  # as OP_ID is dynamically defined
-  WITH_LU = True
-  OP_PARAMS = [
-    ("dry_run", None, ht.TMaybeBool, "Run checks only, don't execute"),
-    ("debug_level", None, ht.TMaybe(ht.TNonNegativeInt), "Debug level"),
-    ("priority", constants.OP_PRIO_DEFAULT,
-     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; see :doc:`design document <design-chained-jobs>`"
-     " for details"),
-    (COMMENT_ATTR, None, ht.TMaybeString,
-     "Comment describing the purpose of the opcode"),
-    (constants.OPCODE_REASON, ht.EmptyList, ht.TMaybeList,
-     "The reason trail, describing why the OpCode is executed"),
-    ]
-  OP_RESULT = None
-
-  def __getstate__(self):
-    """Specialized getstate for opcodes.
-
-    This method adds to the state dictionary the OP_ID of the class,
-    so that on unload we can identify the correct class for
-    instantiating the opcode.
-
-    @rtype:   C{dict}
-    @return:  the state as a dictionary
-
-    """
-    data = BaseOpCode.__getstate__(self)
-    data["OP_ID"] = self.OP_ID
-    return data
-
-  @classmethod
-  def LoadOpCode(cls, data):
-    """Generic load opcode method.
-
-    The method identifies the correct opcode class from the dict-form
-    by looking for a OP_ID key, if this is not found, or its value is
-    not available in this module as a child of this class, we fail.
-
-    @type data:  C{dict}
-    @param data: the serialized opcode
-
-    """
-    if not isinstance(data, dict):
-      raise ValueError("Invalid data to LoadOpCode (%s)" % type(data))
-    if "OP_ID" not in data:
-      raise ValueError("Invalid data to LoadOpcode, missing OP_ID")
-    op_id = data["OP_ID"]
-    op_class = None
-    if op_id in OP_MAPPING:
-      op_class = OP_MAPPING[op_id]
-    else:
-      raise ValueError("Invalid data to LoadOpCode: OP_ID %s unsupported" %
-                       op_id)
-    op = op_class()
-    new_data = data.copy()
-    del new_data["OP_ID"]
-    op.__setstate__(new_data)
-    return op
-
-  def Summary(self):
-    """Generates a summary description of this opcode.
-
-    The summary is the value of the OP_ID attribute (without the "OP_"
-    prefix), plus the value of the OP_DSC_FIELD attribute, if one was
-    defined; this field should allow to easily identify the operation
-    (for an instance creation job, e.g., it would be the instance
-    name).
-
-    """
-    assert self.OP_ID is not None and len(self.OP_ID) > 3
-    # all OP_ID start with OP_, we remove that
-    txt = self.OP_ID[3:]
-    field_name = getattr(self, "OP_DSC_FIELD", None)
-    if field_name:
-      field_value = getattr(self, field_name, None)
-      field_formatter = getattr(self, "OP_DSC_FORMATTER", None)
-      if callable(field_formatter):
-        field_value = field_formatter(field_value)
-      elif isinstance(field_value, (list, tuple)):
-        field_value = ",".join(str(i) for i in field_value)
-      txt = "%s(%s)" % (txt, field_value)
-    return txt
-
-  def TinySummary(self):
-    """Generates a compact summary description of the opcode.
-
-    """
-    assert self.OP_ID.startswith("OP_")
-
-    text = self.OP_ID[3:]
-
-    for (prefix, supplement) in _SUMMARY_PREFIX.items():
-      if text.startswith(prefix):
-        return supplement + text[len(prefix):]
-
-    return text
-
-
-# cluster opcodes
-
-class OpClusterPostInit(OpCode):
-  """Post cluster initialization.
-
-  This opcode does not touch the cluster at all. Its purpose is to run hooks
-  after the cluster has been initialized.
-
-  """
-  OP_RESULT = ht.TBool
-
-
-class OpClusterDestroy(OpCode):
-  """Destroy the cluster.
-
-  This opcode has no other parameters. All the state is irreversibly
-  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):
-  """Submits all jobs necessary to verify the cluster.
-
-  """
-  OP_PARAMS = [
-    _PDebugSimulateErrors,
-    _PErrorCodes,
-    _PSkipChecks,
-    _PIgnoreErrors,
-    _PVerbose,
-    ("group_name", None, ht.TMaybeString, "Group to verify"),
-    ]
-  OP_RESULT = TJobIdListOnly
-
-
-class OpClusterVerifyConfig(OpCode):
-  """Verify the cluster config.
-
-  """
-  OP_PARAMS = [
-    _PDebugSimulateErrors,
-    _PErrorCodes,
-    _PIgnoreErrors,
-    _PVerbose,
-    ]
-  OP_RESULT = ht.TBool
-
-
-class OpClusterVerifyGroup(OpCode):
-  """Run verify on a node group from the cluster.
-
-  @type skip_checks: C{list}
-  @ivar skip_checks: steps to be skipped from the verify process; this
-                     needs to be a subset of
-                     L{constants.VERIFY_OPTIONAL_CHECKS}; currently
-                     only L{constants.VERIFY_NPLUSONE_MEM} can be passed
-
-  """
-  OP_DSC_FIELD = "group_name"
-  OP_PARAMS = [
-    _PGroupName,
-    _PDebugSimulateErrors,
-    _PErrorCodes,
-    _PSkipChecks,
-    _PIgnoreErrors,
-    _PVerbose,
-    ]
-  OP_RESULT = ht.TBool
-
-
-class OpClusterVerifyDisks(OpCode):
-  """Verify the cluster disks.
-
-  """
-  OP_RESULT = TJobIdListOnly
-
-
-class OpGroupVerifyDisks(OpCode):
-  """Verifies the status of all disks in a node group.
-
-  Result: a tuple of three elements:
-    - dict of node names with issues (values: error msg)
-    - list of instances with degraded disks (that should be activated)
-    - dict of instances with missing logical volumes (values: (node, vol)
-      pairs with details about the missing volumes)
-
-  In normal operation, all lists should be empty. A non-empty instance
-  list (3rd element of the result) is still ok (errors were fixed) but
-  non-empty node list means some node is down, and probably there are
-  unfixable drbd errors.
-
-  Note that only instances that are drbd-based are taken into
-  consideration. This might need to be revisited in the future.
-
-  """
-  OP_DSC_FIELD = "group_name"
-  OP_PARAMS = [
-    _PGroupName,
-    ]
-  OP_RESULT = \
-    ht.TAnd(ht.TIsLength(3),
-            ht.TItems([ht.TDictOf(ht.TString, ht.TString),
-                       ht.TListOf(ht.TString),
-                       ht.TDictOf(ht.TString,
-                                  ht.TListOf(ht.TListOf(ht.TString)))]))
-
-
-class OpClusterRepairDiskSizes(OpCode):
-  """Verify the disk sizes of the instances and fixes configuration
-  mimatches.
-
-  Parameters: optional instances list, in case we want to restrict the
-  checks to only a subset of the instances.
-
-  Result: a list of tuples, (instance, disk, parameter, new-size) for changed
-  configurations.
-
-  In normal operation, the list should be empty.
-
-  @type instances: list
-  @ivar instances: the list of instances to check, or empty for all instances
-
-  """
-  OP_PARAMS = [
-    ("instances", ht.EmptyList, ht.TListOf(ht.TNonEmptyString), None),
-    ]
-  OP_RESULT = ht.TListOf(ht.TAnd(ht.TIsLength(4),
-                                 ht.TItems([ht.TNonEmptyString,
-                                            ht.TNonNegativeInt,
-                                            ht.TNonEmptyString,
-                                            ht.TNonNegativeInt])))
-
-
-class OpClusterConfigQuery(OpCode):
-  """Query cluster configuration values."""
-  OP_PARAMS = [
-    _POutputFields,
-    ]
-  OP_RESULT = ht.TListOf(ht.TAny)
-
-
-class OpClusterRename(OpCode):
-  """Rename the cluster.
-
-  @type name: C{str}
-  @ivar name: The new name of the cluster. The name and/or the master IP
-              address will be changed to match the new name and its IP
-              address.
-
-  """
-  OP_DSC_FIELD = "name"
-  OP_PARAMS = [
-    ("name", ht.NoDefault, ht.TNonEmptyString, None),
-    ]
-  OP_RESULT = ht.TNonEmptyString
-
-
-class OpClusterSetParams(OpCode):
-  """Change the parameters of the cluster.
-
-  @type vg_name: C{str} or C{None}
-  @ivar vg_name: The new volume group name or None to disable LVM usage.
-
-  """
-  OP_PARAMS = [
-    _PForce,
-    _PHvState,
-    _PDiskState,
-    ("vg_name", None, ht.TMaybe(ht.TString), "Volume group name"),
-    ("enabled_hypervisors", None,
-     ht.TMaybe(ht.TAnd(ht.TListOf(ht.TElemOf(constants.HYPER_TYPES)),
-                       ht.TTrue)),
-     "List of enabled hypervisors"),
-    ("hvparams", None,
-     ht.TMaybe(ht.TDictOf(ht.TNonEmptyString, ht.TDict)),
-     "Cluster-wide hypervisor parameter defaults, hypervisor-dependent"),
-    ("beparams", None, ht.TMaybeDict,
-     "Cluster-wide backend parameter defaults"),
-    ("os_hvp", None, ht.TMaybe(ht.TDictOf(ht.TNonEmptyString, ht.TDict)),
-     "Cluster-wide per-OS hypervisor parameter defaults"),
-    ("osparams", None,
-     ht.TMaybe(ht.TDictOf(ht.TNonEmptyString, ht.TDict)),
-     "Cluster-wide OS parameter defaults"),
-    _PDiskParams,
-    ("candidate_pool_size", None, ht.TMaybe(ht.TPositiveInt),
-     "Master candidate pool size"),
-    ("uid_pool", None, ht.NoType,
-     "Set UID pool, must be list of lists describing UID ranges (two items,"
-     " start and end inclusive)"),
-    ("add_uids", None, ht.NoType,
-     "Extend UID pool, must be list of lists describing UID ranges (two"
-     " items, start and end inclusive) to be added"),
-    ("remove_uids", None, ht.NoType,
-     "Shrink UID pool, must be list of lists describing UID ranges (two"
-     " items, start and end inclusive) to be removed"),
-    ("maintain_node_health", None, ht.TMaybeBool,
-     "Whether to automatically maintain node health"),
-    ("prealloc_wipe_disks", None, ht.TMaybeBool,
-     "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.TMaybe(ht.TString), "DRBD helper program"),
-    ("default_iallocator", None, ht.TMaybe(ht.TString),
-     "Default iallocator for cluster"),
-    ("master_netdev", None, ht.TMaybe(ht.TString),
-     "Master network device"),
-    ("master_netmask", None, ht.TMaybe(ht.TNonNegativeInt),
-     "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)),
-    ("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)),
-    ("use_external_mip_script", None, ht.TMaybeBool,
-     "Whether to use an external master IP address setup script"),
-    ("enabled_disk_templates", None,
-     ht.TMaybe(ht.TAnd(ht.TListOf(ht.TElemOf(constants.DISK_TEMPLATES)),
-                       ht.TTrue)),
-     "List of enabled disk templates"),
-    ("modify_etc_hosts", None, ht.TMaybeBool,
-     "Whether the cluster can modify and keep in sync the /etc/hosts files"),
-    ("file_storage_dir", None, ht.TMaybe(ht.TString),
-     "Default directory for storing file-backed disks"),
-    ("shared_file_storage_dir", None, ht.TMaybe(ht.TString),
-     "Default directory for storing shared-file-backed disks"),
-    ]
-  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):
-  """Query for resources/items.
-
-  @ivar what: Resources to query for, must be one of L{constants.QR_VIA_OP}
-  @ivar fields: List of fields to retrieve
-  @ivar qfilter: Query filter
-
-  """
-  OP_DSC_FIELD = "what"
-  OP_PARAMS = [
-    _PQueryWhat,
-    _PUseLocking,
-    ("fields", ht.NoDefault, ht.TListOf(ht.TNonEmptyString),
-     "Requested fields"),
-    ("qfilter", None, ht.TMaybe(ht.TList),
-     "Query filter"),
-    ]
-  OP_RESULT = \
-    _GenerateObjectTypeCheck(objects.QueryResponse, {
-      "fields": ht.TListOf(_TQueryFieldDef),
-      "data": _TQueryResult,
-      })
-
-
-class OpQueryFields(OpCode):
-  """Query for available resource/item fields.
-
-  @ivar what: Resources to query for, must be one of L{constants.QR_VIA_OP}
-  @ivar fields: List of fields to retrieve
-
-  """
-  OP_DSC_FIELD = "what"
-  OP_PARAMS = [
-    _PQueryWhat,
-    ("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):
-  """Interact with OOB."""
-  OP_PARAMS = [
-    ("node_names", ht.EmptyList, ht.TListOf(ht.TNonEmptyString),
-     "List of node names to run the OOB command against"),
-    ("node_uuids", None, ht.TMaybeListOf(ht.TNonEmptyString),
-     "List of node UUIDs to run the OOB command against"),
-    ("command", ht.NoDefault, ht.TElemOf(constants.OOB_COMMANDS),
-     "OOB command to be run"),
-    ("timeout", constants.OOB_TIMEOUT, ht.TInt,
-     "Timeout before the OOB helper will be terminated"),
-    ("ignore_status", False, ht.TBool,
-     "Ignores the node offline status for power off"),
-    ("power_delay", constants.OOB_POWER_DELAY, ht.TNonNegativeFloat,
-     "Time in seconds to wait between powering on nodes"),
-    ]
-  # Fixme: Make it more specific with all the special cases in LUOobCommand
-  OP_RESULT = _TQueryResult
-
-
-class OpRestrictedCommand(OpCode):
-  """Runs a restricted command on node(s).
-
-  """
-  OP_PARAMS = [
-    _PUseLocking,
-    ("nodes", ht.NoDefault, ht.TListOf(ht.TNonEmptyString),
-     "Nodes on which the command should be run (at least one)"),
-    ("node_uuids", None, ht.TMaybeListOf(ht.TNonEmptyString),
-     "Node UUIDs on which the command should be run (at least one)"),
-    ("command", ht.NoDefault, ht.TNonEmptyString,
-     "Command name (no parameters)"),
-    ]
-
-  _RESULT_ITEMS = [
-    ht.Comment("success")(ht.TBool),
-    ht.Comment("output or error message")(ht.TString),
-    ]
-
-  OP_RESULT = \
-    ht.TListOf(ht.TAnd(ht.TIsLength(len(_RESULT_ITEMS)),
-                       ht.TItems(_RESULT_ITEMS)))
-
-
-# node opcodes
-
-class OpNodeRemove(OpCode):
-  """Remove a node.
-
-  @type node_name: C{str}
-  @ivar node_name: The name of the node to remove. If the node still has
-                   instances on it, the operation will fail.
-
-  """
-  OP_DSC_FIELD = "node_name"
-  OP_PARAMS = [
-    _PNodeName,
-    _PNodeUuid
-    ]
-  OP_RESULT = ht.TNone
-
-
-class OpNodeAdd(OpCode):
-  """Add a node to the cluster.
-
-  @type node_name: C{str}
-  @ivar node_name: The name of the node to add. This can be a short name,
-                   but it will be expanded to the FQDN.
-  @type primary_ip: IP address
-  @ivar primary_ip: The primary IP of the node. This will be ignored when the
-                    opcode is submitted, but will be filled during the node
-                    add (so it will be visible in the job query).
-  @type secondary_ip: IP address
-  @ivar secondary_ip: The secondary IP of the node. This needs to be passed
-                      if the cluster has been initialized in 'dual-network'
-                      mode, otherwise it must not be given.
-  @type readd: C{bool}
-  @ivar readd: Whether to re-add an existing node to the cluster. If
-               this is not passed, then the operation will abort if the node
-               name is already in the cluster; use this parameter to 'repair'
-               a node that had its configuration broken, or was reinstalled
-               without removal from the cluster.
-  @type group: C{str}
-  @ivar group: The node group to which this node will belong.
-  @type vm_capable: C{bool}
-  @ivar vm_capable: The vm_capable node attribute
-  @type master_capable: C{bool}
-  @ivar master_capable: The master_capable node attribute
-
-  """
-  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"),
-    ("group", None, ht.TMaybeString, "Initial node group"),
-    ("master_capable", None, ht.TMaybeBool,
-     "Whether node can become master or master candidate"),
-    ("vm_capable", None, ht.TMaybeBool,
-     "Whether node can host instances"),
-    ("ndparams", None, ht.TMaybeDict, "Node parameters"),
-    ]
-  OP_RESULT = ht.TNone
-
-
-class OpNodeQuery(OpCode):
-  """Compute the list of nodes."""
-  OP_PARAMS = [
-    _POutputFields,
-    _PUseLocking,
-    ("names", ht.EmptyList, ht.TListOf(ht.TNonEmptyString),
-     "Empty list to query all nodes, node names otherwise"),
-    ]
-  OP_RESULT = _TOldQueryResult
-
-
-class OpNodeQueryvols(OpCode):
-  """Get list of volumes on node."""
-  OP_PARAMS = [
-    _POutputFields,
-    ("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):
-  """Get information on storage for node(s)."""
-  OP_PARAMS = [
-    _POutputFields,
-    _PStorageType,
-    ("nodes", ht.EmptyList, ht.TListOf(ht.TNonEmptyString), "List of nodes"),
-    ("name", None, ht.TMaybeString, "Storage name"),
-    ]
-  OP_RESULT = _TOldQueryResult
-
-
-class OpNodeModifyStorage(OpCode):
-  """Modifies the properies of a storage unit"""
-  OP_DSC_FIELD = "node_name"
-  OP_PARAMS = [
-    _PNodeName,
-    _PNodeUuid,
-    _PStorageType,
-    _PStorageName,
-    ("changes", ht.NoDefault, ht.TDict, "Requested changes"),
-    ]
-  OP_RESULT = ht.TNone
-
-
-class OpRepairNodeStorage(OpCode):
-  """Repairs the volume group on a node."""
-  OP_DSC_FIELD = "node_name"
-  OP_PARAMS = [
-    _PNodeName,
-    _PNodeUuid,
-    _PStorageType,
-    _PStorageName,
-    _PIgnoreConsistency,
-    ]
-  OP_RESULT = ht.TNone
-
-
-class OpNodeSetParams(OpCode):
-  """Change the parameters of a node."""
-  OP_DSC_FIELD = "node_name"
-  OP_PARAMS = [
-    _PNodeName,
-    _PNodeUuid,
-    _PForce,
-    _PHvState,
-    _PDiskState,
-    ("master_candidate", None, ht.TMaybeBool,
-     "Whether the node should become a master candidate"),
-    ("offline", None, ht.TMaybeBool,
-     "Whether the node should be marked as offline"),
-    ("drained", None, ht.TMaybeBool,
-     "Whether the node should be marked as drained"),
-    ("auto_promote", False, ht.TBool,
-     "Whether node(s) should be promoted to master candidate if necessary"),
-    ("master_capable", None, ht.TMaybeBool,
-     "Denote whether node can become master or master candidate"),
-    ("vm_capable", None, ht.TMaybeBool,
-     "Denote whether node can host instances"),
-    ("secondary_ip", None, ht.TMaybeString,
-     "Change node's secondary IP address"),
-    ("ndparams", None, ht.TMaybeDict, "Set node parameters"),
-    ("powered", None, ht.TMaybeBool,
-     "Whether the node should be marked as powered"),
-    ]
-  OP_RESULT = _TSetParamsResult
-
-
-class OpNodePowercycle(OpCode):
-  """Tries to powercycle a node."""
-  OP_DSC_FIELD = "node_name"
-  OP_PARAMS = [
-    _PNodeName,
-    _PNodeUuid,
-    _PForce,
-    ]
-  OP_RESULT = ht.TMaybeString
-
-
-class OpNodeMigrate(OpCode):
-  """Migrate all instances from a node."""
-  OP_DSC_FIELD = "node_name"
-  OP_PARAMS = [
-    _PNodeName,
-    _PNodeUuid,
-    _PMigrationMode,
-    _PMigrationLive,
-    _PMigrationTargetNode,
-    _PMigrationTargetNodeUuid,
-    _PAllowRuntimeChgs,
-    _PIgnoreIpolicy,
-    _PIAllocFromDesc("Iallocator for deciding the target node"
-                     " for shared-storage instances"),
-    ]
-  OP_RESULT = TJobIdListOnly
-
-
-class OpNodeEvacuate(OpCode):
-  """Evacuate instances off a number of nodes."""
-  OP_DSC_FIELD = "node_name"
-  OP_PARAMS = [
-    _PEarlyRelease,
-    _PNodeName,
-    _PNodeUuid,
-    ("remote_node", None, ht.TMaybeString, "New secondary node"),
-    ("remote_node_uuid", None, ht.TMaybeString, "New secondary node UUID"),
-    _PIAllocFromDesc("Iallocator for computing solution"),
-    ("mode", ht.NoDefault, ht.TElemOf(constants.NODE_EVAC_MODES),
-     "Node evacuation mode"),
-    ]
-  OP_RESULT = TJobIdListOnly
-
-
-# instance opcodes
-
-class OpInstanceCreate(OpCode):
-  """Create an instance.
-
-  @ivar instance_name: Instance name
-  @ivar mode: Instance creation mode (one of L{constants.INSTANCE_CREATE_MODES})
-  @ivar source_handshake: Signed handshake from source (remote import only)
-  @ivar source_x509_ca: Source X509 CA in PEM format (remote import only)
-  @ivar source_instance_name: Previous name of instance (remote import only)
-  @ivar source_shutdown_timeout: Shutdown timeout used for source instance
-    (remote import only)
-
-  """
-  OP_DSC_FIELD = "instance_name"
-  OP_PARAMS = [
-    _PInstanceName,
-    _PForceVariant,
-    _PWaitForSync,
-    _PNameCheck,
-    _PIgnoreIpolicy,
-    _POpportunisticLocking,
-    ("beparams", ht.EmptyDict, ht.TDict, "Backend parameters for instance"),
-    ("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"
-     " (%s)" %
-     (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, _BuildDiskTemplateCheck(True),
-     "Disk template"),
-    ("file_driver", None, ht.TMaybe(ht.TElemOf(constants.FILE_DRIVER)),
-     "Driver for file-backed disks"),
-    ("file_storage_dir", None, ht.TMaybeString,
-     "Directory for storing file-backed disks"),
-    ("hvparams", ht.EmptyDict, ht.TDict,
-     "Hypervisor parameters for instance, hypervisor-dependent"),
-    ("hypervisor", None, ht.TMaybeString, "Hypervisor"),
-    _PIAllocFromDesc("Iallocator for deciding which node(s) to use"),
-    ("identify_defaults", False, ht.TBool,
-     "Reset instance parameters to default if equal"),
-    ("ip_check", True, ht.TBool, _PIpCheckDoc),
-    ("conflicts_check", True, ht.TBool, "Check for conflicting IPs"),
-    ("mode", ht.NoDefault, ht.TElemOf(constants.INSTANCE_CREATE_MODES),
-     "Instance creation mode"),
-    ("nics", ht.NoDefault, ht.TListOf(_TestNicDef),
-     "List of NIC (network interface) definitions, for example"
-     " ``[{}, {}, {\"%s\": \"198.51.100.4\"}]``; each NIC definition can"
-     " contain the optional values %s" %
-     (constants.INIC_IP,
-      ", ".join("``%s``" % i for i in sorted(constants.INIC_PARAMS)))),
-    ("no_install", None, ht.TMaybeBool,
-     "Do not install the OS (will disable automatic start)"),
-    ("osparams", ht.EmptyDict, ht.TDict, "OS parameters for instance"),
-    ("os_type", None, ht.TMaybeString, "Operating system"),
-    ("pnode", None, ht.TMaybeString, "Primary node"),
-    ("pnode_uuid", None, ht.TMaybeString, "Primary node UUID"),
-    ("snode", None, ht.TMaybeString, "Secondary node"),
-    ("snode_uuid", None, ht.TMaybeString, "Secondary node UUID"),
-    ("source_handshake", None, ht.TMaybe(ht.TList),
-     "Signed handshake from source (remote import only)"),
-    ("source_instance_name", None, ht.TMaybeString,
-     "Source instance name (remote import only)"),
-    ("source_shutdown_timeout", constants.DEFAULT_SHUTDOWN_TIMEOUT,
-     ht.TNonNegativeInt,
-     "How long source instance was given to shut down (remote import only)"),
-    ("source_x509_ca", None, ht.TMaybeString,
-     "Source X509 CA in PEM format (remote import only)"),
-    ("src_node", None, ht.TMaybeString, "Source node for import"),
-    ("src_node_uuid", None, ht.TMaybeString, "Source node UUID for import"),
-    ("src_path", None, ht.TMaybeString, "Source directory for import"),
-    ("start", True, ht.TBool, "Whether to start instance after creation"),
-    ("tags", ht.EmptyList, ht.TListOf(ht.TNonEmptyString), "Instance tags"),
-    ]
-  OP_RESULT = ht.Comment("instance nodes")(ht.TListOf(ht.TNonEmptyString))
-
-
-class OpInstanceMultiAlloc(OpCode):
-  """Allocates multiple instances.
-
-  """
-  OP_PARAMS = [
-    _POpportunisticLocking,
-    _PIAllocFromDesc("Iallocator used to allocate all the instances"),
-    ("instances", ht.EmptyList, ht.TListOf(ht.TInstanceOf(OpInstanceCreate)),
-     "List of instance create opcodes describing the instances to allocate"),
-    ]
-  _JOB_LIST = ht.Comment("List of submitted jobs")(TJobIdList)
-  ALLOCATABLE_KEY = "allocatable"
-  FAILED_KEY = "allocatable"
-  OP_RESULT = ht.TStrictDict(True, True, {
-    constants.JOB_IDS_KEY: _JOB_LIST,
-    ALLOCATABLE_KEY: ht.TListOf(ht.TNonEmptyString),
-    FAILED_KEY: ht.TListOf(ht.TNonEmptyString),
-    })
-
-  def __getstate__(self):
-    """Generic serializer.
-
-    """
-    state = OpCode.__getstate__(self)
-    if hasattr(self, "instances"):
-      # pylint: disable=E1101
-      state["instances"] = [inst.__getstate__() for inst in self.instances]
-    return state
-
-  def __setstate__(self, state):
-    """Generic unserializer.
-
-    This method just restores from the serialized state the attributes
-    of the current instance.
-
-    @param state: the serialized opcode data
-    @type state: C{dict}
-
-    """
-    if not isinstance(state, dict):
-      raise ValueError("Invalid data to __setstate__: expected dict, got %s" %
-                       type(state))
-
-    if "instances" in state:
-      state["instances"] = map(OpCode.LoadOpCode, state["instances"])
-
-    return OpCode.__setstate__(self, state)
-
-  def Validate(self, set_defaults):
-    """Validates this opcode.
-
-    We do this recursively.
-
-    """
-    OpCode.Validate(self, set_defaults)
-
-    for inst in self.instances: # pylint: disable=E1101
-      inst.Validate(set_defaults)
-
-
-class OpInstanceReinstall(OpCode):
-  """Reinstall an instance's OS."""
-  OP_DSC_FIELD = "instance_name"
-  OP_PARAMS = [
-    _PInstanceName,
-    _PInstanceUuid,
-    _PForceVariant,
-    ("os_type", None, ht.TMaybeString, "Instance operating system"),
-    ("osparams", None, ht.TMaybeDict, "Temporary OS parameters"),
-    ]
-  OP_RESULT = ht.TNone
-
-
-class OpInstanceRemove(OpCode):
-  """Remove an instance."""
-  OP_DSC_FIELD = "instance_name"
-  OP_PARAMS = [
-    _PInstanceName,
-    _PInstanceUuid,
-    _PShutdownTimeout,
-    ("ignore_failures", False, ht.TBool,
-     "Whether to ignore failures during removal"),
-    ]
-  OP_RESULT = ht.TNone
-
-
-class OpInstanceRename(OpCode):
-  """Rename an instance."""
-  OP_PARAMS = [
-    _PInstanceName,
-    _PInstanceUuid,
-    _PNameCheck,
-    ("new_name", ht.NoDefault, ht.TNonEmptyString, "New instance name"),
-    ("ip_check", False, ht.TBool, _PIpCheckDoc),
-    ]
-  OP_RESULT = ht.Comment("New instance name")(ht.TNonEmptyString)
-
-
-class OpInstanceStartup(OpCode):
-  """Startup an instance."""
-  OP_DSC_FIELD = "instance_name"
-  OP_PARAMS = [
-    _PInstanceName,
-    _PInstanceUuid,
-    _PForce,
-    _PIgnoreOfflineNodes,
-    ("hvparams", ht.EmptyDict, ht.TDict,
-     "Temporary hypervisor parameters, hypervisor-dependent"),
-    ("beparams", ht.EmptyDict, ht.TDict, "Temporary backend parameters"),
-    _PNoRemember,
-    _PStartupPaused,
-    ]
-  OP_RESULT = ht.TNone
-
-
-class OpInstanceShutdown(OpCode):
-  """Shutdown an instance."""
-  OP_DSC_FIELD = "instance_name"
-  OP_PARAMS = [
-    _PInstanceName,
-    _PInstanceUuid,
-    _PForce,
-    _PIgnoreOfflineNodes,
-    ("timeout", constants.DEFAULT_SHUTDOWN_TIMEOUT, ht.TNonNegativeInt,
-     "How long to wait for instance to shut down"),
-    _PNoRemember,
-    ]
-  OP_RESULT = ht.TNone
-
-
-class OpInstanceReboot(OpCode):
-  """Reboot an instance."""
-  OP_DSC_FIELD = "instance_name"
-  OP_PARAMS = [
-    _PInstanceName,
-    _PInstanceUuid,
-    _PShutdownTimeout,
-    ("ignore_secondaries", False, ht.TBool,
-     "Whether to start the instance even if secondary disks are failing"),
-    ("reboot_type", ht.NoDefault, ht.TElemOf(constants.REBOOT_TYPES),
-     "How to reboot instance"),
-    ]
-  OP_RESULT = ht.TNone
-
-
-class OpInstanceReplaceDisks(OpCode):
-  """Replace the disks of an instance."""
-  OP_DSC_FIELD = "instance_name"
-  OP_PARAMS = [
-    _PInstanceName,
-    _PInstanceUuid,
-    _PEarlyRelease,
-    _PIgnoreIpolicy,
-    ("mode", ht.NoDefault, ht.TElemOf(constants.REPLACE_MODES),
-     "Replacement mode"),
-    ("disks", ht.EmptyList, ht.TListOf(ht.TNonNegativeInt),
-     "Disk indexes"),
-    ("remote_node", None, ht.TMaybeString, "New secondary node"),
-    ("remote_node_uuid", None, ht.TMaybeString, "New secondary node UUID"),
-    _PIAllocFromDesc("Iallocator for deciding new secondary node"),
-    ]
-  OP_RESULT = ht.TNone
-
-
-class OpInstanceFailover(OpCode):
-  """Failover an instance."""
-  OP_DSC_FIELD = "instance_name"
-  OP_PARAMS = [
-    _PInstanceName,
-    _PInstanceUuid,
-    _PShutdownTimeout,
-    _PIgnoreConsistency,
-    _PMigrationTargetNode,
-    _PMigrationTargetNodeUuid,
-    _PIgnoreIpolicy,
-    _PIAllocFromDesc("Iallocator for deciding the target node for"
-                     " shared-storage instances"),
-    ("cleanup", False, ht.TBool,
-     "Whether a previously failed failover should be cleaned up"),
-    ]
-  OP_RESULT = ht.TNone
-
-
-class OpInstanceMigrate(OpCode):
-  """Migrate an instance.
-
-  This migrates (without shutting down an instance) to its secondary
-  node.
-
-  @ivar instance_name: the name of the instance
-  @ivar mode: the migration mode (live, non-live or None for auto)
-
-  """
-  OP_DSC_FIELD = "instance_name"
-  OP_PARAMS = [
-    _PInstanceName,
-    _PInstanceUuid,
-    _PMigrationMode,
-    _PMigrationLive,
-    _PMigrationTargetNode,
-    _PMigrationTargetNodeUuid,
-    _PAllowRuntimeChgs,
-    _PIgnoreIpolicy,
-    ("cleanup", False, ht.TBool,
-     "Whether a previously failed migration should be cleaned up"),
-    _PIAllocFromDesc("Iallocator for deciding the target node for"
-                     " shared-storage instances"),
-    ("allow_failover", False, ht.TBool,
-     "Whether we can fallback to failover if migration is not possible"),
-    ]
-  OP_RESULT = ht.TNone
-
-
-class OpInstanceMove(OpCode):
-  """Move an instance.
-
-  This move (with shutting down an instance and data copying) to an
-  arbitrary node.
-
-  @ivar instance_name: the name of the instance
-  @ivar target_node: the destination node
-
-  """
-  OP_DSC_FIELD = "instance_name"
-  OP_PARAMS = [
-    _PInstanceName,
-    _PInstanceUuid,
-    _PShutdownTimeout,
-    _PIgnoreIpolicy,
-    ("target_node", ht.NoDefault, ht.TNonEmptyString, "Target node"),
-    ("target_node_uuid", None, ht.TMaybeString, "Target node UUID"),
-    _PIgnoreConsistency,
-    ]
-  OP_RESULT = ht.TNone
-
-
-class OpInstanceConsole(OpCode):
-  """Connect to an instance's console."""
-  OP_DSC_FIELD = "instance_name"
-  OP_PARAMS = [
-    _PInstanceName,
-    _PInstanceUuid,
-    ]
-  OP_RESULT = ht.TDict
-
-
-class OpInstanceActivateDisks(OpCode):
-  """Activate an instance's disks."""
-  OP_DSC_FIELD = "instance_name"
-  OP_PARAMS = [
-    _PInstanceName,
-    _PInstanceUuid,
-    ("ignore_size", False, ht.TBool, "Whether to ignore recorded size"),
-    _PWaitForSyncFalse,
-    ]
-  OP_RESULT = ht.TListOf(ht.TAnd(ht.TIsLength(3),
-                                 ht.TItems([ht.TNonEmptyString,
-                                            ht.TNonEmptyString,
-                                            ht.TNonEmptyString])))
-
-
-class OpInstanceDeactivateDisks(OpCode):
-  """Deactivate an instance's disks."""
-  OP_DSC_FIELD = "instance_name"
-  OP_PARAMS = [
-    _PInstanceName,
-    _PInstanceUuid,
-    _PForce,
-    ]
-  OP_RESULT = ht.TNone
-
-
-class OpInstanceRecreateDisks(OpCode):
-  """Recreate an instance's disks."""
-  _TDiskChanges = \
-    ht.TAnd(ht.TIsLength(2),
-            ht.TItems([ht.Comment("Disk index")(ht.TNonNegativeInt),
-                       ht.Comment("Parameters")(_TDiskParams)]))
-
-  OP_DSC_FIELD = "instance_name"
-  OP_PARAMS = [
-    _PInstanceName,
-    _PInstanceUuid,
-    ("disks", ht.EmptyList,
-     ht.TOr(ht.TListOf(ht.TNonNegativeInt), 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"),
-    ("node_uuids", None, ht.TMaybeListOf(ht.TNonEmptyString),
-     "New instance node UUIDs, if relocation is desired"),
-    _PIAllocFromDesc("Iallocator for deciding new nodes"),
-    ]
-  OP_RESULT = ht.TNone
-
-
-class OpInstanceQuery(OpCode):
-  """Compute the list of instances."""
-  OP_PARAMS = [
-    _POutputFields,
-    _PUseLocking,
-    ("names", ht.EmptyList, ht.TListOf(ht.TNonEmptyString),
-     "Empty list to query all instances, instance names otherwise"),
-    ]
-  OP_RESULT = _TOldQueryResult
-
-
-class OpInstanceQueryData(OpCode):
-  """Compute the run-time status of instances."""
-  OP_PARAMS = [
-    _PUseLocking,
-    ("instances", ht.EmptyList, ht.TListOf(ht.TNonEmptyString),
-     "Instance names"),
-    ("static", False, ht.TBool,
-     "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.TNonNegativeInt),
-      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("Device index, can be negative, e.g. -1 for last disk")
-                 (ht.TOr(ht.TInt, ht.TString)),
-      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.
-
-  """
-  TestNicModifications = _TestInstSetParamsModList(_TestNicDef)
-  TestDiskModifications = _TestInstSetParamsModList(_TDiskParams)
-
-  OP_DSC_FIELD = "instance_name"
-  OP_PARAMS = [
-    _PInstanceName,
-    _PInstanceUuid,
-    _PForce,
-    _PForceVariant,
-    _PIgnoreIpolicy,
-    ("nics", ht.EmptyList, TestNicModifications,
-     "List of NIC changes: each item is of the form"
-     " ``(op, identifier, settings)``, ``op`` is one of ``%s``, ``%s`` or"
-     " ``%s``, ``identifier`` can be a zero-based index number (or -1 to refer"
-     " to the last position), the NIC's UUID of the NIC's name; 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.TMaybePositiveInt, "New runtime memory"),
-    ("hvparams", ht.EmptyDict, ht.TDict,
-     "Per-instance hypervisor parameters, hypervisor-dependent"),
-    ("disk_template", None, ht.TMaybe(_BuildDiskTemplateCheck(False)),
-     "Disk template for instance"),
-    ("pnode", None, ht.TMaybeString, "New primary node"),
-    ("pnode_uuid", None, ht.TMaybeString, "New primary node UUID"),
-    ("remote_node", None, ht.TMaybeString,
-     "Secondary node (used when changing disk template)"),
-    ("remote_node_uuid", None, ht.TMaybeString,
-     "Secondary node UUID (used when changing disk template)"),
-    ("os_name", None, ht.TMaybeString,
-     "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"),
-    ("conflicts_check", True, ht.TBool, "Check for conflicting IPs"),
-    ]
-  OP_RESULT = _TSetParamsResult
-
-
-class OpInstanceGrowDisk(OpCode):
-  """Grow a disk of an instance."""
-  OP_DSC_FIELD = "instance_name"
-  OP_PARAMS = [
-    _PInstanceName,
-    _PInstanceUuid,
-    _PWaitForSync,
-    ("disk", ht.NoDefault, ht.TInt, "Disk index"),
-    ("amount", ht.NoDefault, ht.TNonNegativeInt,
-     "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):
-  """Moves an instance to another node group."""
-  OP_DSC_FIELD = "instance_name"
-  OP_PARAMS = [
-    _PInstanceName,
-    _PInstanceUuid,
-    _PEarlyRelease,
-    _PIAllocFromDesc("Iallocator for computing solution"),
-    _PTargetGroups,
-    ]
-  OP_RESULT = TJobIdListOnly
-
-
-# Node group opcodes
-
-class OpGroupAdd(OpCode):
-  """Add a node group to the cluster."""
-  OP_DSC_FIELD = "group_name"
-  OP_PARAMS = [
-    _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):
-  """Assign nodes to a node group."""
-  OP_DSC_FIELD = "group_name"
-  OP_PARAMS = [
-    _PGroupName,
-    _PForce,
-    ("nodes", ht.NoDefault, ht.TListOf(ht.TNonEmptyString),
-     "List of nodes to assign"),
-    ("node_uuids", None, ht.TMaybeListOf(ht.TNonEmptyString),
-     "List of node UUIDs to assign"),
-    ]
-  OP_RESULT = ht.TNone
-
-
-class OpGroupQuery(OpCode):
-  """Compute the list of node groups."""
-  OP_PARAMS = [
-    _POutputFields,
-    ("names", ht.EmptyList, ht.TListOf(ht.TNonEmptyString),
-     "Empty list to query all groups, group names otherwise"),
-    ]
-  OP_RESULT = _TOldQueryResult
-
-
-class OpGroupSetParams(OpCode):
-  """Change the parameters of a node group."""
-  OP_DSC_FIELD = "group_name"
-  OP_PARAMS = [
-    _PGroupName,
-    _PNodeGroupAllocPolicy,
-    _PGroupNodeParams,
-    _PDiskParams,
-    _PHvState,
-    _PDiskState,
-    ("ipolicy", None, ht.TMaybeDict, "Group-wide instance policy specs"),
-    ]
-  OP_RESULT = _TSetParamsResult
-
-
-class OpGroupRemove(OpCode):
-  """Remove a node group from the cluster."""
-  OP_DSC_FIELD = "group_name"
-  OP_PARAMS = [
-    _PGroupName,
-    ]
-  OP_RESULT = ht.TNone
-
-
-class OpGroupRename(OpCode):
-  """Rename a node group in the cluster."""
-  OP_PARAMS = [
-    _PGroupName,
-    ("new_name", ht.NoDefault, ht.TNonEmptyString, "New group name"),
-    ]
-  OP_RESULT = ht.Comment("New group name")(ht.TNonEmptyString)
-
-
-class OpGroupEvacuate(OpCode):
-  """Evacuate a node group in the cluster."""
-  OP_DSC_FIELD = "group_name"
-  OP_PARAMS = [
-    _PGroupName,
-    _PEarlyRelease,
-    _PIAllocFromDesc("Iallocator for computing solution"),
-    _PTargetGroups,
-    ]
-  OP_RESULT = TJobIdListOnly
-
-
-# OS opcodes
-class OpOsDiagnose(OpCode):
-  """Compute the list of guest operating systems."""
-  OP_PARAMS = [
-    _POutputFields,
-    ("names", ht.EmptyList, ht.TListOf(ht.TNonEmptyString),
-     "Which operating systems to diagnose"),
-    ]
-  OP_RESULT = _TOldQueryResult
-
-
-# ExtStorage opcodes
-class OpExtStorageDiagnose(OpCode):
-  """Compute the list of external storage providers."""
-  OP_PARAMS = [
-    _POutputFields,
-    ("names", ht.EmptyList, ht.TListOf(ht.TNonEmptyString),
-     "Which ExtStorage Provider to diagnose"),
-    ]
-  OP_RESULT = _TOldQueryResult
-
-
-# Exports opcodes
-class OpBackupQuery(OpCode):
-  """Compute the list of exported images."""
-  OP_PARAMS = [
-    _PUseLocking,
-    ("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):
-  """Prepares an instance export.
-
-  @ivar instance_name: Instance name
-  @ivar mode: Export mode (one of L{constants.EXPORT_MODES})
-
-  """
-  OP_DSC_FIELD = "instance_name"
-  OP_PARAMS = [
-    _PInstanceName,
-    _PInstanceUuid,
-    ("mode", ht.NoDefault, ht.TElemOf(constants.EXPORT_MODES),
-     "Export mode"),
-    ]
-  OP_RESULT = ht.TMaybeDict
-
-
-class OpBackupExport(OpCode):
-  """Export an instance.
-
-  For local exports, the export destination is the node name. For
-  remote exports, the export destination is a list of tuples, each
-  consisting of hostname/IP address, port, magic, HMAC and HMAC
-  salt. The HMAC is calculated using the cluster domain secret over
-  the value "${index}:${hostname}:${port}". The destination X509 CA
-  must be a signed certificate.
-
-  @ivar mode: Export mode (one of L{constants.EXPORT_MODES})
-  @ivar target_node: Export destination
-  @ivar x509_key_name: X509 key to use (remote export only)
-  @ivar destination_x509_ca: Destination X509 CA in PEM format (remote export
-                             only)
-
-  """
-  OP_DSC_FIELD = "instance_name"
-  OP_PARAMS = [
-    _PInstanceName,
-    _PInstanceUuid,
-    _PShutdownTimeout,
-    # TODO: Rename target_node as it changes meaning for different export modes
-    # (e.g. "destination")
-    ("target_node", ht.NoDefault, ht.TOr(ht.TNonEmptyString, ht.TList),
-     "Destination information, depends on export mode"),
-    ("target_node_uuid", None, ht.TMaybeString,
-     "Target node UUID (if local export)"),
-    ("shutdown", True, ht.TBool, "Whether to shutdown instance before export"),
-    ("remove_instance", False, ht.TBool,
-     "Whether to remove instance after export"),
-    ("ignore_remove_failures", False, ht.TBool,
-     "Whether to ignore failures while removing instances"),
-    ("mode", constants.EXPORT_MODE_LOCAL, ht.TElemOf(constants.EXPORT_MODES),
-     "Export mode"),
-    ("x509_key_name", None, ht.TMaybe(ht.TList),
-     "Name of X509 key (remote export only)"),
-    ("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):
-  """Remove an instance's export."""
-  OP_DSC_FIELD = "instance_name"
-  OP_PARAMS = [
-    _PInstanceName,
-    _PInstanceUuid,
-    ]
-  OP_RESULT = ht.TNone
-
-
-# Tags opcodes
-class OpTagsGet(OpCode):
-  """Returns the tags of the given object."""
-  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,
-     "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,
-     "Search pattern (regular expression)"),
-    ]
-  OP_RESULT = ht.TListOf(ht.TAnd(ht.TIsLength(2), ht.TItems([
-    ht.TNonEmptyString,
-    ht.TNonEmptyString,
-    ])))
-
-
-class OpTagsSet(OpCode):
-  """Add a list of tags on a given object."""
-  OP_PARAMS = [
-    _PTagKind,
-    _PTags,
-    # Name is only meaningful for groups, nodes and instances
-    ("name", ht.NoDefault, ht.TMaybeString,
-     "Name of object where tag(s) should be added"),
-    ]
-  OP_RESULT = ht.TNone
-
-
-class OpTagsDel(OpCode):
-  """Remove a list of tags from a given object."""
-  OP_PARAMS = [
-    _PTagKind,
-    _PTags,
-    # Name is only meaningful for groups, nodes and instances
-    ("name", ht.NoDefault, ht.TMaybeString,
-     "Name of object where tag(s) should be deleted"),
-    ]
-  OP_RESULT = ht.TNone
-
-
-# Test opcodes
-class OpTestDelay(OpCode):
-  """Sleeps for a configured amount of time.
-
-  This is used just for debugging and testing.
-
-  Parameters:
-    - duration: the time to sleep, in seconds
-    - on_master: if true, sleep on the master
-    - on_nodes: list of nodes in which to sleep
-
-  If the on_master parameter is true, it will execute a sleep on the
-  master (before any node sleep).
-
-  If the on_nodes list is not empty, it will sleep on those nodes
-  (after the sleep on the master, if that is enabled).
-
-  As an additional feature, the case of duration < 0 will be reported
-  as an execution error, so this opcode can be used as a failure
-  generator. The case of duration == 0 will not be treated specially.
-
-  """
-  OP_DSC_FIELD = "duration"
-  OP_PARAMS = [
-    ("duration", ht.NoDefault, ht.TNumber, None),
-    ("on_master", True, ht.TBool, None),
-    ("on_nodes", ht.EmptyList, ht.TListOf(ht.TNonEmptyString), None),
-    ("on_node_uuids", None, ht.TMaybeListOf(ht.TNonEmptyString), None),
-    ("repeat", 0, ht.TNonNegativeInt, None),
-    ]
-
-  def OP_DSC_FORMATTER(self, value): # pylint: disable=C0103,R0201
-    """Custom formatter for duration.
-
-    """
-    try:
-      v = float(value)
-    except TypeError:
-      v = value
-    return str(v)
-
-
-class OpTestAllocator(OpCode):
-  """Allocator framework testing.
-
-  This opcode has two modes:
-    - gather and return allocator input for a given mode (allocate new
-      or replace secondary) and a given instance definition (direction
-      'in')
-    - run a selected allocator for a given operation (as above) and
-      return the allocator output (direction 'out')
-
-  """
-  OP_DSC_FIELD = "iallocator"
-  OP_PARAMS = [
-    ("direction", ht.NoDefault,
-     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.TMaybeListOf(ht.TDictOf(ht.TElemOf([constants.INIC_MAC,
-                                            constants.INIC_IP,
-                                            "bridge"]),
-                                ht.TMaybeString)),
-     None),
-    ("disks", ht.NoDefault, ht.TMaybe(ht.TList), None),
-    ("hypervisor", None, ht.TMaybeString, None),
-    _PIAllocFromDesc(None),
-    ("tags", ht.EmptyList, ht.TListOf(ht.TNonEmptyString), None),
-    ("memory", None, ht.TMaybe(ht.TNonNegativeInt), None),
-    ("vcpus", None, ht.TMaybe(ht.TNonNegativeInt), None),
-    ("os", None, ht.TMaybeString, None),
-    ("disk_template", None, ht.TMaybeString, None),
-    ("instances", None, ht.TMaybeListOf(ht.TNonEmptyString), None),
-    ("evac_mode", None,
-     ht.TMaybe(ht.TElemOf(constants.IALLOCATOR_NEVAC_MODES)), None),
-    ("target_groups", None, ht.TMaybeListOf(ht.TNonEmptyString), None),
-    ("spindle_use", 1, ht.TNonNegativeInt, None),
-    ("count", 1, ht.TNonNegativeInt, None),
-    ]
-
-
-class OpTestJqueue(OpCode):
-  """Utility opcode to test some aspects of the job queue.
-
-  """
-  OP_PARAMS = [
-    ("notify_waitlock", False, ht.TBool, None),
-    ("notify_exec", False, ht.TBool, None),
-    ("log_messages", ht.EmptyList, ht.TListOf(ht.TString), None),
-    ("fail", False, ht.TBool, None),
-    ]
-
-
-class OpTestDummy(OpCode):
-  """Utility opcode used by unittests.
-
-  """
-  OP_PARAMS = [
-    ("result", ht.NoDefault, ht.NoType, None),
-    ("messages", ht.NoDefault, ht.NoType, None),
-    ("fail", ht.NoDefault, ht.NoType, None),
-    ("submit_jobs", None, ht.NoType, None),
-    ]
-  WITH_LU = False
-
-
-# Network opcodes
-# Add a new network in the cluster
-class OpNetworkAdd(OpCode):
-  """Add an IP network to the cluster."""
-  OP_DSC_FIELD = "network_name"
-  OP_PARAMS = [
-    _PNetworkName,
-    ("network", ht.NoDefault, _TIpNetwork4, "IPv4 subnet"),
-    ("gateway", None, ht.TMaybe(_TIpAddress4), "IPv4 gateway"),
-    ("network6", None, ht.TMaybe(_TIpNetwork6), "IPv6 subnet"),
-    ("gateway6", None, ht.TMaybe(_TIpAddress6), "IPv6 gateway"),
-    ("mac_prefix", None, ht.TMaybeString,
-     "MAC address prefix that overrides cluster one"),
-    ("add_reserved_ips", None, _TMaybeAddr4List,
-     "Which IP addresses to reserve"),
-    ("conflicts_check", True, ht.TBool,
-     "Whether to check for conflicting IP addresses"),
-    ("tags", ht.EmptyList, ht.TListOf(ht.TNonEmptyString), "Network tags"),
-    ]
-  OP_RESULT = ht.TNone
-
-
-class OpNetworkRemove(OpCode):
-  """Remove an existing network from the cluster.
-     Must not be connected to any nodegroup.
-
-  """
-  OP_DSC_FIELD = "network_name"
-  OP_PARAMS = [
-    _PNetworkName,
-    _PForce,
-    ]
-  OP_RESULT = ht.TNone
-
-
-class OpNetworkSetParams(OpCode):
-  """Modify Network's parameters except for IPv4 subnet"""
-  OP_DSC_FIELD = "network_name"
-  OP_PARAMS = [
-    _PNetworkName,
-    ("gateway", None, ht.TMaybeValueNone(_TIpAddress4), "IPv4 gateway"),
-    ("network6", None, ht.TMaybeValueNone(_TIpNetwork6), "IPv6 subnet"),
-    ("gateway6", None, ht.TMaybeValueNone(_TIpAddress6), "IPv6 gateway"),
-    ("mac_prefix", None, ht.TMaybeValueNone(ht.TString),
-     "MAC address prefix that overrides cluster one"),
-    ("add_reserved_ips", None, _TMaybeAddr4List,
-     "Which external IP addresses to reserve"),
-    ("remove_reserved_ips", None, _TMaybeAddr4List,
-     "Which external IP addresses to release"),
-    ]
-  OP_RESULT = ht.TNone
-
-
-class OpNetworkConnect(OpCode):
-  """Connect a Network to a specific Nodegroup with the defined netparams
-     (mode, link). Nics in this Network will inherit those params.
-     Produce errors if a NIC (that its not already assigned to a network)
-     has an IP that is contained in the Network this will produce error unless
-     --no-conflicts-check is passed.
-
-  """
-  OP_DSC_FIELD = "network_name"
-  OP_PARAMS = [
-    _PGroupName,
-    _PNetworkName,
-    ("network_mode", ht.NoDefault, ht.TElemOf(constants.NIC_VALID_MODES),
-     "Connectivity mode"),
-    ("network_link", ht.NoDefault, ht.TString, "Connectivity link"),
-    ("conflicts_check", True, ht.TBool, "Whether to check for conflicting IPs"),
-    ]
-  OP_RESULT = ht.TNone
-
-
-class OpNetworkDisconnect(OpCode):
-  """Disconnect a Network from a Nodegroup. Produce errors if NICs are
-     present in the Network unless --no-conficts-check option is passed.
-
-  """
-  OP_DSC_FIELD = "network_name"
-  OP_PARAMS = [
-    _PGroupName,
-    _PNetworkName,
-    ]
-  OP_RESULT = ht.TNone
-
-
-class OpNetworkQuery(OpCode):
-  """Compute the list of networks."""
-  OP_PARAMS = [
-    _POutputFields,
-    _PUseLocking,
-    ("names", ht.EmptyList, ht.TListOf(ht.TNonEmptyString),
-     "Empty list to query all groups, group names otherwise"),
-    ]
-  OP_RESULT = _TOldQueryResult
-
-
-def _GetOpList():
-  """Returns list of all defined opcodes.
-
-  Does not eliminate duplicates by C{OP_ID}.
-
-  """
-  return [v for v in globals().values()
-          if (isinstance(v, type) and issubclass(v, OpCode) and
-              hasattr(v, "OP_ID") and v is not OpCode)]
-
-
-OP_MAPPING = dict((v.OP_ID, v) for v in _GetOpList())
diff --git a/lib/opcodes.py.in_after b/lib/opcodes.py.in_after
new file mode 100644 (file)
index 0000000..4bb6b21
--- /dev/null
@@ -0,0 +1,15 @@
+
+
+def _GetOpList():
+  """Returns list of all defined opcodes.
+
+  Does not eliminate duplicates by C{OP_ID}.
+
+  """
+  return [v for v in globals().values()
+          if (isinstance(v, type) and issubclass(v, OpCode) and
+              hasattr(v, "OP_ID") and v is not OpCode and
+              v.OP_ID != 'OP_INSTANCE_MULTI_ALLOC_BASE')]
+
+
+OP_MAPPING = dict((v.OP_ID, v) for v in _GetOpList())
diff --git a/lib/opcodes.py.in_before b/lib/opcodes.py.in_before
new file mode 100644 (file)
index 0000000..0a4700f
--- /dev/null
@@ -0,0 +1,214 @@
+#
+#
+
+# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 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.
+
+
+"""OpCodes module
+
+Note that this file is autogenerated using @src/hs2py@ with a header
+from @lib/opcodes.py.in_before@ and a footer from @lib/opcodes.py.in_after@.
+
+This module implements part of the data structures which define the
+cluster operations - the so-called opcodes.
+
+Every operation which modifies the cluster state is expressed via
+opcodes.
+
+"""
+
+# this are practically structures, so disable the message about too
+# few public methods:
+# pylint: disable=R0903
+# pylint: disable=C0301
+
+from ganeti import constants
+from ganeti import ht
+
+from ganeti import opcodes_base
+
+
+class OpCode(opcodes_base.BaseOpCode):
+  """Abstract OpCode.
+
+  This is the root of the actual OpCode hierarchy. All clases derived
+  from this class should override OP_ID.
+
+  @cvar OP_ID: The ID of this opcode. This should be unique amongst all
+               children of this class.
+  @cvar OP_DSC_FIELD: The name of a field whose value will be included in the
+                      string returned by Summary(); see the docstring of that
+                      method for details).
+  @cvar OP_DSC_FORMATTER: A callable that should format the OP_DSC_FIELD; if
+                          not present, then the field will be simply converted
+                          to string
+  @cvar OP_PARAMS: List of opcode attributes, the default values they should
+                   get if not already defined, and types they must match.
+  @cvar OP_RESULT: Callable to verify opcode result
+  @cvar WITH_LU: Boolean that specifies whether this should be included in
+      mcpu's dispatch table
+  @ivar dry_run: Whether the LU should be run in dry-run mode, i.e. just
+                 the check steps
+  @ivar priority: Opcode priority for queue
+
+  """
+  # pylint: disable=E1101
+  # as OP_ID is dynamically defined
+  WITH_LU = True
+  OP_PARAMS = [
+    ("dry_run", None, ht.TMaybe(ht.TBool), "Run checks only, don't execute"),
+    ("debug_level", None, ht.TMaybe(ht.TNonNegative(ht.TInt)), "Debug level"),
+    ("priority", constants.OP_PRIO_DEFAULT,
+     ht.TElemOf(constants.OP_PRIO_SUBMIT_VALID), "Opcode priority"),
+    (opcodes_base.DEPEND_ATTR, None, opcodes_base.BuildJobDepCheck(True),
+     "Job dependencies; if used through ``SubmitManyJobs`` relative (negative)"
+     " job IDs can be used; see :doc:`design document <design-chained-jobs>`"
+     " for details"),
+    (opcodes_base.COMMENT_ATTR, None, ht.TMaybe(ht.TString),
+     "Comment describing the purpose of the opcode"),
+    (constants.OPCODE_REASON, [], ht.TMaybe(ht.TListOf(ht.TAny)),
+     "The reason trail, describing why the OpCode is executed"),
+    ]
+  OP_RESULT = None
+
+  def __getstate__(self):
+    """Specialized getstate for opcodes.
+
+    This method adds to the state dictionary the OP_ID of the class,
+    so that on unload we can identify the correct class for
+    instantiating the opcode.
+
+    @rtype:   C{dict}
+    @return:  the state as a dictionary
+
+    """
+    data = opcodes_base.BaseOpCode.__getstate__(self)
+    data["OP_ID"] = self.OP_ID
+    return data
+
+  @classmethod
+  def LoadOpCode(cls, data):
+    """Generic load opcode method.
+
+    The method identifies the correct opcode class from the dict-form
+    by looking for a OP_ID key, if this is not found, or its value is
+    not available in this module as a child of this class, we fail.
+
+    @type data:  C{dict}
+    @param data: the serialized opcode
+
+    """
+    if not isinstance(data, dict):
+      raise ValueError("Invalid data to LoadOpCode (%s)" % type(data))
+    if "OP_ID" not in data:
+      raise ValueError("Invalid data to LoadOpcode, missing OP_ID")
+    op_id = data["OP_ID"]
+    op_class = None
+    if op_id in OP_MAPPING:
+      op_class = OP_MAPPING[op_id]
+    else:
+      raise ValueError("Invalid data to LoadOpCode: OP_ID %s unsupported" %
+                       op_id)
+    op = op_class()
+    new_data = data.copy()
+    del new_data["OP_ID"]
+    op.__setstate__(new_data)
+    return op
+
+  def Summary(self):
+    """Generates a summary description of this opcode.
+
+    The summary is the value of the OP_ID attribute (without the "OP_"
+    prefix), plus the value of the OP_DSC_FIELD attribute, if one was
+    defined; this field should allow to easily identify the operation
+    (for an instance creation job, e.g., it would be the instance
+    name).
+
+    """
+    assert self.OP_ID is not None and len(self.OP_ID) > 3
+    # all OP_ID start with OP_, we remove that
+    txt = self.OP_ID[3:]
+    field_name = getattr(self, "OP_DSC_FIELD", None)
+    if field_name:
+      field_value = getattr(self, field_name, None)
+      field_formatter = getattr(self, "OP_DSC_FORMATTER", None)
+      if callable(field_formatter):
+        field_value = field_formatter(field_value)
+      elif isinstance(field_value, (list, tuple)):
+        field_value = ",".join(str(i) for i in field_value)
+      txt = "%s(%s)" % (txt, field_value)
+    return txt
+
+  def TinySummary(self):
+    """Generates a compact summary description of the opcode.
+
+    """
+    assert self.OP_ID.startswith("OP_")
+
+    text = self.OP_ID[3:]
+
+    for (prefix, supplement) in opcodes_base.SUMMARY_PREFIX.items():
+      if text.startswith(prefix):
+        return supplement + text[len(prefix):]
+
+    return text
+
+
+class OpInstanceMultiAllocBase(OpCode):
+  """Allocates multiple instances.
+
+  """
+  def __getstate__(self):
+    """Generic serializer.
+
+    """
+    state = OpCode.__getstate__(self)
+    if hasattr(self, "instances"):
+      # pylint: disable=E1101
+      state["instances"] = [inst.__getstate__() for inst in self.instances]
+    return state
+
+  def __setstate__(self, state):
+    """Generic unserializer.
+
+    This method just restores from the serialized state the attributes
+    of the current instance.
+
+    @param state: the serialized opcode data
+    @type state: C{dict}
+
+    """
+    if not isinstance(state, dict):
+      raise ValueError("Invalid data to __setstate__: expected dict, got %s" %
+                       type(state))
+
+    if "instances" in state:
+      state["instances"] = map(OpCode.LoadOpCode, state["instances"])
+
+    return OpCode.__setstate__(self, state)
+
+  def Validate(self, set_defaults):
+    """Validates this opcode.
+
+    We do this recursively.
+
+    """
+    OpCode.Validate(self, set_defaults)
+
+    for inst in self.instances: # pylint: disable=E1101
+      inst.Validate(set_defaults)
diff --git a/lib/opcodes_base.py b/lib/opcodes_base.py
new file mode 100644 (file)
index 0000000..556307e
--- /dev/null
@@ -0,0 +1,272 @@
+#
+#
+
+# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013 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.
+
+
+"""OpCodes base module
+
+This module implements part of the data structures which define the
+cluster operations - the so-called opcodes.
+
+Every operation which modifies the cluster state is expressed via
+opcodes.
+
+"""
+
+# this are practically structures, so disable the message about too
+# few public methods:
+# pylint: disable=R0903
+
+import copy
+import logging
+import re
+
+from ganeti import constants
+from ganeti import errors
+from ganeti import ht
+from ganeti import outils
+
+
+#: OP_ID conversion regular expression
+_OPID_RE = re.compile("([a-z])([A-Z])")
+
+SUMMARY_PREFIX = {
+  "CLUSTER_": "C_",
+  "GROUP_": "G_",
+  "NODE_": "N_",
+  "INSTANCE_": "I_",
+  }
+
+#: Attribute name for dependencies
+DEPEND_ATTR = "depends"
+
+#: Attribute name for comment
+COMMENT_ATTR = "comment"
+
+
+def _NameComponents(name):
+  """Split an opcode class name into its components
+
+  @type name: string
+  @param name: the class name, as OpXxxYyy
+  @rtype: array of strings
+  @return: the components of the name
+
+  """
+  assert name.startswith("Op")
+  # Note: (?<=[a-z])(?=[A-Z]) would be ideal, since it wouldn't
+  # consume any input, and hence we would just have all the elements
+  # in the list, one by one; but it seems that split doesn't work on
+  # non-consuming input, hence we have to process the input string a
+  # bit
+  name = _OPID_RE.sub(r"\1,\2", name)
+  elems = name.split(",")
+  return elems
+
+
+def _NameToId(name):
+  """Convert an opcode class name to an OP_ID.
+
+  @type name: string
+  @param name: the class name, as OpXxxYyy
+  @rtype: string
+  @return: the name in the OP_XXXX_YYYY format
+
+  """
+  if not name.startswith("Op"):
+    return None
+  return "_".join(n.upper() for n in _NameComponents(name))
+
+
+def NameToReasonSrc(name):
+  """Convert an opcode class name to a source string for the reason trail
+
+  @type name: string
+  @param name: the class name, as OpXxxYyy
+  @rtype: string
+  @return: the name in the OP_XXXX_YYYY format
+
+  """
+  if not name.startswith("Op"):
+    return None
+  return "%s:%s" % (constants.OPCODE_REASON_SRC_OPCODE,
+                    "_".join(n.lower() for n in _NameComponents(name)))
+
+
+class _AutoOpParamSlots(outils.AutoSlots):
+  """Meta class for opcode definitions.
+
+  """
+  def __new__(mcs, name, bases, attrs):
+    """Called when a class should be created.
+
+    @param mcs: The meta class
+    @param name: Name of created class
+    @param bases: Base classes
+    @type attrs: dict
+    @param attrs: Class attributes
+
+    """
+    assert "OP_ID" not in attrs, "Class '%s' defining OP_ID" % name
+
+    slots = mcs._GetSlots(attrs)
+    assert "OP_DSC_FIELD" not in attrs or attrs["OP_DSC_FIELD"] in slots, \
+      "Class '%s' uses unknown field in OP_DSC_FIELD" % name
+    assert ("OP_DSC_FORMATTER" not in attrs or
+            callable(attrs["OP_DSC_FORMATTER"])), \
+      ("Class '%s' uses non-callable in OP_DSC_FORMATTER (%s)" %
+       (name, type(attrs["OP_DSC_FORMATTER"])))
+
+    attrs["OP_ID"] = _NameToId(name)
+
+    return outils.AutoSlots.__new__(mcs, name, bases, attrs)
+
+  @classmethod
+  def _GetSlots(mcs, attrs):
+    """Build the slots out of OP_PARAMS.
+
+    """
+    # Always set OP_PARAMS to avoid duplicates in BaseOpCode.GetAllParams
+    params = attrs.setdefault("OP_PARAMS", [])
+
+    # Use parameter names as slots
+    return [pname for (pname, _, _, _) in params]
+
+
+class BaseOpCode(outils.ValidatedSlots):
+  """A simple serializable object.
+
+  This object serves as a parent class for OpCode without any custom
+  field handling.
+
+  """
+  # pylint: disable=E1101
+  # as OP_ID is dynamically defined
+  __metaclass__ = _AutoOpParamSlots
+
+  def __getstate__(self):
+    """Generic serializer.
+
+    This method just returns the contents of the instance as a
+    dictionary.
+
+    @rtype:  C{dict}
+    @return: the instance attributes and their values
+
+    """
+    state = {}
+    for name in self.GetAllSlots():
+      if hasattr(self, name):
+        state[name] = getattr(self, name)
+    return state
+
+  def __setstate__(self, state):
+    """Generic unserializer.
+
+    This method just restores from the serialized state the attributes
+    of the current instance.
+
+    @param state: the serialized opcode data
+    @type state:  C{dict}
+
+    """
+    if not isinstance(state, dict):
+      raise ValueError("Invalid data to __setstate__: expected dict, got %s" %
+                       type(state))
+
+    for name in self.GetAllSlots():
+      if name not in state and hasattr(self, name):
+        delattr(self, name)
+
+    for name in state:
+      setattr(self, name, state[name])
+
+  @classmethod
+  def GetAllParams(cls):
+    """Compute list of all parameters for an opcode.
+
+    """
+    slots = []
+    for parent in cls.__mro__:
+      slots.extend(getattr(parent, "OP_PARAMS", []))
+    return slots
+
+  def Validate(self, set_defaults): # pylint: disable=W0221
+    """Validate opcode parameters, optionally setting default values.
+
+    @type set_defaults: bool
+    @param set_defaults: Whether to set default values
+    @raise errors.OpPrereqError: When a parameter value doesn't match
+                                 requirements
+
+    """
+    for (attr_name, default, test, _) in self.GetAllParams():
+      assert callable(test)
+
+      if hasattr(self, attr_name):
+        attr_val = getattr(self, attr_name)
+      else:
+        attr_val = copy.deepcopy(default)
+
+      if test(attr_val):
+        if set_defaults:
+          setattr(self, attr_name, attr_val)
+      elif ht.TInt(attr_val) and test(float(attr_val)):
+        if set_defaults:
+          setattr(self, attr_name, float(attr_val))
+      else:
+        logging.error("OpCode %s, parameter %s, has invalid type %s/value"
+                      " '%s' expecting type %s",
+                      self.OP_ID, attr_name, type(attr_val), attr_val, test)
+
+        if attr_val is None:
+          logging.error("OpCode %s, parameter %s, has default value None which"
+                        " is does not check against the parameter's type: this"
+                        " means this parameter is required but no value was"
+                        " given",
+                        self.OP_ID, attr_name)
+
+        raise errors.OpPrereqError("Parameter '%s.%s' fails validation" %
+                                   (self.OP_ID, attr_name),
+                                   errors.ECODE_INVAL)
+
+
+def BuildJobDepCheck(relative):
+  """Builds check for job dependencies (L{DEPEND_ATTR}).
+
+  @type relative: bool
+  @param relative: Whether to accept relative job IDs (negative)
+  @rtype: callable
+
+  """
+  if relative:
+    job_id = ht.TOr(ht.TJobId, ht.TRelativeJobId)
+  else:
+    job_id = ht.TJobId
+
+  job_dep = \
+    ht.TAnd(ht.TOr(ht.TListOf(ht.TAny), ht.TTuple),
+            ht.TIsLength(2),
+            ht.TItems([job_id,
+                       ht.TListOf(ht.TElemOf(constants.JOBS_FINALIZED))]))
+
+  return ht.TMaybe(ht.TListOf(job_dep))
+
+
+TNoRelativeJobDependencies = BuildJobDepCheck(False)
index 735c415..756aec4 100644 (file)
@@ -23,7 +23,7 @@
 
 """
 
-from ganeti import _autoconf
+from ganeti import _constants
 from ganeti import compat
 from ganeti import vcluster
 
@@ -34,23 +34,27 @@ DEFAULT_FILE_STORAGE_DIR = vcluster.AddNodePrefix(DEFAULT_FILE_STORAGE_DIR)
 DEFAULT_SHARED_FILE_STORAGE_DIR = "/srv/ganeti/shared-file-storage"
 DEFAULT_SHARED_FILE_STORAGE_DIR = \
     vcluster.AddNodePrefix(DEFAULT_SHARED_FILE_STORAGE_DIR)
-EXPORT_DIR = vcluster.AddNodePrefix(_autoconf.EXPORT_DIR)
-OS_SEARCH_PATH = _autoconf.OS_SEARCH_PATH
-ES_SEARCH_PATH = _autoconf.ES_SEARCH_PATH
-SSH_CONFIG_DIR = _autoconf.SSH_CONFIG_DIR
-XEN_CONFIG_DIR = vcluster.AddNodePrefix(_autoconf.XEN_CONFIG_DIR)
-SYSCONFDIR = vcluster.AddNodePrefix(_autoconf.SYSCONFDIR)
-TOOLSDIR = _autoconf.TOOLSDIR
-LOCALSTATEDIR = vcluster.AddNodePrefix(_autoconf.LOCALSTATEDIR)
+EXPORT_DIR = vcluster.AddNodePrefix(_constants.EXPORT_DIR)
+OS_SEARCH_PATH = _constants.OS_SEARCH_PATH
+ES_SEARCH_PATH = _constants.ES_SEARCH_PATH
+SSH_CONFIG_DIR = _constants.SSH_CONFIG_DIR
+XEN_CONFIG_DIR = vcluster.AddNodePrefix(_constants.XEN_CONFIG_DIR)
+SYSCONFDIR = vcluster.AddNodePrefix(_constants.SYSCONFDIR)
+TOOLSDIR = _constants.TOOLSDIR
+PKGLIBDIR = _constants.PKGLIBDIR
+SHAREDIR = _constants.SHAREDIR
+LOCALSTATEDIR = vcluster.AddNodePrefix(_constants.LOCALSTATEDIR)
 
 # Paths which don't change for a virtual cluster
-DAEMON_UTIL = _autoconf.PKGLIBDIR + "/daemon-util"
-IMPORT_EXPORT_DAEMON = _autoconf.PKGLIBDIR + "/import-export"
-KVM_CONSOLE_WRAPPER = _autoconf.PKGLIBDIR + "/tools/kvm-console-wrapper"
-KVM_IFUP = _autoconf.PKGLIBDIR + "/kvm-ifup"
-PREPARE_NODE_JOIN = _autoconf.PKGLIBDIR + "/prepare-node-join"
-NODE_DAEMON_SETUP = _autoconf.PKGLIBDIR + "/node-daemon-setup"
-XEN_CONSOLE_WRAPPER = _autoconf.PKGLIBDIR + "/tools/xen-console-wrapper"
+DAEMON_UTIL = _constants.PKGLIBDIR + "/daemon-util"
+IMPORT_EXPORT_DAEMON = _constants.PKGLIBDIR + "/import-export"
+KVM_CONSOLE_WRAPPER = _constants.PKGLIBDIR + "/tools/kvm-console-wrapper"
+KVM_IFUP = _constants.PKGLIBDIR + "/kvm-ifup"
+PREPARE_NODE_JOIN = _constants.PKGLIBDIR + "/prepare-node-join"
+NODE_DAEMON_SETUP = _constants.PKGLIBDIR + "/node-daemon-setup"
+XEN_CONSOLE_WRAPPER = _constants.PKGLIBDIR + "/tools/xen-console-wrapper"
+CFGUPGRADE = _constants.PKGLIBDIR + "/tools/cfgupgrade"
+ENSURE_DIRS = _constants.PKGLIBDIR + "/ensure-dirs"
 ETC_HOSTS = vcluster.ETC_HOSTS
 
 # Top-level paths
@@ -62,10 +66,10 @@ RUN_DIR = LOCALSTATEDIR + "/run/ganeti"
 #: Script to configure master IP address
 DEFAULT_MASTER_SETUP_SCRIPT = TOOLSDIR + "/master-ip-setup"
 
-SSH_HOST_DSA_PRIV = SSH_CONFIG_DIR + "/ssh_host_dsa_key"
-SSH_HOST_DSA_PUB = SSH_HOST_DSA_PRIV + ".pub"
-SSH_HOST_RSA_PRIV = SSH_CONFIG_DIR + "/ssh_host_rsa_key"
-SSH_HOST_RSA_PUB = SSH_HOST_RSA_PRIV + ".pub"
+SSH_HOST_DSA_PRIV = _constants.SSH_HOST_DSA_PRIV
+SSH_HOST_DSA_PUB = _constants.SSH_HOST_DSA_PUB
+SSH_HOST_RSA_PRIV = _constants.SSH_HOST_RSA_PRIV
+SSH_HOST_RSA_PUB = _constants.SSH_HOST_RSA_PUB
 
 BDEV_CACHE_DIR = RUN_DIR + "/bdev-cache"
 DISK_LINKS_DIR = RUN_DIR + "/instance-disks"
@@ -89,6 +93,7 @@ CLUSTER_DOMAIN_SECRET_FILE = DATA_DIR + "/cluster-domain-secret"
 SSH_KNOWN_HOSTS_FILE = DATA_DIR + "/known_hosts"
 RAPI_USERS_FILE = DATA_DIR + "/rapi/users"
 QUEUE_DIR = DATA_DIR + "/queue"
+INTENT_TO_UPGRADE = DATA_DIR + "/intent-to-upgrade"
 CONF_DIR = SYSCONFDIR + "/ganeti"
 USER_SCRIPTS_DIR = CONF_DIR + "/scripts"
 VNC_PASSWORD_FILE = CONF_DIR + "/vnc-cluster-password"
index c934ba2..60d7efa 100644 (file)
@@ -126,7 +126,7 @@ def RunWithRPC(fn):
   return wrapper
 
 
-def _Compress(data):
+def _Compress(_, data):
   """Compresses a string for transport over RPC.
 
   Small amounts of data are not compressed.
@@ -460,14 +460,14 @@ class _RpcClientBase:
     self._encoder = compat.partial(self._EncodeArg, encoder_fn)
 
   @staticmethod
-  def _EncodeArg(encoder_fn, (argkind, value)):
+  def _EncodeArg(encoder_fn, node, (argkind, value)):
     """Encode argument.
 
     """
     if argkind is None:
       return value
     else:
-      return encoder_fn(argkind)(value)
+      return encoder_fn(argkind)(node, value)
 
   def _Call(self, cdef, node_list, args):
     """Entry point for automatically generated RPC wrappers.
@@ -489,18 +489,16 @@ class _RpcClientBase:
     if len(args) != len(argdefs):
       raise errors.ProgrammerError("Number of passed arguments doesn't match")
 
-    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)
+      prep_fn = lambda _, args: args
+    assert callable(prep_fn)
+
+    # encode the arguments for each node individually, pass them and the node
+    # name to the prep_fn, and serialise its return value
+    encode_args_fn = lambda node: map(compat.partial(self._encoder, node),
+                                      zip(map(compat.snd, argdefs), args))
+    pnbody = dict((n, serializer.DumpJson(prep_fn(n, encode_args_fn(n))))
+                  for n in node_list)
 
     result = self._proc(node_list, procedure, pnbody, read_timeout,
                         req_resolver_opts)
@@ -512,7 +510,7 @@ class _RpcClientBase:
       return result
 
 
-def _ObjectToDict(value):
+def _ObjectToDict(_, value):
   """Converts an object to a dictionary.
 
   @note: See L{objects}.
@@ -521,27 +519,19 @@ def _ObjectToDict(value):
   return value.ToDict()
 
 
-def _ObjectListToDict(value):
+def _ObjectListToDict(node, value):
   """Converts a list of L{objects} to dictionaries.
 
   """
-  return map(_ObjectToDict, value)
-
-
-def _EncodeNodeToDiskDict(value):
-  """Encodes a dictionary with node name as key and disk objects as values.
-
-  """
-  return dict((name, _ObjectListToDict(disks))
-              for name, disks in value.items())
+  return map(compat.partial(_ObjectToDict, node), value)
 
 
-def _PrepareFileUpload(getents_fn, filename):
+def _PrepareFileUpload(getents_fn, node, filename):
   """Loads a file and prepares it for an upload to nodes.
 
   """
   statcb = utils.FileStatHelper()
-  data = _Compress(utils.ReadFile(filename, preread=statcb))
+  data = _Compress(node, utils.ReadFile(filename, preread=statcb))
   st = statcb.st
 
   if getents_fn is None:
@@ -555,7 +545,7 @@ def _PrepareFileUpload(getents_fn, filename):
           getents.LookupGid(st.st_gid), st.st_atime, st.st_mtime]
 
 
-def _PrepareFinalizeExportDisks(snap_disks):
+def _PrepareFinalizeExportDisks(_, snap_disks):
   """Encodes disks for finalizing export.
 
   """
@@ -570,22 +560,7 @@ def _PrepareFinalizeExportDisks(snap_disks):
   return flat_disks
 
 
-def _EncodeImportExportIO((ieio, ieioargs)):
-  """Encodes import/export I/O information.
-
-  """
-  if ieio == constants.IEIO_RAW_DISK:
-    assert len(ieioargs) == 1
-    return (ieio, (ieioargs[0].ToDict(), ))
-
-  if ieio == constants.IEIO_SCRIPT:
-    assert len(ieioargs) == 2
-    return (ieio, (ieioargs[0].ToDict(), ieioargs[1]))
-
-  return (ieio, ieioargs)
-
-
-def _EncodeBlockdevRename(value):
+def _EncodeBlockdevRename(_, value):
   """Encodes information for renaming block devices.
 
   """
@@ -611,49 +586,43 @@ def _AddSpindlesToLegacyNodeInfo(result, space_info):
     result["spindles_free"] = lvm_pv_info["storage_free"]
     result["spindles_total"] = lvm_pv_info["storage_size"]
   else:
-    raise errors.OpExecError("No spindle storage information available.")
+    result["spindles_free"] = 0
+    result["spindles_total"] = 0
 
 
-def _AddDefaultStorageInfoToLegacyNodeInfo(result, space_info):
-  """Extracts the storage space information of the default storage type from
+def _AddStorageInfoToLegacyNodeInfoByTemplate(
+    result, space_info, disk_template):
+  """Extracts the storage space information of the disk template from
   the space info and adds it to the result dictionary.
 
   @see: C{_AddSpindlesToLegacyNodeInfo} for parameter information.
 
   """
-  # Check if there is at least one row for non-spindle storage info.
-  no_defaults = (len(space_info) < 1) or \
-      (space_info[0]["type"] == constants.ST_LVM_PV and len(space_info) == 1)
-
-  default_space_info = None
-  if no_defaults:
-    logging.warning("No storage info provided for default storage type.")
+  if utils.storage.DiskTemplateSupportsSpaceReporting(disk_template):
+    disk_info = utils.storage.LookupSpaceInfoByDiskTemplate(
+        space_info, disk_template)
+    result["name"] = disk_info["name"]
+    result["storage_free"] = disk_info["storage_free"]
+    result["storage_size"] = disk_info["storage_size"]
   else:
-    default_space_info = space_info[0]
-
-  if default_space_info:
-    result["name"] = default_space_info["name"]
-    result["storage_free"] = default_space_info["storage_free"]
-    result["storage_size"] = default_space_info["storage_size"]
+    # FIXME: consider displaying '-' in this case
+    result["storage_free"] = 0
+    result["storage_size"] = 0
 
 
-def MakeLegacyNodeInfo(data, require_spindles=False):
+def MakeLegacyNodeInfo(data, disk_template):
   """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.
 
-  @param require_spindles: add spindle storage information to the legacy node
-      info
-
   """
   (bootid, space_info, (hv_info, )) = data
 
   ret = utils.JoinDisjointDicts(hv_info, {"bootid": bootid})
 
-  if require_spindles:
-    _AddSpindlesToLegacyNodeInfo(ret, space_info)
-  _AddDefaultStorageInfoToLegacyNodeInfo(ret, space_info)
+  _AddSpindlesToLegacyNodeInfo(ret, space_info)
+  _AddStorageInfoToLegacyNodeInfoByTemplate(ret, space_info, disk_template)
 
   return ret
 
@@ -683,25 +652,26 @@ def _AnnotateDParamsGeneric(disk, (params, )):
   return disk
 
 
-def AnnotateDiskParams(template, disks, disk_params):
+def AnnotateDiskParams(disks, disk_params):
   """Annotates the disk objects with the disk parameters.
 
-  @param template: The disk template used
   @param disks: The list of disks objects to annotate
-  @param disk_params: The disk paramaters for annotation
+  @param disk_params: The disk parameters for annotation
   @returns: A list of disk objects annotated
 
   """
-  ld_params = objects.Disk.ComputeLDParams(template, disk_params)
+  def AnnotateDisk(disk):
+    if disk.dev_type == constants.DT_DISKLESS:
+      return disk
 
-  if template == constants.DT_DRBD8:
-    annotation_fn = _AnnotateDParamsDRBD
-  elif template == constants.DT_DISKLESS:
-    annotation_fn = lambda disk, _: disk
-  else:
-    annotation_fn = _AnnotateDParamsGeneric
+    ld_params = objects.Disk.ComputeLDParams(disk.dev_type, disk_params)
 
-  return [annotation_fn(disk.Copy(), ld_params) for disk in disks]
+    if disk.dev_type == constants.DT_DRBD8:
+      return _AnnotateDParamsDRBD(disk, ld_params)
+    else:
+      return _AnnotateDParamsGeneric(disk, ld_params)
+
+  return [AnnotateDisk(disk.Copy()) for disk in disks]
 
 
 def _GetExclusiveStorageFlag(cfg, node_uuid):
@@ -730,8 +700,10 @@ def _AddExclusiveStorageFlagToLvmStorageUnits(storage_units, es_flag):
   """
   result = []
   for (storage_type, storage_key) in storage_units:
-    if storage_type in [constants.ST_LVM_VG, constants.ST_LVM_PV]:
+    if storage_type in [constants.ST_LVM_VG]:
       result.append((storage_type, storage_key, [es_flag]))
+      if es_flag:
+        result.append((constants.ST_LVM_PV, storage_key, [es_flag]))
     else:
       result.append((storage_type, storage_key, []))
   return result
@@ -785,10 +757,8 @@ def PrepareStorageUnitsForNodes(cfg, storage_units, node_uuids):
 _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,
   }
 
@@ -820,14 +790,18 @@ class RpcRunner(_RpcClientBase,
       rpc_defs.ED_INST_DICT_HVP_BEP_DP: self._InstDictHvpBepDp,
       rpc_defs.ED_INST_DICT_OSP_DP: self._InstDictOspDp,
       rpc_defs.ED_NIC_DICT: self._NicDict,
+      rpc_defs.ED_DEVICE_DICT: self._DeviceDict,
 
       # Encoders annotating disk parameters
       rpc_defs.ED_DISKS_DICT_DP: self._DisksDictDP,
       rpc_defs.ED_MULTI_DISKS_DICT_DP: self._MultiDiskDictDP,
       rpc_defs.ED_SINGLE_DISK_DICT_DP: self._SingleDiskDictDP,
+      rpc_defs.ED_NODE_TO_DISK_DICT_DP: self._EncodeNodeToDiskDictDP,
 
       # Encoders with special requirements
       rpc_defs.ED_FILE_DETAILS: compat.partial(_PrepareFileUpload, _getents),
+
+      rpc_defs.ED_IMPEXP_IO: self._EncodeImportExportIO,
       })
 
     # Resolver using configuration
@@ -846,7 +820,7 @@ class RpcRunner(_RpcClientBase,
     _generated_rpc.RpcClientDnsOnly.__init__(self)
     _generated_rpc.RpcClientDefault.__init__(self)
 
-  def _NicDict(self, nic):
+  def _NicDict(self, _, nic):
     """Convert the given nic to a dict and encapsulate netinfo
 
     """
@@ -858,7 +832,13 @@ class RpcRunner(_RpcClientBase,
         n.netinfo = objects.Network.ToDict(nobj)
     return n.ToDict()
 
-  def _InstDict(self, instance, hvp=None, bep=None, osp=None):
+  def _DeviceDict(self, _, (device, instance)):
+    if isinstance(device, objects.NIC):
+      return self._NicDict(None, device)
+    elif isinstance(device, objects.Disk):
+      return self._SingleDiskDictDP(None, (device, instance))
+
+  def _InstDict(self, node, instance, hvp=None, bep=None, osp=None):
     """Convert the given instance to a dict.
 
     This is done via the instance's ToDict() method and additionally
@@ -888,7 +868,7 @@ class RpcRunner(_RpcClientBase,
     idict["osparams"] = cluster.SimpleFillOS(instance.os, instance.osparams)
     if osp is not None:
       idict["osparams"].update(osp)
-    idict["disks"] = self._DisksDictDP((instance.disks, instance))
+    idict["disks"] = self._DisksDictDP(node, (instance.disks, instance))
     for nic in idict["nics"]:
       nic["nicparams"] = objects.FillDict(
         cluster.nicparams[constants.PP_DEFAULT],
@@ -901,42 +881,71 @@ class RpcRunner(_RpcClientBase,
           nic["netinfo"] = objects.Network.ToDict(nobj)
     return idict
 
-  def _InstDictHvpBepDp(self, (instance, hvp, bep)):
+  def _InstDictHvpBepDp(self, node, (instance, hvp, bep)):
     """Wrapper for L{_InstDict}.
 
     """
-    return self._InstDict(instance, hvp=hvp, bep=bep)
+    return self._InstDict(node, instance, hvp=hvp, bep=bep)
 
-  def _InstDictOspDp(self, (instance, osparams)):
+  def _InstDictOspDp(self, node, (instance, osparams)):
     """Wrapper for L{_InstDict}.
 
     """
-    return self._InstDict(instance, osp=osparams)
+    return self._InstDict(node, instance, osp=osparams)
 
-  def _DisksDictDP(self, (disks, instance)):
+  def _DisksDictDP(self, node, (disks, instance)):
     """Wrapper for L{AnnotateDiskParams}.
 
     """
     diskparams = self._cfg.GetInstanceDiskParams(instance)
-    return [disk.ToDict()
-            for disk in AnnotateDiskParams(instance.disk_template,
-                                           disks, diskparams)]
+    ret = []
+    for disk in AnnotateDiskParams(disks, diskparams):
+      disk_node_uuids = disk.GetNodes(instance.primary_node)
+      node_ips = dict((uuid, node.secondary_ip) for (uuid, node)
+                      in self._cfg.GetMultiNodeInfo(disk_node_uuids))
 
-  def _MultiDiskDictDP(self, disks_insts):
+      disk.UpdateDynamicDiskParams(node, node_ips)
+
+      ret.append(disk.ToDict(include_dynamic_params=True))
+
+    return ret
+
+  def _MultiDiskDictDP(self, node, disks_insts):
     """Wrapper for L{AnnotateDiskParams}.
 
     Supports a list of (disk, instance) tuples.
     """
     return [disk for disk_inst in disks_insts
-            for disk in self._DisksDictDP(disk_inst)]
+            for disk in self._DisksDictDP(node, disk_inst)]
 
-  def _SingleDiskDictDP(self, (disk, instance)):
+  def _SingleDiskDictDP(self, node, (disk, instance)):
     """Wrapper for L{AnnotateDiskParams}.
 
     """
-    (anno_disk,) = self._DisksDictDP(([disk], instance))
+    (anno_disk,) = self._DisksDictDP(node, ([disk], instance))
     return anno_disk
 
+  def _EncodeNodeToDiskDictDP(self, node, value):
+    """Encode dict of node name -> list of (disk, instance) tuples as values.
+
+    """
+    return dict((name, [self._SingleDiskDictDP(node, disk) for disk in disks])
+                for name, disks in value.items())
+
+  def _EncodeImportExportIO(self, node, (ieio, ieioargs)):
+    """Encodes import/export I/O information.
+
+    """
+    if ieio == constants.IEIO_RAW_DISK:
+      assert len(ieioargs) == 1
+      return (ieio, (self._SingleDiskDictDP(node, ieioargs[0]), ))
+
+    if ieio == constants.IEIO_SCRIPT:
+      assert len(ieioargs) == 2
+      return (ieio, (self._SingleDiskDictDP(node, ieioargs[0]), ieioargs[1]))
+
+    return (ieio, ieioargs)
+
 
 class JobQueueRunner(_RpcClientBase, _generated_rpc.RpcClientJobQueue):
   """RPC wrappers for job queue.
index 95fd9e1..bbea012 100644 (file)
@@ -62,7 +62,7 @@ ACCEPT_OFFLINE_NODE = object()
  ED_OBJECT_DICT_LIST,
  ED_INST_DICT,
  ED_INST_DICT_HVP_BEP_DP,
- ED_NODE_TO_DISK_DICT,
+ ED_NODE_TO_DISK_DICT_DP,
  ED_INST_DICT_OSP_DP,
  ED_IMPEXP_IO,
  ED_FILE_DETAILS,
@@ -72,7 +72,8 @@ ACCEPT_OFFLINE_NODE = object()
  ED_DISKS_DICT_DP,
  ED_MULTI_DISKS_DICT_DP,
  ED_SINGLE_DISK_DICT_DP,
- ED_NIC_DICT) = range(1, 16)
+ ED_NIC_DICT,
+ ED_DEVICE_DICT) = range(1, 17)
 
 
 def _Prepare(calls):
@@ -143,11 +144,6 @@ def _NodeInfoPreProc(node, args):
     return args
 
 
-def _DrbdCallsPreProc(node, args):
-  """Add the target node UUID as additional field for DRBD related calls."""
-  return args + [node]
-
-
 def _OsGetPostProc(result):
   """Post-processor for L{rpc.RpcRunner.call_os_get}.
 
@@ -296,6 +292,14 @@ _INSTANCE_CALLS = [
     ("reinstall", None, None),
     ("debug", None, None),
     ], None, None, "Starts an instance"),
+  ("hotplug_device", SINGLE, None, constants.RPC_TMO_NORMAL, [
+    ("instance", ED_INST_DICT, "Instance object"),
+    ("action", None, "Hotplug Action"),
+    ("dev_type", None, "Device type"),
+    ("device", ED_DEVICE_DICT, "Device dict"),
+    ("extra", None, "Extra info for device (dev_path for disk)"),
+    ("seq", None, "Device seq"),
+    ], None, None, "Hoplug a device to a running instance"),
   ]
 
 _IMPEXP_CALLS = [
@@ -351,7 +355,7 @@ _BLOCKDEV_CALLS = [
     ], None, None,
    "Gets the sizes of requested block devices present on a node"),
   ("blockdev_create", SINGLE, None, constants.RPC_TMO_NORMAL, [
-    ("bdev", ED_OBJECT_DICT, None),
+    ("bdev", ED_SINGLE_DISK_DICT_DP, None),
     ("size", None, None),
     ("owner", None, None),
     ("on_primary", None, None),
@@ -365,7 +369,7 @@ _BLOCKDEV_CALLS = [
     ], None, None,
     "Request wipe at given offset with given size of a block device"),
   ("blockdev_remove", SINGLE, None, constants.RPC_TMO_NORMAL, [
-    ("bdev", ED_OBJECT_DICT, None),
+    ("bdev", ED_SINGLE_DISK_DICT_DP, None),
     ], None, None, "Request removal of a given block device"),
   ("blockdev_pause_resume_sync", SINGLE, None, constants.RPC_TMO_NORMAL, [
     ("disks", ED_DISKS_DICT_DP, None),
@@ -382,41 +386,37 @@ _BLOCKDEV_CALLS = [
     ], None, None, "Request shutdown of a given block device"),
   ("blockdev_addchildren", SINGLE, None, constants.RPC_TMO_NORMAL, [
     ("bdev", ED_SINGLE_DISK_DICT_DP, None),
-    ("ndevs", ED_OBJECT_DICT_LIST, None),
+    ("ndevs", ED_DISKS_DICT_DP, None),
     ], None, None,
    "Request adding a list of children to a (mirroring) device"),
   ("blockdev_removechildren", SINGLE, None, constants.RPC_TMO_NORMAL, [
-    ("bdev", ED_OBJECT_DICT, None),
-    ("ndevs", ED_OBJECT_DICT_LIST, None),
+    ("bdev", ED_SINGLE_DISK_DICT_DP, None),
+    ("ndevs", ED_DISKS_DICT_DP, None),
     ], None, None,
    "Request removing a list of children from a (mirroring) device"),
   ("blockdev_close", SINGLE, None, constants.RPC_TMO_NORMAL, [
     ("instance_name", None, None),
-    ("disks", ED_OBJECT_DICT_LIST, None),
+    ("disks", ED_DISKS_DICT_DP, None),
     ], None, None, "Closes the given block devices"),
   ("blockdev_getdimensions", SINGLE, None, constants.RPC_TMO_NORMAL, [
-    ("disks", ED_OBJECT_DICT_LIST, None),
+    ("disks", ED_MULTI_DISKS_DICT_DP, None),
     ], None, None, "Returns size and spindles of the given disks"),
   ("drbd_disconnect_net", MULTI, None, constants.RPC_TMO_NORMAL, [
-    ("nodes_ip", None, None),
-    ("disks", ED_OBJECT_DICT_LIST, None),
-    ], _DrbdCallsPreProc, None,
+    ("disks", ED_DISKS_DICT_DP, None),
+    ], None, None,
    "Disconnects the network of the given drbd devices"),
   ("drbd_attach_net", MULTI, None, constants.RPC_TMO_NORMAL, [
-    ("nodes_ip", None, None),
     ("disks", ED_DISKS_DICT_DP, None),
     ("instance_name", None, None),
     ("multimaster", None, None),
-    ], _DrbdCallsPreProc, None, "Connects the given DRBD devices"),
+    ], None, None, "Connects the given DRBD devices"),
   ("drbd_wait_sync", MULTI, None, constants.RPC_TMO_SLOW, [
-    ("nodes_ip", None, None),
     ("disks", ED_DISKS_DICT_DP, None),
-    ], _DrbdCallsPreProc, None,
+    ], None, None,
    "Waits for the synchronization of drbd devices is complete"),
   ("drbd_needs_activation", SINGLE, None, constants.RPC_TMO_NORMAL, [
-    ("nodes_ip", None, None),
     ("disks", ED_MULTI_DISKS_DICT_DP, None),
-    ], _DrbdCallsPreProc, None,
+    ], None, None,
    "Returns the drbd disks which need activation"),
   ("blockdev_grow", SINGLE, None, constants.RPC_TMO_NORMAL, [
     ("cf_bdev", ED_SINGLE_DISK_DICT_DP, None),
@@ -439,7 +439,7 @@ _BLOCKDEV_CALLS = [
     ("devlist", ED_BLOCKDEV_RENAME, None),
     ], None, None, "Request rename of the given block devices"),
   ("blockdev_find", SINGLE, None, constants.RPC_TMO_NORMAL, [
-    ("disk", ED_OBJECT_DICT, None),
+    ("disk", ED_SINGLE_DISK_DICT_DP, None),
     ], None, _BlockdevFindPostProc,
     "Request identification of a given block device"),
   ("blockdev_getmirrorstatus", SINGLE, None, constants.RPC_TMO_NORMAL, [
@@ -447,12 +447,12 @@ _BLOCKDEV_CALLS = [
     ], None, _BlockdevGetMirrorStatusPostProc,
     "Request status of a (mirroring) device"),
   ("blockdev_getmirrorstatus_multi", MULTI, None, constants.RPC_TMO_NORMAL, [
-    ("node_disks", ED_NODE_TO_DISK_DICT, None),
+    ("node_disks", ED_NODE_TO_DISK_DICT_DP, None),
     ], _BlockdevGetMirrorStatusMultiPreProc,
    _BlockdevGetMirrorStatusMultiPostProc,
     "Request status of (mirroring) devices from multiple nodes"),
   ("blockdev_setinfo", SINGLE, None, constants.RPC_TMO_NORMAL, [
-    ("disk", ED_OBJECT_DICT, None),
+    ("disk", ED_SINGLE_DISK_DICT_DP, None),
     ("info", None, None),
     ], None, None, "Sets metadata information on a given block device"),
   ]
@@ -501,6 +501,10 @@ _NODE_CALLS = [
     ("hypervisor", None, "Hypervisor type"),
     ("hvparams", None, "Hypervisor parameters"),
     ], None, None, "Tries to powercycle a node"),
+  ("node_configure_ovs", SINGLE, None, constants.RPC_TMO_NORMAL, [
+    ("ovs_name", None, "Name of the OpenvSwitch to create"),
+    ("ovs_link", None, "Link of the OpenvSwitch to the outside"),
+    ], None, None, "This will create and setup the OpenvSwitch"),
   ]
 
 _MISC_CALLS = [
index a41b203..1130e90 100644 (file)
@@ -61,6 +61,8 @@ from ganeti import runtime
 from ganeti import pathutils
 from ganeti import ht
 
+from ganeti.utils import version
+
 
 CLIENT_REQUEST_WORKERS = 16
 
@@ -90,7 +92,7 @@ class ClientRequestWorker(workerpool.BaseWorker):
     client_ops = ClientOps(server)
 
     try:
-      (method, args, version) = luxi.ParseRequest(message)
+      (method, args, ver) = luxi.ParseRequest(message)
     except luxi.ProtocolError, err:
       logging.error("Protocol Error: %s", err)
       client.close_log()
@@ -99,9 +101,9 @@ class ClientRequestWorker(workerpool.BaseWorker):
     success = False
     try:
       # Verify client's version if there was one in the request
-      if version is not None and version != constants.LUXI_VERSION:
+      if ver is not None and ver != constants.LUXI_VERSION:
         raise errors.LuxiError("LUXI version mismatch, server %s, request %s" %
-                               (constants.LUXI_VERSION, version))
+                               (constants.LUXI_VERSION, ver))
 
       result = client_ops.handle_request(method, args)
       success = True
@@ -293,6 +295,14 @@ class ClientOps:
       _LogNewJob(True, job_id, ops)
       return job_id
 
+    elif method == luxi.REQ_SUBMIT_JOB_TO_DRAINED_QUEUE:
+      logging.info("Forcefully receiving new job")
+      (job_def, ) = args
+      ops = [opcodes.OpCode.LoadOpCode(state) for state in job_def]
+      job_id = queue.SubmitJobToDrainedQueue(ops)
+      _LogNewJob(True, job_id, ops)
+      return job_id
+
     elif method == luxi.REQ_SUBMIT_MANY_JOBS:
       logging.info("Receiving multiple jobs")
       (job_defs, ) = args
@@ -685,8 +695,8 @@ def CheckMasterd(options, args):
   try:
     config.ConfigWriter()
   except errors.ConfigVersionMismatch, err:
-    v1 = "%s.%s.%s" % constants.SplitVersion(err.args[0])
-    v2 = "%s.%s.%s" % constants.SplitVersion(err.args[1])
+    v1 = "%s.%s.%s" % version.SplitVersion(err.args[0])
+    v2 = "%s.%s.%s" % version.SplitVersion(err.args[1])
     print >> sys.stderr,  \
         ("Configuration version mismatch. The current Ganeti software"
          " expects version %s, but the on-disk configuration file has"
index 4266e86..9969b00 100644 (file)
@@ -415,9 +415,9 @@ class NodeRequestHandler(http.server.HttpServerHandler):
     disk list must all be drbd devices.
 
     """
-    nodes_ip, disks, target_node_uuid = params
-    disks = [objects.Disk.FromDict(cf) for cf in disks]
-    return backend.DrbdDisconnectNet(target_node_uuid, nodes_ip, disks)
+    (disks,) = params
+    disks = [objects.Disk.FromDict(disk) for disk in disks]
+    return backend.DrbdDisconnectNet(disks)
 
   @staticmethod
   def perspective_drbd_attach_net(params):
@@ -427,10 +427,9 @@ class NodeRequestHandler(http.server.HttpServerHandler):
     disk list must all be drbd devices.
 
     """
-    nodes_ip, disks, instance_name, multimaster, target_node_uuid = params
-    disks = [objects.Disk.FromDict(cf) for cf in disks]
-    return backend.DrbdAttachNet(target_node_uuid, nodes_ip, disks,
-                                 instance_name, multimaster)
+    disks, instance_name, multimaster = params
+    disks = [objects.Disk.FromDict(disk) for disk in disks]
+    return backend.DrbdAttachNet(disks, instance_name, multimaster)
 
   @staticmethod
   def perspective_drbd_wait_sync(params):
@@ -440,9 +439,9 @@ class NodeRequestHandler(http.server.HttpServerHandler):
     disk list must all be drbd devices.
 
     """
-    nodes_ip, disks, target_node_uuid = params
-    disks = [objects.Disk.FromDict(cf) for cf in disks]
-    return backend.DrbdWaitSync(target_node_uuid, nodes_ip, disks)
+    (disks,) = params
+    disks = [objects.Disk.FromDict(disk) for disk in disks]
+    return backend.DrbdWaitSync(disks)
 
   @staticmethod
   def perspective_drbd_needs_activation(params):
@@ -452,12 +451,12 @@ class NodeRequestHandler(http.server.HttpServerHandler):
     disk list must all be drbd devices.
 
     """
-    nodes_ip, disks, target_node_uuid = params
-    disks = [objects.Disk.FromDict(cf) for cf in disks]
-    return backend.DrbdNeedsActivation(target_node_uuid, nodes_ip, disks)
+    (disks,) = params
+    disks = [objects.Disk.FromDict(disk) for disk in disks]
+    return backend.DrbdNeedsActivation(disks)
 
   @staticmethod
-  def perspective_drbd_helper(params):
+  def perspective_drbd_helper(_):
     """Query drbd helper.
 
     """
@@ -617,6 +616,21 @@ class NodeRequestHandler(http.server.HttpServerHandler):
     return backend.StartInstance(instance, startup_paused, trail)
 
   @staticmethod
+  def perspective_hotplug_device(params):
+    """Hotplugs device to a running instance.
+
+    """
+    (idict, action, dev_type, ddict, extra, seq) = params
+    instance = objects.Instance.FromDict(idict)
+    if dev_type == constants.HOTPLUG_TARGET_DISK:
+      device = objects.Disk.FromDict(ddict)
+    elif dev_type == constants.HOTPLUG_TARGET_NIC:
+      device = objects.NIC.FromDict(ddict)
+    else:
+      assert dev_type in constants.HOTPLUG_ALL_TARGETS
+    return backend.HotplugDevice(instance, action, dev_type, device, extra, seq)
+
+  @staticmethod
   def perspective_migration_info(params):
     """Gather information about an instance to be migrated.
 
@@ -825,12 +839,20 @@ class NodeRequestHandler(http.server.HttpServerHandler):
 
   @staticmethod
   def perspective_node_powercycle(params):
-    """Tries to powercycle the nod.
+    """Tries to powercycle the node.
 
     """
     (hypervisor_type, hvparams) = params
     return backend.PowercycleNode(hypervisor_type, hvparams)
 
+  @staticmethod
+  def perspective_node_configure_ovs(params):
+    """Sets up OpenvSwitch on the node.
+
+    """
+    (ovs_name, ovs_link) = params
+    return backend.ConfigureOVS(ovs_name, ovs_link)
+
   # cluster --------------------------
 
   @staticmethod
index 11af2a2..a027ab5 100644 (file)
@@ -273,6 +273,14 @@ class SimpleStore(object):
     nl = data.splitlines(False)
     return nl
 
+  def GetOnlineNodeList(self):
+    """Return the list of online cluster nodes.
+
+    """
+    data = self._ReadFile(constants.SS_ONLINE_NODES)
+    nl = data.splitlines(False)
+    return nl
+
   def GetNodePrimaryIPList(self):
     """Return the list of cluster nodes' primary IP.
 
index 8769530..f9c4631 100644 (file)
@@ -72,7 +72,7 @@ class BlockDev(object):
   after assembly we'll have our correct major/minor.
 
   """
-  def __init__(self, unique_id, children, size, params):
+  def __init__(self, unique_id, children, size, params, dyn_params):
     self._children = children
     self.dev_path = None
     self.unique_id = unique_id
@@ -81,6 +81,7 @@ class BlockDev(object):
     self.attached = False
     self.size = size
     self.params = params
+    self.dyn_params = dyn_params
 
   def Assemble(self):
     """Assemble the device from its components.
@@ -109,7 +110,8 @@ class BlockDev(object):
     raise NotImplementedError
 
   @classmethod
-  def Create(cls, unique_id, children, size, spindles, params, excl_stor):
+  def Create(cls, unique_id, children, size, spindles, params, excl_stor,
+             dyn_params):
     """Create the device.
 
     If the device cannot be created, it will return None
@@ -132,6 +134,9 @@ class BlockDev(object):
     @param params: device-specific options/parameters
     @type excl_stor: bool
     @param excl_stor: whether exclusive_storage is active
+    @type dyn_params: dict
+    @param dyn_params: dynamic parameters of the disk only valid for this node.
+        As set by L{objects.Disk.UpdateDynamicDiskParams}.
     @rtype: L{BlockDev}
     @return: the created device, or C{None} in case of an error
 
@@ -354,6 +359,18 @@ class BlockDev(object):
     """
     return (self.GetActualSize(), self.GetActualSpindles())
 
+  def GetUserspaceAccessUri(self, hypervisor):
+    """Return URIs hypervisors can use to access disks in userspace mode.
+
+    @rtype: string
+    @return: userspace device URI
+    @raise errors.BlockDeviceError: if userspace access is not supported
+
+    """
+    ThrowError("Userspace access with %s block device and %s hypervisor is not "
+               "supported." % (self.__class__.__name__,
+                               hypervisor))
+
   def __repr__(self):
     return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
             (self.__class__, self.unique_id, self._children,
index 005731a..0f6a2cf 100644 (file)
@@ -67,13 +67,14 @@ class LogicalVolume(base.BlockDev):
   _INVALID_NAMES = compat.UniqueFrozenset([".", "..", "snapshot", "pvmove"])
   _INVALID_SUBSTRINGS = compat.UniqueFrozenset(["_mlog", "_mimage"])
 
-  def __init__(self, unique_id, children, size, params):
+  def __init__(self, unique_id, children, size, params, dyn_params):
     """Attaches to a LV device.
 
     The unique_id is a tuple (vg_name, lv_name)
 
     """
-    super(LogicalVolume, self).__init__(unique_id, children, size, params)
+    super(LogicalVolume, self).__init__(unique_id, children, size, params,
+                                        dyn_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
@@ -123,7 +124,8 @@ class LogicalVolume(base.BlockDev):
     return map((lambda pv: pv.name), empty_pvs)
 
   @classmethod
-  def Create(cls, unique_id, children, size, spindles, params, excl_stor):
+  def Create(cls, unique_id, children, size, spindles, params, excl_stor,
+             dyn_params):
     """Create a new logical volume.
 
     """
@@ -205,7 +207,7 @@ class LogicalVolume(base.BlockDev):
     if result.failed:
       base.ThrowError("LV create failed (%s): %s",
                       result.fail_reason, result.output)
-    return LogicalVolume(unique_id, children, size, params)
+    return LogicalVolume(unique_id, children, size, params, dyn_params)
 
   @staticmethod
   def _GetVolumeInfo(lvm_cmd, fields):
@@ -597,7 +599,8 @@ class LogicalVolume(base.BlockDev):
     snap_name = self._lv_name + ".snap"
 
     # remove existing snapshot if found
-    snap = LogicalVolume((self._vg_name, snap_name), None, size, self.params)
+    snap = LogicalVolume((self._vg_name, snap_name), None, size, self.params,
+                         self.dyn_params)
     base.IgnoreError(snap.Remove)
 
     vg_info = self.GetVGInfo([self._vg_name], False)
@@ -717,13 +720,14 @@ class FileStorage(base.BlockDev):
   The unique_id for the file device is a (file_driver, file_path) tuple.
 
   """
-  def __init__(self, unique_id, children, size, params):
+  def __init__(self, unique_id, children, size, params, dyn_params):
     """Initalizes a file device backend.
 
     """
     if children:
       raise errors.BlockDeviceError("Invalid setup for file device")
-    super(FileStorage, self).__init__(unique_id, children, size, params)
+    super(FileStorage, self).__init__(unique_id, children, size, params,
+                                      dyn_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]
@@ -836,7 +840,8 @@ class FileStorage(base.BlockDev):
       base.ThrowError("Can't stat %s: %s", self.dev_path, err)
 
   @classmethod
-  def Create(cls, unique_id, children, size, spindles, params, excl_stor):
+  def Create(cls, unique_id, children, size, spindles, params, excl_stor,
+             dyn_params):
     """Create a new file.
 
     @param size: the size of file in MiB
@@ -865,7 +870,7 @@ class FileStorage(base.BlockDev):
         base.ThrowError("File already existing: %s", dev_path)
       base.ThrowError("Error in file creation: %", str(err))
 
-    return FileStorage(unique_id, children, size, params)
+    return FileStorage(unique_id, children, size, params, dyn_params)
 
 
 class PersistentBlockDevice(base.BlockDev):
@@ -878,14 +883,14 @@ class PersistentBlockDevice(base.BlockDev):
   For the time being, pathnames are required to lie under /dev.
 
   """
-  def __init__(self, unique_id, children, size, params):
+  def __init__(self, unique_id, children, size, params, dyn_params):
     """Attaches to a static block device.
 
     The unique_id is a path under /dev.
 
     """
     super(PersistentBlockDevice, self).__init__(unique_id, children, size,
-                                                params)
+                                                params, dyn_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]
@@ -904,7 +909,8 @@ class PersistentBlockDevice(base.BlockDev):
     self.Attach()
 
   @classmethod
-  def Create(cls, unique_id, children, size, spindles, params, excl_stor):
+  def Create(cls, unique_id, children, size, spindles, params, excl_stor,
+             dyn_params):
     """Create a new device
 
     This is a noop, we only return a PersistentBlockDevice instance
@@ -913,7 +919,7 @@ class PersistentBlockDevice(base.BlockDev):
     if excl_stor:
       raise errors.ProgrammerError("Persistent block device requested with"
                                    " exclusive_storage")
-    return PersistentBlockDevice(unique_id, children, 0, params)
+    return PersistentBlockDevice(unique_id, children, 0, params, dyn_params)
 
   def Remove(self):
     """Remove a device
@@ -990,21 +996,24 @@ class RADOSBlockDevice(base.BlockDev):
   this to be functional.
 
   """
-  def __init__(self, unique_id, children, size, params):
+  def __init__(self, unique_id, children, size, params, dyn_params):
     """Attaches to an rbd device.
 
     """
-    super(RADOSBlockDevice, self).__init__(unique_id, children, size, params)
+    super(RADOSBlockDevice, self).__init__(unique_id, children, size, params,
+                                           dyn_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.rbd_pool = params[constants.LDP_POOL]
 
     self.major = self.minor = None
     self.Attach()
 
   @classmethod
-  def Create(cls, unique_id, children, size, spindles, params, excl_stor):
+  def Create(cls, unique_id, children, size, spindles, params, excl_stor,
+             dyn_params):
     """Create a new rbd device.
 
     Provision a new rbd volume inside a RADOS pool.
@@ -1027,7 +1036,7 @@ class RADOSBlockDevice(base.BlockDev):
       base.ThrowError("rbd creation failed (%s): %s",
                       result.fail_reason, result.output)
 
-    return RADOSBlockDevice(unique_id, children, size, params)
+    return RADOSBlockDevice(unique_id, children, size, params, dyn_params)
 
   def Remove(self):
     """Remove the rbd device.
@@ -1335,6 +1344,18 @@ class RADOSBlockDevice(base.BlockDev):
       base.ThrowError("rbd resize failed (%s): %s",
                       result.fail_reason, result.output)
 
+  def GetUserspaceAccessUri(self, hypervisor):
+    """Generate KVM userspace URIs to be used as `-drive file` settings.
+
+    @see: L{BlockDev.GetUserspaceAccessUri}
+
+    """
+    if hypervisor == constants.HT_KVM:
+      return "rbd:" + self.rbd_pool + "/" + self.rbd_name
+    else:
+      base.ThrowError("Hypervisor %s doesn't support RBD userspace access" %
+                      hypervisor)
+
 
 class ExtStorageDevice(base.BlockDev):
   """A block device provided by an ExtStorage Provider.
@@ -1343,11 +1364,12 @@ class ExtStorageDevice(base.BlockDev):
   handling of the externally provided block devices.
 
   """
-  def __init__(self, unique_id, children, size, params):
+  def __init__(self, unique_id, children, size, params, dyn_params):
     """Attaches to an extstorage block device.
 
     """
-    super(ExtStorageDevice, self).__init__(unique_id, children, size, params)
+    super(ExtStorageDevice, self).__init__(unique_id, children, size, params,
+                                           dyn_params)
     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
       raise ValueError("Invalid configuration data %s" % str(unique_id))
 
@@ -1358,7 +1380,8 @@ class ExtStorageDevice(base.BlockDev):
     self.Attach()
 
   @classmethod
-  def Create(cls, unique_id, children, size, spindles, params, excl_stor):
+  def Create(cls, unique_id, children, size, spindles, params, excl_stor,
+             dyn_params):
     """Create a new extstorage device.
 
     Provision a new volume using an extstorage provider, which will
@@ -1377,7 +1400,7 @@ class ExtStorageDevice(base.BlockDev):
     _ExtStorageAction(constants.ES_ACTION_CREATE, unique_id,
                       params, str(size))
 
-    return ExtStorageDevice(unique_id, children, size, params)
+    return ExtStorageDevice(unique_id, children, size, params, dyn_params)
 
   def Remove(self):
     """Remove the extstorage device.
@@ -1757,8 +1780,8 @@ def FindDevice(disk, children):
 
   """
   _VerifyDiskType(disk.dev_type)
-  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
-                                  disk.params)
+  device = DEV_MAP[disk.dev_type](disk.logical_id, children, disk.size,
+                                  disk.params, disk.dynamic_params)
   if not device.attached:
     return None
   return device
@@ -1779,8 +1802,8 @@ def Assemble(disk, children):
   """
   _VerifyDiskType(disk.dev_type)
   _VerifyDiskParams(disk)
-  device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
-                                  disk.params)
+  device = DEV_MAP[disk.dev_type](disk.logical_id, children, disk.size,
+                                  disk.params, disk.dynamic_params)
   device.Assemble()
   return device
 
@@ -1801,6 +1824,7 @@ def Create(disk, children, excl_stor):
   """
   _VerifyDiskType(disk.dev_type)
   _VerifyDiskParams(disk)
-  device = DEV_MAP[disk.dev_type].Create(disk.physical_id, children, disk.size,
-                                         disk.spindles, disk.params, excl_stor)
+  device = DEV_MAP[disk.dev_type].Create(disk.logical_id, children, disk.size,
+                                         disk.spindles, disk.params, excl_stor,
+                                         disk.dynamic_params)
   return device
index a9598c5..5642f29 100644 (file)
@@ -163,10 +163,10 @@ class DRBD8Dev(base.BlockDev):
   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 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.
+  The unique_id for the drbd device is a (pnode_uuid, snode_uuid,
+  port, pnode_minor, lnode_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.
 
   """
   _DRBD_MAJOR = 147
@@ -174,21 +174,32 @@ class DRBD8Dev(base.BlockDev):
   # timeout constants
   _NET_RECONFIG_TIMEOUT = 60
 
-  def __init__(self, unique_id, children, size, params):
+  def __init__(self, unique_id, children, size, params, dyn_params):
     if children and children.count(None) > 0:
       children = []
     if len(children) not in (0, 2):
       raise ValueError("Invalid configuration data %s" % str(children))
     if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
       raise ValueError("Invalid configuration data %s" % str(unique_id))
-    (self._lhost, self._lport,
-     self._rhost, self._rport,
-     self._aminor, self._secret) = unique_id
+    if constants.DDP_LOCAL_IP not in dyn_params or \
+       constants.DDP_REMOTE_IP not in dyn_params or \
+       constants.DDP_LOCAL_MINOR not in dyn_params or \
+       constants.DDP_REMOTE_MINOR not in dyn_params:
+      raise ValueError("Invalid dynamic parameters %s" % str(dyn_params))
+
+    self._lhost = dyn_params[constants.DDP_LOCAL_IP]
+    self._lport = unique_id[2]
+    self._rhost = dyn_params[constants.DDP_REMOTE_IP]
+    self._rport = unique_id[2]
+    self._aminor = dyn_params[constants.DDP_LOCAL_MINOR]
+    self._secret = unique_id[5]
+
     if children:
       if not _CanReadDevice(children[1].dev_path):
         logging.info("drbd%s: Ignoring unreadable meta device", self._aminor)
         children = []
-    super(DRBD8Dev, self).__init__(unique_id, children, size, params)
+    super(DRBD8Dev, self).__init__(unique_id, children, size, params,
+                                   dyn_params)
     self.major = self._DRBD_MAJOR
 
     info = DRBD8.GetProcInfo()
@@ -207,8 +218,8 @@ class DRBD8Dev(base.BlockDev):
 
     if (self._lhost is not None and self._lhost == self._rhost and
             self._lport == self._rport):
-      raise ValueError("Invalid configuration data, same local/remote %s" %
-                       (unique_id,))
+      raise ValueError("Invalid configuration data, same local/remote %s, %s" %
+                       (unique_id, dyn_params))
     self.Attach()
 
   @staticmethod
@@ -1005,7 +1016,8 @@ class DRBD8Dev(base.BlockDev):
       base.ThrowError("Can't initialize meta device: %s", result.output)
 
   @classmethod
-  def Create(cls, unique_id, children, size, spindles, params, excl_stor):
+  def Create(cls, unique_id, children, size, spindles, params, excl_stor,
+             dyn_params):
     """Create a new DRBD8 device.
 
     Since DRBD devices are not created per se, just assembled, this
@@ -1017,8 +1029,11 @@ class DRBD8Dev(base.BlockDev):
     if excl_stor:
       raise errors.ProgrammerError("DRBD device requested with"
                                    " exclusive_storage")
+    if constants.DDP_LOCAL_MINOR not in dyn_params:
+      raise errors.ProgrammerError("Invalid dynamic params for drbd device %s"
+                                   % dyn_params)
     # check that the minor is unused
-    aminor = unique_id[4]
+    aminor = dyn_params[constants.DDP_LOCAL_MINOR]
 
     info = DRBD8.GetProcInfo()
     if info.HasMinorStatus(aminor):
@@ -1036,7 +1051,7 @@ class DRBD8Dev(base.BlockDev):
                       aminor, meta)
     cls._CheckMetaSize(meta.dev_path)
     cls._InitMeta(aminor, meta.dev_path)
-    return cls(unique_id, children, size, params)
+    return cls(unique_id, children, size, params, dyn_params)
 
 
 def _CanReadDevice(path):
index 3390179..fcab352 100644 (file)
@@ -141,6 +141,11 @@ class DRBD8Status(object): # pylint: disable=R0902
         self.sync_percent = None
       self.est_time = None
 
+  def __repr__(self):
+    return ("<%s: cstatus=%s, lrole=%s, rrole=%s, ldisk=%s, rdisk=%s>" %
+            (self.__class__, self.cstatus, self.lrole, self.rrole,
+             self.ldisk, self.rdisk))
+
 
 class DRBD8Info(object):
   """Represents information DRBD exports (usually via /proc/drbd).
index 8ca7a88..276c6fc 100644 (file)
@@ -56,6 +56,7 @@ from ganeti.utils.retry import *
 from ganeti.utils.storage import *
 from ganeti.utils.text import *
 from ganeti.utils.wrapper import *
+from ganeti.utils.version import *
 from ganeti.utils.x509 import *
 
 
@@ -98,6 +99,11 @@ def ForceDictType(target, key_types, allowed_values=None):
 
     if ktype in (constants.VTYPE_STRING, constants.VTYPE_MAYBE_STRING):
       if target[key] is None and ktype == constants.VTYPE_MAYBE_STRING:
+        msg = ("'None' is not a valid Maybe value for '%s'. "
+               "Use 'VALUE_HS_NOTHING'") % (key, )
+        logging.warning(msg)
+      elif (target[key] == constants.VALUE_HS_NOTHING
+            and ktype == constants.VTYPE_MAYBE_STRING):
         pass
       elif not isinstance(target[key], basestring):
         if isinstance(target[key], bool) and not target[key]:
index 273c286..7cba91f 100644 (file)
@@ -34,11 +34,6 @@ def GetDiskTemplatesOfStorageType(storage_type):
           if constants.MAP_DISK_TEMPLATE_STORAGE_TYPE[dt] == storage_type]
 
 
-def GetLvmDiskTemplates():
-  """Returns all disk templates that use LVM."""
-  return GetDiskTemplatesOfStorageType(constants.ST_LVM_VG)
-
-
 def IsDiskTemplateEnabled(disk_template, enabled_disk_templates):
   """Checks if a particular disk template is enabled.
 
@@ -62,8 +57,7 @@ def IsSharedFileStorageEnabled(enabled_disk_templates):
 
 def IsLvmEnabled(enabled_disk_templates):
   """Check whether or not any lvm-based disk templates are enabled."""
-  return len(set(GetLvmDiskTemplates())
-             .intersection(set(enabled_disk_templates))) != 0
+  return len(constants.DTS_LVM & set(enabled_disk_templates)) != 0
 
 
 def LvmGetsEnabled(enabled_disk_templates, new_enabled_disk_templates):
@@ -73,8 +67,7 @@ def LvmGetsEnabled(enabled_disk_templates, new_enabled_disk_templates):
   """
   if IsLvmEnabled(enabled_disk_templates):
     return False
-  return set(GetLvmDiskTemplates()).intersection(
-      set(new_enabled_disk_templates))
+  return len(constants.DTS_LVM & set(new_enabled_disk_templates)) != 0
 
 
 def _GetDefaultStorageUnitForDiskTemplate(cfg, disk_template):
@@ -92,7 +85,7 @@ def _GetDefaultStorageUnitForDiskTemplate(cfg, disk_template):
   """
   storage_type = constants.MAP_DISK_TEMPLATE_STORAGE_TYPE[disk_template]
   cluster = cfg.GetClusterInfo()
-  if disk_template in GetLvmDiskTemplates():
+  if disk_template in constants.DTS_LVM:
     return (storage_type, cfg.GetVGName())
   elif disk_template == constants.DT_FILE:
     return (storage_type, cluster.file_storage_dir)
@@ -102,28 +95,23 @@ def _GetDefaultStorageUnitForDiskTemplate(cfg, disk_template):
     return (storage_type, None)
 
 
-def _GetDefaultStorageUnitForSpindles(cfg):
-  """Creates a 'spindle' storage unit, by retrieving the volume group
-  name and associating it to the lvm-pv storage type.
+def DiskTemplateSupportsSpaceReporting(disk_template):
+  """Check whether the disk template supports storage space reporting."""
+  return (constants.MAP_DISK_TEMPLATE_STORAGE_TYPE[disk_template]
+          in constants.STS_REPORT)
 
-  @rtype: (string, string)
-  @return: tuple (storage_type, storage_key), where storage type is
-    'lvm-pv' and storage_key the name of the default volume group
 
-  """
-  return (constants.ST_LVM_PV, cfg.GetVGName())
+def GetStorageUnits(cfg, disk_templates):
+  """Get the cluster's storage units for the given disk templates.
 
-
-def GetStorageUnitsOfCluster(cfg, include_spindles=False):
-  """Examines the cluster's configuration and returns a list of storage
-  units and their storage keys, ordered by the order in which they
-  are enabled.
+  If any lvm-based disk template is requested, spindle information
+  is added to the request.
 
   @type cfg: L{config.ConfigWriter}
   @param cfg: Cluster configuration
-  @type include_spindles: boolean
-  @param include_spindles: flag to include an extra storage unit for physical
-    volumes
+  @type disk_templates: list of string
+  @param disk_templates: list of disk templates for which the storage
+    units will be computed
   @rtype: list of tuples (string, string)
   @return: list of storage units, each storage unit being a tuple of
     (storage_type, storage_key); storage_type is in
@@ -132,22 +120,30 @@ def GetStorageUnitsOfCluster(cfg, include_spindles=False):
     name for LVM storage or a file for file storage.
 
   """
-  cluster_config = cfg.GetClusterInfo()
   storage_units = []
-  for disk_template in cluster_config.enabled_disk_templates:
-    if constants.MAP_DISK_TEMPLATE_STORAGE_TYPE[disk_template]\
-        in constants.STS_REPORT:
+  for disk_template in disk_templates:
+    if DiskTemplateSupportsSpaceReporting(disk_template):
       storage_units.append(
           _GetDefaultStorageUnitForDiskTemplate(cfg, disk_template))
-  if include_spindles:
-    included_storage_types = set([st for (st, _) in storage_units])
-    if not constants.ST_LVM_PV in included_storage_types:
-      storage_units.append(
-          _GetDefaultStorageUnitForSpindles(cfg))
-
   return storage_units
 
 
+def LookupSpaceInfoByDiskTemplate(storage_space_info, disk_template):
+  """Looks up the storage space info for a given disk template.
+
+  @type storage_space_info: list of dicts
+  @param storage_space_info: result of C{GetNodeInfo}
+  @type disk_template: string
+  @param disk_template: disk template to get storage space info
+  @rtype: tuple
+  @return: returns the element of storage_space_info that matches the given
+    disk template
+
+  """
+  storage_type = constants.MAP_DISK_TEMPLATE_STORAGE_TYPE[disk_template]
+  return LookupSpaceInfoByStorageType(storage_space_info, storage_type)
+
+
 def LookupSpaceInfoByStorageType(storage_space_info, storage_type):
   """Looks up the storage space info for a given storage type.
 
index e768588..a69af17 100644 (file)
@@ -453,6 +453,19 @@ def UnescapeAndSplit(text, sep=","):
   return rlist
 
 
+def EscapeAndJoin(slist, sep=","):
+  """Encode a list in a way parsable by UnescapeAndSplit.
+
+  @type slist: list of strings
+  @param slist: the strings to be encoded
+  @rtype: string
+  @return: the encoding of the list oas a string
+
+  """
+  return sep.join([re.sub("\\" + sep, "\\\\" + sep,
+                          re.sub(r"\\", r"\\\\", v)) for v in slist])
+
+
 def CommaJoin(names):
   """Nicely join a set of identifiers.
 
diff --git a/lib/utils/version.py b/lib/utils/version.py
new file mode 100644 (file)
index 0000000..9ae4838
--- /dev/null
@@ -0,0 +1,154 @@
+#
+#
+
+# Copyright (C) 2013 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.
+
+
+"""Version utilities."""
+
+import re
+
+from ganeti import constants
+
+_FULL_VERSION_RE = re.compile(r"(\d+)\.(\d+)\.(\d+)")
+_SHORT_VERSION_RE = re.compile(r"(\d+)\.(\d+)")
+
+# The first Ganeti version that supports automatic upgrades
+FIRST_UPGRADE_VERSION = (2, 10, 0)
+
+CURRENT_VERSION = (constants.VERSION_MAJOR, constants.VERSION_MINOR,
+                   constants.VERSION_REVISION)
+
+# Format for CONFIG_VERSION:
+#   01 03 0123 = 01030123
+#   ^^ ^^ ^^^^
+#   |  |  + Configuration version/revision
+#   |  + Minor version
+#   + Major version
+#
+# It is stored as an integer. Make sure not to write an octal number.
+
+# BuildVersion and SplitVersion must be in here because we can't import other
+# modules. The cfgupgrade tool must be able to read and write version numbers
+# and thus requires these functions. To avoid code duplication, they're kept in
+# here.
+
+
+def BuildVersion(major, minor, revision):
+  """Calculates int version number from major, minor and revision numbers.
+
+  Returns: int representing version number
+
+  """
+  assert isinstance(major, int)
+  assert isinstance(minor, int)
+  assert isinstance(revision, int)
+  return (1000000 * major +
+            10000 * minor +
+                1 * revision)
+
+
+def SplitVersion(version):
+  """Splits version number stored in an int.
+
+  Returns: tuple; (major, minor, revision)
+
+  """
+  assert isinstance(version, int)
+
+  (major, remainder) = divmod(version, 1000000)
+  (minor, revision) = divmod(remainder, 10000)
+
+  return (major, minor, revision)
+
+
+def ParseVersion(versionstring):
+  """Parses a version string.
+
+  @param versionstring: the version string to parse
+  @type versionstring: string
+  @rtype: tuple or None
+  @return: (major, minor, revision) if parsable, None otherwise.
+
+  """
+  m = _FULL_VERSION_RE.match(versionstring)
+  if m is not None:
+    return (int(m.group(1)), int(m.group(2)), int(m.group(3)))
+
+  m = _SHORT_VERSION_RE.match(versionstring)
+  if m is not None:
+    return (int(m.group(1)), int(m.group(2)), 0)
+
+  return None
+
+
+def UpgradeRange(target, current=CURRENT_VERSION):
+  """Verify whether a version is within the range of automatic upgrades.
+
+  @param target: The version to upgrade to as (major, minor, revision)
+  @type target: tuple
+  @param current: The version to upgrade from as (major, minor, revision)
+  @type current: tuple
+  @rtype: string or None
+  @return: None, if within the range, and a human-readable error message
+      otherwise
+
+  """
+  if target < FIRST_UPGRADE_VERSION or current < FIRST_UPGRADE_VERSION:
+    return "automatic upgrades only supported from 2.10 onwards"
+
+  if target[0] != current[0]:
+    return "different major versions"
+
+  if target[1] < current[1] - 1:
+    return "can only downgrade one minor version at a time"
+
+  return None
+
+
+def ShouldCfgdowngrade(version, current=CURRENT_VERSION):
+  """Decide whether cfgupgrade --downgrade should be called.
+
+  Given the current version and the version to change to, decide
+  if in the transition process cfgupgrade --downgrade should
+  be called
+
+  @param version: The version to upgrade to as (major, minor, revision)
+  @type version: tuple
+  @param current: The version to upgrade from as (major, minor, revision)
+  @type current: tuple
+  @rtype: bool
+  @return: True, if cfgupgrade --downgrade should be called.
+
+  """
+  return version[0] == current[0] and version[1] == current[1] - 1
+
+
+def IsCorrectConfigVersion(targetversion, configversion):
+  """Decide whether configuration version is compatible with the target.
+
+  @param targetversion: The version to upgrade to as (major, minor, revision)
+  @type targetversion: tuple
+  @param configversion: The version of the current configuration
+  @type configversion: tuple
+  @rtype: bool
+  @return: True, if the configversion fits with the target version.
+
+  """
+  return (configversion[0] == targetversion[0] and
+          configversion[1] == targetversion[1])
index 153cd23..d6a373b 100644 (file)
@@ -124,6 +124,21 @@ exclusive_storage
     parameter cannot be set on individual nodes, as its value must be
     the same within each node group.
 
+ovs
+    When this Boolean flag is enabled, OpenvSwitch will be used as the
+    network layer. This will cause the initialization of OpenvSwitch on
+    the nodes when added to the cluster. Per default this is not enabled.
+
+ovs_name
+    When ovs is enabled, this parameter will represent the name of the
+    OpenvSwitch to generate and use. This will default to `switch1`.
+
+ovs_link
+    When ovs is enabled, a OpenvSwitch will be initialized on new nodes
+    and will have this as its connection to the outside. This parameter
+    is not set per default, as it depends very much on the specific
+    setup.
+
 
 Hypervisor State Parameters
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
index 3b0ee1c..e599bff 100644 (file)
@@ -469,6 +469,21 @@ pool
     When a new RADOS cluster is deployed, the default pool to put rbd
     volumes (Images in RADOS terminology) is 'rbd'.
 
+access
+    If 'userspace', instances will access their disks directly without
+    going through a block device, avoiding expensive context switches
+    with kernel space and the potential for deadlocks_ in low memory
+    scenarios.
+
+    The default value is 'kernelspace' and it disables this behaviour.
+    This setting may only be changed to 'userspace' if all instance
+    disks in the affected group or cluster can be accessed in userspace.
+
+    Attempts to use this feature without rbd support compiled in KVM
+    result in a "no such file or directory" error messages.
+
+.. _deadlocks: http://tracker.ceph.com/issues/3076
+
 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
@@ -788,6 +803,22 @@ When all the disk sizes are consistent, the command will return no
 output. Otherwise it will log details about the inconsistencies in
 the configuration.
 
+UPGRADE
+~~~~~~~
+
+**upgrade** {--to *version* | --resume}
+
+This command safely switches all nodes of the cluster to a new Ganeti
+version. It is a prerequisite that the new version is already installed,
+albeit not activated, on all nodes; this requisite is checked before any
+actions are done.
+
+If called with the ``--resume`` option, any pending upgrade is
+continued, that was interrupted by a power failure or similar on
+master. It will do nothing, if not run on the master node, or if no
+upgrade was in progress.
+
+
 VERIFY
 ~~~~~~
 
index 9189748..d48adb1 100644 (file)
@@ -160,6 +160,11 @@ name
    this option specifies a name for the NIC, which can be used as a NIC
    identifier. An instance can not have two NICs with the same name.
 
+vlan
+   in openvswitch mode specifies the VLANs that the NIC will be
+   connected to. To connect as an access port use ``n`` or ``.n`` with
+   **n** being the VLAN ID. To connect as an trunk port use ``:n[:n]``.
+   A hybrid port can be created with ``.n:n[:n]``
 
 Of these "mode" and "link" are NIC parameters, and inherit their
 default at cluster level.  Alternatively, if no network is desired for
@@ -718,11 +723,24 @@ cpu\_sockets
     Number of emulated CPU sockets.
 
 soundhw
-    Valid for the KVM hypervisor.
+    Valid for the KVM and XEN hypervisors.
 
     Comma separated list of emulated sounds cards, or "all" to enable
     all the available ones.
 
+cpuid
+    Valid for the XEN hypervisor.
+
+    Modify the values returned by CPUID_ instructions run within instances.
+
+    This allows you to enable migration between nodes with different CPU
+    attributes like cores, threads, hyperthreading or SS4 support by hiding
+    the extra features where needed.
+
+    See the XEN documentation for syntax and more information.
+
+.. _CPUID: http://en.wikipedia.org/wiki/CPUID
+
 usb\_devices
     Valid for the KVM hypervisor.
 
@@ -974,6 +992,14 @@ follows::
     # gnt-instance batch-create instances.json
     Submitted jobs 37, 38
 
+
+Note: If the allocator is used for computing suitable nodes for the
+instances, it will only take into account disk information for the
+default disk template. That means, even if other disk templates are
+specified for the instances, storage space information of these disk
+templates will not be considered in the allocation computation.
+
+
 REMOVE
 ^^^^^^
 
@@ -1110,6 +1136,7 @@ MODIFY
 | [\--offline \| \--online]
 | [\--submit] [\--print-job-id]
 | [\--ignore-ipolicy]
+| [\--hotplug]
 | {*instance*}
 
 Modifies the memory size, number of vcpus, ip address, MAC address
@@ -1139,14 +1166,16 @@ The ``--disk add:size=*SIZE*,[options..]`` option adds a disk to the
 instance, and ``--disk *N*:add:size=*SIZE*,[options..]`` will add a disk
 to the the instance at a specific index. The available options are the
 same as in the **add** command(``spindles``, ``mode``, ``name``, ``vg``,
-``metavg``). When adding an ExtStorage disk the ``provider=*PROVIDER*``
-option is also mandatory and specifies the ExtStorage provider. Also,
-for ExtStorage disks arbitrary parameters can be passed as additional
-comma separated options, same as in the **add** command. -The ``--disk
-remove`` option will remove the last disk of the instance. Use ``--disk
-`` *ID*``:remove`` to remove a disk by its identifier. *ID* can be the
-index of the disk, the disks's name or the disks's UUID. The ``--disk
-*ID*:modify[,options...]`` will change the options of the disk.
+``metavg``). Per default, gnt-instance waits for the disk mirror to sync.
+If you do not want this behavior, use the ``--no-wait-for-sync`` option.
+When adding an ExtStorage disk, the ``provider=*PROVIDER*`` option is
+also mandatory and specifies the ExtStorage provider. Also, for
+ExtStorage disks arbitrary parameters can be passed as additional comma
+separated options, same as in the **add** command. The ``--disk remove``
+option will remove the last disk of the instance. Use
+``--disk `` *ID*``:remove`` to remove a disk by its identifier. *ID*
+can be the index of the disk, the disks's name or the disks's UUID. The
+``--disk *ID*:modify[,options...]`` will change the options of the disk.
 Available options are:
 
 mode
@@ -1185,6 +1214,16 @@ immediately.
 If ``--ignore-ipolicy`` is given any instance policy violations occuring
 during this operation are ignored.
 
+If ``--hotplug`` is given any disk and NIC modifications will take
+effect without the need of actual reboot. Please note that this feature
+is currently supported only for KVM hypervisor and there are some
+restrictions: a) KVM versions >= 1.0 support it b) instances with chroot
+or uid pool security model do not support disk hotplug c) RBD disks with
+userspace access mode can not be hotplugged (yet) d) if hotplug fails
+(for any reason) a warning is printed but execution is continued e)
+for existing NIC modification interactive verification is needed unless
+``--force`` option is passed.
+
 See **ganeti**\(7) for a description of ``--submit`` and other common
 options.
 
@@ -1890,7 +1929,9 @@ CHANGE-GROUP
 
 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
-cluster default.
+cluster default. Note that the iallocator does only consider disk
+information of the default disk template, even if the instances'
+disk templates differ from that.
 
 If no specific destination groups are specified using ``--to``, all
 groups except the one containing the instance are considered.
index fdb4bd9..7b5efff 100644 (file)
@@ -39,7 +39,7 @@ the node in the cluster. The command needs to be run on the Ganeti
 master.
 
 Note that the command is potentially destructive, as it will
-forcibly join the specified host the cluster, not paying attention
+forcibly join the specified host to the cluster, not paying attention
 to its current status (it could be already in a cluster, etc.)
 
 The ``-s (--secondary-ip)`` is used in dual-home clusters and
@@ -48,7 +48,7 @@ discussion in **gnt-cluster**\(8) for more information.
 
 In case you're readding a node after hardware failure, you can use
 the ``--readd`` parameter. In this case, you don't need to pass the
-secondary IP again, it will reused from the cluster. Also, the
+secondary IP again, it will be reused from the cluster. Also, the
 drained and offline flags of the node will be cleared before
 re-adding it.
 
@@ -123,6 +123,10 @@ each affected instance individually:
   instance that the node is a secondary for.
 - when neither of the above is done a combination of the two cases is run
 
+Note that the iallocator currently only considers disk information of
+the default disk template, even if the instance's disk templates differ
+from that.
+
 See **ganeti**\(7) for a description of ``--submit`` and other common
 options.
 
@@ -219,6 +223,13 @@ meanings. For example, some solutions share the node memory with the
 pool of memory used for instances (KVM), whereas others have separate
 memory for the node and for the instances (Xen).
 
+Note that the field 'dtotal' and 'dfree' refer to the storage type
+that is defined by the default disk template. The default disk template
+is the first on in the list of cluster-wide enabled disk templates and
+can be set with ``gnt-cluster modify``. Currently, only the disk
+templates 'plain', 'drbd', 'file', and 'sharedfile' support storage
+reporting, for all others '0' is displayed.
+
 If exactly one argument is given and it appears to be a query filter
 (see **ganeti**\(7)), the query result is filtered accordingly. For
 ambiguous cases (e.g. a single field name as a filter) the ``--filter``
index 72733a2..c7fe773 100644 (file)
@@ -27,6 +27,10 @@ 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*.
 
+Apart from input data, hail collects data over the network from all
+MonDs with the --mond option. Currently it uses only data produced by
+the CPUload collector.
+
 ALGORITHM
 ~~~~~~~~~
 
@@ -75,6 +79,25 @@ The options that can be passed to the program are as follows:
   in the JSON request itself. This is mostly used for debugging. The
   format of the file is described in the man page **htools**\(1).
 
+\--mond
+  If given the program will query all MonDs to fetch data from the
+  supported data collectors over the network.
+
+\--mond-data *datafile*
+  The name of the file holding the data provided by MonD, to override
+  quering MonDs over the network. This is mostly used for debugging. The
+  file must be in JSON format and present an array of JSON objects ,
+  one for every node, with two members. The first member named ``node``
+  is the name of the node and the second member named ``reports`` is an
+  array of report objects. The report objects must be in the same format
+  as produced by the monitoring agent.
+
+\--ignore-dynu
+  If given, all dynamic utilisation information will be ignored by
+  assuming it to be 0. This option will take precedence over any data
+  passed by the MonDs with the ``--mond`` and the ``--mond-data``
+  option.
+
 \--simulate *description*
   Backend specification: similar to the **-t** option, this allows
   overriding the cluster data with a simulated cluster. For details
index 4b1e5ef..4ef2371 100644 (file)
@@ -30,6 +30,7 @@ Algorithm options:
 **[ \--no-disk-moves ]**
 **[ \--no-instance-moves ]**
 **[ -U *util-file* ]**
+**[ \--ignore-dynu ]**
 **[ \--evac-mode ]**
 **[ \--select-instances *inst...* ]**
 **[ \--exclude-instances *inst...* ]**
@@ -57,6 +58,10 @@ 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.
 
+The program accesses the cluster state via Rapi or Luxi. It also
+requests data over the network from all MonDs with the --mond option.
+Currently it uses only data produced by CPUload collector.
+
 By default, the program will show the solution incrementally as it is
 computed, in a somewhat cryptic format; for getting the actual Ganeti
 command list, use the **-C** option.
@@ -120,6 +125,7 @@ following components:
   primary instances of the node)
 - standard deviation of the dynamic load on the nodes, for cpus,
   memory, disk and network
+- standard deviation of the CPU load provided by MonD
 
 The free memory and free disk values help ensure that all nodes are
 somewhat balanced in their resource usage. The reserved memory helps
@@ -158,6 +164,13 @@ different), and that they are normalised to between zero and one. Note
 that it's recommended to not have zero as the load value for any
 instance metric since then secondary instances are not well balanced.
 
+The CPUload from MonD's data collector will be used only if all MonDs
+are running, otherwise it won't affect the cluster score. Since we can't
+find the CPU load of each instance, we can assume that the CPU load of
+an instance is proportional to the number of its vcpus. With this
+heuristic, instances from nodes with high CPU load will tend to move to
+nodes with less CPU load.
+
 On a perfectly balanced cluster (all nodes the same size, all
 instances the same size and spread across the nodes equally), the
 values for all metrics would be zero. This doesn't happen too often in
@@ -316,6 +329,12 @@ 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.
 
+\--ignore-dynu
+  If given, all dynamic utilisation information will be ignored by
+  assuming it to be 0. This option will take precedence over any data
+  passed by the ``-U`` option or by the MonDs with the ``--mond`` and
+  the ``--mond-data`` option.
+
 -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"
@@ -330,6 +349,19 @@ The options that can be passed to the program are as follows:
   other backends must be selected. The option is described in the man
   page **htools**\(1).
 
+\--mond
+  If given the program will query all MonDs to fetch data from the
+  supported data collectors over the network.
+
+\--mond-data *datafile*
+  The name of the file holding the data provided by MonD, to override
+  quering MonDs over the network. This is mostly used for debugging. The
+  file must be in JSON format and present an array of JSON objects ,
+  one for every node, with two members. The first member named ``node``
+  is the name of the node and the second member named ``reports`` is an
+  array of report objects. The report objects must be in the same format
+  as produced by the monitoring agent.
+
 -m *cluster*
   Backend specification: collect data directly from the *cluster* given
   as an argument via RAPI. The option is described in the man page
index ecd5ece..37591aa 100644 (file)
@@ -219,6 +219,25 @@ support all options. Some common options are:
   - vcpu ratio
   - spindle ratio
 
+\--mond
+  If given the program will query all MonDs to fetch data from the
+  supported data collectors over the network.
+
+\--mond-data *datafile*
+  The name of the file holding the data provided by MonD, to override
+  quering MonDs over the network. This is mostly used for debugging. The
+  file must be in JSON format and present an array of JSON objects ,
+  one for every node, with two members. The first member named ``node``
+  is the name of the node and the second member named ``reports`` is an
+  array of report objects. The report objects must be in the same format
+  as produced by the monitoring agent.
+
+\--ignore-dynu
+  If given, all dynamic utilisation information will be ignored by
+  assuming it to be 0. This option will take precedence over any data
+  passed by the ``-U`` option (available with hbal) or by the MonDs with
+  the ``--mond`` and the ``--mond-data`` option.
+
 -m *cluster*
   Backend specification: collect data directly from the *cluster* given
   as an argument via RAPI. If the argument doesn't contain a colon (:),
index 4fdec6e..7707163 100644 (file)
--- a/pylintrc
+++ b/pylintrc
@@ -1,6 +1,8 @@
 # Configuration file for pylint (http://www.logilab.org/project/pylint). See
 # http://www.logilab.org/card/pylintfeatures for more detailed variable
 # descriptions.
+#
+# NOTE: Keep this file in sync (as much as possible) with pylintrc-test!
 
 [MASTER]
 profile = no
diff --git a/pylintrc-test b/pylintrc-test
new file mode 100644 (file)
index 0000000..d61735a
--- /dev/null
@@ -0,0 +1,113 @@
+# pylint configuration file tailored to test code.
+#
+# NOTE: Keep in sync as much as possible with the standard pylintrc file.
+# Only a few settings had to be adapted for the test code.
+
+[MASTER]
+profile = no
+ignore =
+persistent = no
+cache-size = 50000
+load-plugins =
+
+[REPORTS]
+output-format = colorized
+include-ids = yes
+files-output = no
+reports = no
+evaluation = 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+comment = yes
+
+[BASIC]
+required-attributes =
+# disabling docstring checks since we have way too many without (complex
+# inheritance hierarchies)
+#no-docstring-rgx = __.*__
+no-docstring-rgx = .*
+module-rgx = (([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+# added lower-case names
+const-rgx = ((_{0,2}[A-Za-z][A-Za-z0-9_]*)|(__.*__))$
+class-rgx = _?[A-Z][a-zA-Z0-9]+$
+# added lower-case names
+function-rgx = (_?((([A-Z]+[a-z0-9]+)|test|assert)([A-Z]+[a-z0-9]*)*)|main|([a-z_][a-z0-9_]*))$
+# add lower-case names, since derived classes must obey method names
+method-rgx = (_{0,2}(([A-Z]+[a-z0-9]+)|test|assert)([A-Z]+[a-z0-9]*)*|__.*__|runTests|setUp|tearDown|([a-z_][a-z0-9_]*))$
+attr-rgx = [a-z_][a-z0-9_]{1,30}$
+argument-rgx = [a-z_][a-z0-9_]*$
+variable-rgx = (_?([a-z_][a-z0-9_]*)|(_?[A-Z0-9_]+))$
+inlinevar-rgx = [A-Za-z_][A-Za-z0-9_]*$
+good-names = i,j,k,_
+bad-names = foo,bar,baz,toto,tutu,tata
+bad-functions = xrange
+
+[TYPECHECK]
+ignore-mixin-members = yes
+zope = no
+acquired-members =
+
+[VARIABLES]
+init-import = no
+dummy-variables-rgx = _
+additional-builtins =
+
+[CLASSES]
+ignore-iface-methods =
+defining-attr-methods = __init__,__new__,setUp
+
+[DESIGN]
+max-args = 15
+max-locals = 50
+max-returns = 10
+max-branchs = 80
+max-statements = 200
+max-parents = 7
+max-attributes = 20
+# zero as struct-like (PODS) classes don't export any methods
+min-public-methods = 0
+max-public-methods = 50
+
+[IMPORTS]
+deprecated-modules = regsub,string,TERMIOS,Bastion,rexec
+import-graph =
+ext-import-graph =
+int-import-graph =
+
+[FORMAT]
+max-line-length = 80
+max-module-lines = 4500
+indent-string = "  "
+
+[MISCELLANEOUS]
+notes = FIXME,XXX,TODO
+
+[SIMILARITIES]
+min-similarity-lines = 4
+ignore-comments = yes
+ignore-docstrings = yes
+
+[MESSAGES CONTROL]
+
+# Enable only checker(s) with the given id(s). This option conflicts with the
+# disable-checker option
+#enable-checker=
+
+# Enable all checker(s) except those with the given id(s). This option
+# conflicts with the enable-checker option
+#disable-checker=
+disable-checker=similarities
+
+# Enable all messages in the listed categories (IRCWEF).
+#enable-msg-cat=
+
+# Disable all messages in the listed categories (IRCWEF).
+disable-msg-cat=
+
+# Enable the message(s) with the given id(s).
+#enable-msg=
+
+# Disable the message(s) with the given id(s).
+disable-msg=W0511,R0922,W0201
+
+# The new pylint 0.21+ style (plus the similarities checker, which is no longer
+# a separate opiton, but a generic disable control)
+disable=W0511,R0922,W0201,R0922,R0801,I0011,R0201
index b096ec5..365f876 100644 (file)
     "instance-rename": true,
     "instance-shutdown": true,
     "instance-device-names": true,
+    "instance-device-hotplug": false,
 
     "job-list": true,
 
index 5d19317..8da65c5 100644 (file)
@@ -27,6 +27,7 @@ import re
 import tempfile
 import os.path
 
+from ganeti import _constants
 from ganeti import constants
 from ganeti import compat
 from ganeti import utils
@@ -225,7 +226,13 @@ def TestClusterInit(rapi_user, rapi_secret):
   qa_config.SetExclusiveStorage(e_s)
 
   extra_args = qa_config.get("cluster-init-args")
+
   if extra_args:
+    # This option was removed in 2.10, but in order to not break QA of older
+    # branches we remove it from the extra_args if it is in there.
+    opt_drbd_storage = "--no-drbd-storage"
+    if opt_drbd_storage in extra_args:
+      extra_args.remove(opt_drbd_storage)
     cmd.extend(extra_args)
 
   cmd.append(qa_config.get("name"))
@@ -556,8 +563,8 @@ def TestClusterModifyDiskTemplates():
   enabled_disk_templates = qa_config.GetEnabledDiskTemplates()
   default_disk_template = qa_config.GetDefaultDiskTemplate()
 
-  _TestClusterModifyDiskTemplatesArguments(default_disk_template,
-                                           enabled_disk_templates)
+  _TestClusterModifyDiskTemplatesArguments(default_disk_template)
+  _TestClusterModifyDiskTemplatesDrbdHelper(enabled_disk_templates)
   _TestClusterModifyDiskTemplatesVgName(enabled_disk_templates)
 
   _RestoreEnabledDiskTemplates()
@@ -566,7 +573,6 @@ def TestClusterModifyDiskTemplates():
   instance_template = enabled_disk_templates[0]
   instance = qa_instance.CreateInstanceByDiskTemplate(nodes, instance_template)
 
-  _TestClusterModifyUnusedDiskTemplate(instance_template)
   _TestClusterModifyUsedDiskTemplate(instance_template,
                                      enabled_disk_templates)
 
@@ -594,8 +600,57 @@ def _RestoreEnabledDiskTemplates():
   AssertCommand(cmd, fail=False)
 
 
-def _TestClusterModifyDiskTemplatesArguments(default_disk_template,
-                                             enabled_disk_templates):
+def _TestClusterModifyDiskTemplatesDrbdHelper(enabled_disk_templates):
+  """Tests argument handling of 'gnt-cluster modify' with respect to
+     the parameter '--drbd-usermode-helper'. This test is independent
+     of instances.
+
+  """
+  _RestoreEnabledDiskTemplates()
+
+  if constants.DT_DRBD8 not in enabled_disk_templates:
+    return
+  if constants.DT_PLAIN not in enabled_disk_templates:
+    return
+
+  drbd_usermode_helper = qa_config.get("drbd-usermode-helper", "/bin/true")
+  bogus_usermode_helper = "/tmp/pinkbunny"
+  for command, fail in [
+    (["gnt-cluster", "modify",
+      "--enabled-disk-templates=%s" % constants.DT_DRBD8,
+      "--ipolicy-disk-templates=%s" % constants.DT_DRBD8], False),
+    (["gnt-cluster", "modify",
+      "--drbd-usermode-helper=%s" % drbd_usermode_helper], False),
+    (["gnt-cluster", "modify",
+      "--drbd-usermode-helper=%s" % bogus_usermode_helper], True),
+    # unsetting helper when DRBD is enabled should not work
+    (["gnt-cluster", "modify",
+      "--drbd-usermode-helper="], True),
+    (["gnt-cluster", "modify",
+      "--enabled-disk-templates=%s" % constants.DT_PLAIN,
+      "--ipolicy-disk-templates=%s" % constants.DT_PLAIN], False),
+    (["gnt-cluster", "modify",
+      "--drbd-usermode-helper="], False),
+    (["gnt-cluster", "modify",
+      "--drbd-usermode-helper=%s" % drbd_usermode_helper], False),
+    (["gnt-cluster", "modify",
+      "--drbd-usermode-helper=%s" % drbd_usermode_helper,
+      "--enabled-disk-templates=%s" % constants.DT_DRBD8,
+      "--ipolicy-disk-templates=%s" % constants.DT_DRBD8], False),
+    (["gnt-cluster", "modify",
+      "--drbd-usermode-helper=",
+      "--enabled-disk-templates=%s" % constants.DT_PLAIN,
+      "--ipolicy-disk-templates=%s" % constants.DT_PLAIN], False),
+    (["gnt-cluster", "modify",
+      "--drbd-usermode-helper=%s" % drbd_usermode_helper,
+      "--enabled-disk-templates=%s" % constants.DT_DRBD8,
+      "--ipolicy-disk-templates=%s" % constants.DT_DRBD8], False),
+    ]:
+    AssertCommand(command, fail=fail)
+  _RestoreEnabledDiskTemplates()
+
+
+def _TestClusterModifyDiskTemplatesArguments(default_disk_template):
   """Tests argument handling of 'gnt-cluster modify' with respect to
      the parameter '--enabled-disk-templates'. This test is independent
      of instances.
@@ -616,27 +671,6 @@ def _TestClusterModifyDiskTemplatesArguments(default_disk_template,
      "--ipolicy-disk-templates=%s" % default_disk_template],
     fail=False)
 
-  if constants.DT_DRBD8 in enabled_disk_templates:
-    # interaction with --drbd-usermode-helper option
-    drbd_usermode_helper = qa_config.get("drbd-usermode-helper", None)
-    if not drbd_usermode_helper:
-      drbd_usermode_helper = "/bin/true"
-    # specifying a helper when drbd gets disabled is ok. Note that drbd still
-    # has to be installed on the nodes in this case
-    AssertCommand(["gnt-cluster", "modify",
-                   "--drbd-usermode-helper=%s" % drbd_usermode_helper,
-                   "--enabled-disk-templates=%s" % constants.DT_DISKLESS,
-                   "--ipolicy-disk-templates=%s" % constants.DT_DISKLESS],
-                   fail=False)
-    # specifying a helper when drbd is re-enabled
-    AssertCommand(["gnt-cluster", "modify",
-                   "--drbd-usermode-helper=%s" % drbd_usermode_helper,
-                   "--enabled-disk-templates=%s" %
-                     ",".join(enabled_disk_templates),
-                   "--ipolicy-disk-templates=%s" %
-                     ",".join(enabled_disk_templates)],
-                  fail=False)
-
 
 def _TestClusterModifyDiskTemplatesVgName(enabled_disk_templates):
   """Tests argument handling of 'gnt-cluster modify' with respect to
@@ -649,10 +683,9 @@ def _TestClusterModifyDiskTemplatesVgName(enabled_disk_templates):
     return
 
   # determine an LVM and a non-LVM disk template for the tests
-  non_lvm_template = _GetOtherEnabledDiskTemplate(utils.GetLvmDiskTemplates(),
+  non_lvm_template = _GetOtherEnabledDiskTemplate(constants.DTS_LVM,
                                                   enabled_disk_templates)
-  lvm_template = list(set(enabled_disk_templates)
-                      .intersection(set(utils.GetLvmDiskTemplates())))[0]
+  lvm_template = list(set(enabled_disk_templates) & constants.DTS_LVM)[0]
 
   vgname = qa_config.get("vg-name", constants.DEFAULT_VG)
 
@@ -743,26 +776,6 @@ def _TestClusterModifyUsedDiskTemplate(instance_template,
     fail=True)
 
 
-def _TestClusterModifyUnusedDiskTemplate(instance_template):
-  """Tests that unused disk templates can be disabled safely."""
-  all_disk_templates = constants.DISK_TEMPLATES
-  if not utils.IsLvmEnabled(qa_config.GetEnabledDiskTemplates()):
-    all_disk_templates = list(set(all_disk_templates) -
-                              set(utils.GetLvmDiskTemplates()))
-
-  AssertCommand(
-    ["gnt-cluster", "modify",
-     "--enabled-disk-templates=%s" % ",".join(all_disk_templates),
-     "--ipolicy-disk-templates=%s" % ",".join(all_disk_templates)],
-    fail=False)
-  new_disk_templates = [instance_template]
-  AssertCommand(
-    ["gnt-cluster", "modify",
-     "--enabled-disk-templates=%s" % ",".join(new_disk_templates),
-     "--ipolicy-disk-templates=%s" % ",".join(new_disk_templates)],
-    fail=False)
-
-
 def TestClusterModifyBe():
   """gnt-cluster modify -B"""
   for fail, cmd in [
@@ -1076,7 +1089,9 @@ def TestClusterBurnin():
     try:
       disks = qa_config.GetDiskOptions()
       # Run burnin
-      cmd = [script,
+      cmd = ["env",
+             "PYTHONPATH=%s" % _constants.VERSIONEDSHAREDIR,
+             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),
index e7d8bb4..0e66be1 100644 (file)
@@ -534,6 +534,15 @@ def TestInstanceModify(instance):
       ["-H", "%s=acn" % constants.HV_BOOT_ORDER],
       ["-H", "%s=%s" % (constants.HV_BOOT_ORDER, constants.VALUE_DEFAULT)],
       ])
+  elif default_hv == constants.HT_KVM and \
+    qa_config.TestEnabled("instance-device-hotplug"):
+    args.extend([
+      ["--net", "-1:add", "--hotplug"],
+      ["--net", "-1:modify,mac=aa:bb:cc:dd:ee:ff", "--hotplug", "--force"],
+      ["--net", "-1:remove", "--hotplug"],
+      ["--disk", "-1:add,size=1G", "--hotplug"],
+      ["--disk", "-1:remove", "--hotplug"],
+      ])
 
   for alist in args:
     AssertCommand(["gnt-instance", "modify"] + alist + [instance.name])
index 90bc1b9..8151622 100644 (file)
@@ -23,7 +23,7 @@
 
 """
 
-from ganeti import _autoconf
+from ganeti import _constants
 from ganeti import constants
 
 import qa_config
@@ -32,7 +32,7 @@ from qa_utils import AssertCommand
 from qa_instance_utils import CreateInstanceByDiskTemplate, \
                               RemoveInstance
 
-MON_COLLECTOR = _autoconf.PKGLIBDIR + "/mon-collector"
+MON_COLLECTOR = _constants.PKGLIBDIR + "/mon-collector"
 
 
 def TestInstStatusCollector():
index 16fdb15..a2928d6 100644 (file)
@@ -129,8 +129,7 @@ def TestNodeStorage():
 
     # Test all storage fields
     cmd = ["gnt-node", "list-storage", "--storage-type", storage_type,
-           "--output=%s" % ",".join(list(constants.VALID_STORAGE_FIELDS) +
-                                    [constants.SF_NODE, constants.SF_TYPE])]
+           "--output=%s" % ",".join(list(constants.VALID_STORAGE_FIELDS))]
     AssertCommand(cmd)
 
     # Get list of valid storage devices
diff --git a/src/AutoConf.hs.in b/src/AutoConf.hs.in
new file mode 100644 (file)
index 0000000..9c00056
--- /dev/null
@@ -0,0 +1,220 @@
+{-| Build-time configuration for Ganeti.
+
+Note that this file is autogenerated by the Makefile with a header
+from @AutoConf.hs.in@.
+
+-}
+
+{-
+
+Copyright (C) 2013 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 AutoConf where
+
+split :: String -> [String]
+split str =
+  case span (/= ',') str of
+    (x, []) -> [x]
+    (x, _:xs) -> x:split xs
+
+packageVersion :: String
+packageVersion = "PACKAGE_VERSION"
+
+versionMajor :: Int
+versionMajor = VERSION_MAJOR
+
+versionMinor :: Int
+versionMinor = VERSION_MINOR
+
+versionRevision :: Int
+versionRevision = VERSION_REVISION
+
+versionSuffix :: String
+versionSuffix = "VERSION_SUFFIX"
+
+versionFull :: String
+versionFull = "VERSION_FULL"
+
+dirVersion :: String
+dirVersion = "DIRVERSION"
+
+localstatedir :: String
+localstatedir = "LOCALSTATEDIR"
+
+sysconfdir :: String
+sysconfdir = "SYSCONFDIR"
+
+sshConfigDir :: String
+sshConfigDir = "SSH_CONFIG_DIR"
+
+sshLoginUser :: String
+sshLoginUser = "SSH_LOGIN_USER"
+
+sshConsoleUser :: String
+sshConsoleUser = "SSH_CONSOLE_USER"
+
+exportDir :: String
+exportDir = "EXPORT_DIR"
+
+osSearchPath :: [String]
+osSearchPath = split OS_SEARCH_PATH
+
+esSearchPath :: [String]
+esSearchPath = split ES_SEARCH_PATH
+
+xenBootloader :: String
+xenBootloader = "XEN_BOOTLOADER"
+
+xenConfigDir :: String
+xenConfigDir = "XEN_CONFIG_DIR"
+
+xenKernel :: String
+xenKernel = "XEN_KERNEL"
+
+xenInitrd :: String
+xenInitrd = "XEN_INITRD"
+
+kvmKernel :: String
+kvmKernel = "KVM_KERNEL"
+
+sharedFileStorageDir :: String
+sharedFileStorageDir = "SHARED_FILE_STORAGE_DIR"
+
+iallocatorSearchPath :: [String]
+iallocatorSearchPath = split IALLOCATOR_SEARCH_PATH
+
+kvmPath :: String
+kvmPath = "KVM_PATH"
+
+ipPath :: String
+ipPath = "IP_PATH"
+
+socatPath :: String
+socatPath = "SOCAT_PATH"
+
+socatUseEscape :: Bool
+socatUseEscape = SOCAT_USE_ESCAPE
+
+socatUseCompress :: Bool
+socatUseCompress = SOCAT_USE_COMPRESS
+
+lvmStripecount :: Int
+lvmStripecount = LVM_STRIPECOUNT
+
+toolsdir :: String
+toolsdir = "TOOLSDIR"
+
+gntScripts :: [String]
+gntScripts = GNT_SCRIPTS[]
+
+htoolsProgs :: [String]
+htoolsProgs = HS_HTOOLS_PROGS[]
+
+pkglibdir :: String
+pkglibdir = "PKGLIBDIR"
+
+sharedir :: String
+sharedir = "SHAREDIR"
+
+versionedsharedir :: String
+versionedsharedir = "VERSIONEDSHAREDIR"
+
+drbdBarriers :: String
+drbdBarriers = "DRBD_BARRIERS"
+
+drbdNoMetaFlush :: Bool
+drbdNoMetaFlush = DRBD_NO_META_FLUSH
+
+syslogUsage :: String
+syslogUsage = "SYSLOG_USAGE"
+
+daemonsGroup :: String
+daemonsGroup = "DAEMONS_GROUP"
+
+adminGroup :: String
+adminGroup = "ADMIN_GROUP"
+
+masterdUser :: String
+masterdUser = "MASTERD_USER"
+
+masterdGroup :: String
+masterdGroup = "MASTERD_GROUP"
+
+rapiUser :: String
+rapiUser = "RAPI_USER"
+
+rapiGroup :: String
+rapiGroup = "RAPI_GROUP"
+
+confdUser :: String
+confdUser = "CONFD_USER"
+
+confdGroup :: String
+confdGroup = "CONFD_GROUP"
+
+luxidUser :: String
+luxidUser = "LUXID_USER"
+
+luxidGroup :: String
+luxidGroup = "LUXID_GROUP"
+
+nodedUser :: String
+nodedUser = "NODED_USER"
+
+nodedGroup :: String
+nodedGroup = "NODED_GROUP"
+
+mondUser :: String
+mondUser = "MOND_USER"
+
+mondGroup :: String
+mondGroup = "MOND_GROUP"
+
+diskSeparator :: String
+diskSeparator = "DISK_SEPARATOR"
+
+qemuimgPath :: String
+qemuimgPath = "QEMUIMG_PATH"
+
+htools :: Bool
+htools = True
+
+enableConfd :: Bool
+enableConfd = ENABLE_CONFD
+
+xenCmd :: String
+xenCmd = "XEN_CMD"
+
+enableSplitQuery :: Bool
+enableSplitQuery = ENABLE_SPLIT_QUERY
+
+enableRestrictedCommands :: Bool
+enableRestrictedCommands = ENABLE_RESTRICTED_COMMANDS
+
+enableMond :: Bool
+enableMond = ENABLE_MOND
+
+hasGnuLn :: Bool
+hasGnuLn = HAS_GNU_LN
+
+-- Write dictionary with man page name as the key and the section
+-- number as the value
+manPages :: [(String, Int)]
+manPages = MAN_PAGES[]
index 080c6a9..8b02d70 100644 (file)
@@ -41,6 +41,8 @@ module Ganeti.BasicTypes
   , goodMatchPriority
   , prefixMatch
   , compareNameComponent
+  , ListSet(..)
+  , emptyListSet
   ) where
 
 import Control.Applicative
@@ -48,6 +50,10 @@ import Control.Monad
 import Control.Monad.Trans
 import Data.Function
 import Data.List
+import Data.Set (Set)
+import qualified Data.Set as Set (empty)
+import Text.JSON (JSON)
+import qualified Text.JSON as JSON (readJSON, showJSON)
 
 -- | Generic monad for our error handling mechanisms.
 data GenericResult a b
@@ -224,3 +230,18 @@ lookupName :: [String]      -- ^ List of keys
            -> LookupResult  -- ^ Result of the lookup
 lookupName l s = foldr (chooseLookupResult s)
                        (LookupResult FailMatch s) l
+
+-- | Wrapper for a Haskell 'Set'
+--
+-- This type wraps a 'Set' and it is used in the Haskell to Python
+-- opcode generation to transform a Haskell 'Set' into a Python 'list'
+-- without duplicate elements.
+newtype ListSet a = ListSet { unListSet :: Set a }
+  deriving (Eq, Show)
+
+instance (Ord a, JSON a) => JSON (ListSet a) where
+  showJSON = JSON.showJSON . unListSet
+  readJSON = liftM ListSet . JSON.readJSON
+
+emptyListSet :: ListSet a
+emptyListSet = ListSet Set.empty
index 25cbb17..99f66c7 100644 (file)
@@ -246,7 +246,7 @@ respondInner cfg hmac rq =
       innermsg = serializeResponse (cfg >>= flip buildResponse rq)
       innerserialised = J.encodeStrict innermsg
       outermsg = signMessage hmac rsalt innerserialised
-      outerserialised = confdMagicFourcc ++ J.encodeStrict outermsg
+      outerserialised = C.confdMagicFourcc ++ J.encodeStrict outermsg
   in outerserialised
 
 -- | Main listener loop.
@@ -255,7 +255,7 @@ listener :: S.Socket -> HashKey
          -> IO ()
 listener s hmac resp = do
   (msg, _, peer) <- S.recvFrom s 4096
-  if confdMagicFourcc `isPrefixOf` msg
+  if C.confdMagicFourcc `isPrefixOf` msg
     then forkIO (resp s hmac (drop 4 msg) peer) >> return ()
     else logDebug "Invalid magic code!" >> return ()
   return ()
index f503c3d..03aded6 100644 (file)
@@ -26,21 +26,18 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 -}
 
 module Ganeti.Confd.Types
-  ( C.confdProtocolVersion
-  , C.confdMaxClockSkew
-  , C.confdConfigReloadTimeout
-  , C.confdConfigReloadRatelimit
-  , C.confdMagicFourcc
-  , C.confdDefaultReqCoverage
-  , C.confdClientExpireTimeout
-  , C.maxUdpDataSize
-  , ConfdClient(..)
+  ( ConfdClient(..)
   , ConfdRequestType(..)
-  , ConfdReqQ(..)
+  , confdRequestTypeToRaw
   , ConfdReqField(..)
+  , confdReqFieldToRaw
+  , ConfdReqQ(..)
   , ConfdReplyStatus(..)
+  , confdReplyStatusToRaw
   , ConfdNodeRole(..)
+  , confdNodeRoleToRaw
   , ConfdErrorType(..)
+  , confdErrorTypeToRaw
   , ConfdRequest(..)
   , newConfdRequest
   , ConfdReply(..)
@@ -51,41 +48,28 @@ module Ganeti.Confd.Types
 import Text.JSON
 import qualified Network.Socket as S
 
-import qualified Ganeti.Constants as C
+import qualified Ganeti.ConstantUtils as ConstantUtils
 import Ganeti.Hash
 import Ganeti.THH
 import Ganeti.Utils (newUUID)
 
-{-
-   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 )
-  , ("ReqNodeInstances",    'C.confdReqNodeInstances)
+$(declareILADT "ConfdRequestType"
+  [ ("ReqPing",             0)
+  , ("ReqNodeRoleByName",   1)
+  , ("ReqNodePipByInstPip", 2)
+  , ("ReqClusterMaster",    3)
+  , ("ReqNodePipList",      4)
+  , ("ReqMcPipList",        5)
+  , ("ReqInstIpsList",      6)
+  , ("ReqNodeDrbd",         7)
+  , ("ReqNodeInstances",    8)
   ])
 $(makeJSONInstance ''ConfdRequestType)
 
-$(declareSADT "ConfdReqField"
-  [ ("ReqFieldName",     'C.confdReqfieldName )
-  , ("ReqFieldIp",       'C.confdReqfieldIp )
-  , ("ReqFieldMNodePip", 'C.confdReqfieldMnodePip )
+$(declareILADT "ConfdReqField"
+  [ ("ReqFieldName",     0)
+  , ("ReqFieldIp",       1)
+  , ("ReqFieldMNodePip", 2)
   ])
 $(makeJSONInstance ''ConfdReqField)
 
@@ -95,14 +79,17 @@ $(makeJSONInstance ''ConfdReqField)
 
 $(buildObject "ConfdReqQ" "confdReqQ"
   [ renameField "Ip" .
-                optionalField $ simpleField C.confdReqqIp [t| String   |]
+    optionalField $
+    simpleField ConstantUtils.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] |]
+    defaultField [| [] |] $
+    simpleField ConstantUtils.confdReqqIplist [t| [String] |]
+  , renameField "Link" .
+    optionalField $
+    simpleField ConstantUtils.confdReqqLink [t| String |]
+  , renameField "Fields" .
+    defaultField [| [] |] $
+    simpleField ConstantUtils.confdReqqFields [t| [ConfdReqField] |]
   ])
 
 -- | Confd query type. This is complex enough that we can't
@@ -124,30 +111,29 @@ instance JSON ConfdQuery where
                   PlainQuery s -> showJSON s
                   DictQuery drq -> showJSON drq
 
-$(declareIADT "ConfdReplyStatus"
-  [ ( "ReplyStatusOk",      'C.confdReplStatusOk )
-  , ( "ReplyStatusError",   'C.confdReplStatusError )
-  , ( "ReplyStatusNotImpl", 'C.confdReplStatusNotimplemented )
+$(declareILADT "ConfdReplyStatus"
+  [ ("ReplyStatusOk",      0)
+  , ("ReplyStatusError",   1)
+  , ("ReplyStatusNotImpl", 2)
   ])
 $(makeJSONInstance ''ConfdReplyStatus)
 
-$(declareIADT "ConfdNodeRole"
-  [ ( "NodeRoleMaster",    'C.confdNodeRoleMaster )
-  , ( "NodeRoleCandidate", 'C.confdNodeRoleCandidate )
-  , ( "NodeRoleOffline",   'C.confdNodeRoleOffline )
-  , ( "NodeRoleDrained",   'C.confdNodeRoleDrained )
-  , ( "NodeRoleRegular",   'C.confdNodeRoleRegular )
+$(declareILADT "ConfdNodeRole"
+  [ ("NodeRoleMaster",    0)
+  , ("NodeRoleCandidate", 1)
+  , ("NodeRoleOffline",   2)
+  , ("NodeRoleDrained",   3)
+  , ("NodeRoleRegular",   4)
   ])
 $(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 )
+$(declareILADT "ConfdErrorType"
+  [ ("ConfdErrorUnknownEntry", 0)
+  , ("ConfdErrorInternal",     1)
+  , ("ConfdErrorArgument",     2)
   ])
 $(makeJSONInstance ''ConfdErrorType)
 
@@ -163,7 +149,7 @@ $(buildObject "ConfdRequest" "confdRq"
 newConfdRequest :: ConfdRequestType -> ConfdQuery -> IO ConfdRequest
 newConfdRequest reqType query = do
   rsalt <- newUUID
-  return $ ConfdRequest C.confdProtocolVersion reqType query rsalt
+  return $ ConfdRequest ConstantUtils.confdProtocolVersion reqType query rsalt
 
 $(buildObject "ConfdReply" "confdReply"
   [ simpleField "protocol" [t| Int              |]
diff --git a/src/Ganeti/ConstantUtils.hs b/src/Ganeti/ConstantUtils.hs
new file mode 100644 (file)
index 0000000..c5ae57b
--- /dev/null
@@ -0,0 +1,200 @@
+{-| ConstantUtils contains the helper functions for constants
+
+This module cannot be merged with 'Ganeti.Utils' because it would
+create a circular dependency if imported, for example, from
+'Ganeti.Constants'.
+
+-}
+
+{-
+
+Copyright (C) 2013 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.ConstantUtils where
+
+import Data.Char (ord)
+import Data.Set (Set)
+import qualified Data.Set as Set (difference, fromList, toList, union)
+
+import Ganeti.THH (PyValue(..))
+import Ganeti.PyValueInstances ()
+
+-- | 'PythonChar' wraps a Python 'char'
+newtype PythonChar = PythonChar { unPythonChar :: Char }
+  deriving (Show)
+
+instance PyValue PythonChar where
+  showValue c = "chr(" ++ show (ord (unPythonChar c)) ++ ")"
+
+-- | 'PythonNone' wraps Python 'None'
+data PythonNone = PythonNone
+
+instance PyValue PythonNone where
+  showValue _ = "None"
+
+-- | FrozenSet wraps a Haskell 'Set'
+--
+-- See 'PyValue' instance for 'FrozenSet'.
+newtype FrozenSet a = FrozenSet { unFrozenSet :: Set a }
+  deriving (Eq, Ord, Show)
+
+-- | Converts a Haskell 'Set' into a Python 'frozenset'
+--
+-- This instance was supposed to be for 'Set' instead of 'FrozenSet'.
+-- However, 'ghc-6.12.1' seems to be crashing with 'segmentation
+-- fault' due to the presence of more than one instance of 'Set',
+-- namely, this one and the one in 'Ganeti.OpCodes'.  For this reason,
+-- we wrap 'Set' into 'FrozenSet'.
+instance PyValue a => PyValue (FrozenSet a) where
+  showValue s = "frozenset(" ++ showValue (Set.toList (unFrozenSet s)) ++ ")"
+
+mkSet :: Ord a => [a] -> FrozenSet a
+mkSet = FrozenSet . Set.fromList
+
+toList :: FrozenSet a -> [a]
+toList = Set.toList . unFrozenSet
+
+union :: Ord a => FrozenSet a -> FrozenSet a -> FrozenSet a
+union x y = FrozenSet (unFrozenSet x `Set.union` unFrozenSet y)
+
+difference :: Ord a => FrozenSet a -> FrozenSet a -> FrozenSet a
+difference x y = FrozenSet (unFrozenSet x `Set.difference` unFrozenSet y)
+
+-- | 'Protocol' represents the protocols used by the daemons
+data Protocol = Tcp | Udp
+  deriving (Show)
+
+-- | 'PyValue' instance of 'Protocol'
+--
+-- This instance is used by the Haskell to Python constants
+instance PyValue Protocol where
+  showValue Tcp = "\"tcp\""
+  showValue Udp = "\"udp\""
+
+-- | Failure exit code
+--
+-- These are defined here and not in 'Ganeti.HsConstants' together with
+-- the other exit codes in order to avoid a circular dependency
+-- between 'Ganeti.HsConstants' and 'Ganeti.Runtime'
+exitFailure :: Int
+exitFailure = 1
+
+-- | Console device
+--
+-- This is defined here and not in 'Ganeti.HsConstants' order to avoid
+-- a circular dependency between 'Ganeti.HsConstants' and
+-- 'Ganeti.Logging'
+devConsole :: String
+devConsole = "/dev/console"
+
+-- | Random uuid generator
+--
+-- This is defined here and not in 'Ganeti.HsConstants' order to avoid
+-- a circular dependendy between 'Ganeti.HsConstants' and
+-- 'Ganeti.Types'
+randomUuidFile :: String
+randomUuidFile = "/proc/sys/kernel/random/uuid"
+
+-- * Priority levels
+--
+-- This is defined here and not in 'Ganeti.Types' in order to avoid a
+-- GHC stage restriction and because there is no suitable 'declareADT'
+-- variant that handles integer values directly.
+
+priorityLow :: Int
+priorityLow = 10
+
+priorityNormal :: Int
+priorityNormal = 0
+
+priorityHigh :: Int
+priorityHigh = -10
+
+-- | Calculates int version number from major, minor and revision
+-- numbers.
+buildVersion :: Int -> Int -> Int -> Int
+buildVersion major minor revision =
+  1000000 * major + 10000 * minor + 1 * revision
+
+-- | Confd protocol version
+--
+-- This is defined here in order to avoid a circular dependency
+-- between 'Ganeti.Confd.Types' and 'Ganeti.HsConstants'.
+confdProtocolVersion :: Int
+confdProtocolVersion = 1
+
+-- * Confd request query fields
+--
+-- These are defined here and not in 'Ganeti.Types' due to GHC stage
+-- restrictions concerning Template Haskell.  They are also not
+-- defined in 'Ganeti.HsConstants' in order to avoid a circular
+-- dependency between that module and 'Ganeti.Types'.
+
+confdReqqLink :: String
+confdReqqLink = "0"
+
+confdReqqIp :: String
+confdReqqIp = "1"
+
+confdReqqIplist :: String
+confdReqqIplist = "2"
+
+confdReqqFields :: String
+confdReqqFields = "3"
+
+-- * ISpec
+
+ispecMemSize :: String
+ispecMemSize = "memory-size"
+
+ispecCpuCount :: String
+ispecCpuCount = "cpu-count"
+
+ispecDiskCount :: String
+ispecDiskCount = "disk-count"
+
+ispecDiskSize :: String
+ispecDiskSize = "disk-size"
+
+ispecNicCount :: String
+ispecNicCount = "nic-count"
+
+ispecSpindleUse :: String
+ispecSpindleUse = "spindle-use"
+
+ispecsMinmax :: String
+ispecsMinmax = "minmax"
+
+ispecsStd :: String
+ispecsStd = "std"
+
+ipolicyDts :: String
+ipolicyDts = "disk-templates"
+
+ipolicyVcpuRatio :: String
+ipolicyVcpuRatio = "vcpu-ratio"
+
+ipolicySpindleRatio :: String
+ipolicySpindleRatio = "spindle-ratio"
+
+ipolicyDefaultsVcpuRatio :: Double
+ipolicyDefaultsVcpuRatio = 4.0
+
+ipolicyDefaultsSpindleRatio :: Double
+ipolicyDefaultsSpindleRatio = 32.0
diff --git a/src/Ganeti/Constants.hs b/src/Ganeti/Constants.hs
new file mode 100644 (file)
index 0000000..1e42563
--- /dev/null
@@ -0,0 +1,33 @@
+{-| Ganeti constants.
+
+This module reexports the Haskell constants and those generated from
+Python.
+
+-}
+
+{-
+
+Copyright (C) 2013 Google Inc.
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+02110-1301, USA.
+
+-}
+
+module Ganeti.Constants (module Ganeti.HsConstants,
+                         module Ganeti.PyConstants) where
+
+import Ganeti.HsConstants
+import Ganeti.PyConstants
diff --git a/src/Ganeti/Cpu/LoadParser.hs b/src/Ganeti/Cpu/LoadParser.hs
new file mode 100644 (file)
index 0000000..a951382
--- /dev/null
@@ -0,0 +1,69 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-| /proc/stat file parser
+
+This module holds the definition of the parser that extracts information
+about the CPU load of the system from the @/proc/stat@ file.
+
+-}
+{-
+
+Copyright (C) 2013 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.Cpu.LoadParser (cpustatParser) where
+
+import Control.Applicative ((<*>), (<*), (*>), (<$>), (<|>))
+import qualified Data.Attoparsec.Text as A
+import qualified Data.Attoparsec.Combinator as AC
+import Data.Attoparsec.Text (Parser)
+
+import Ganeti.Parsers
+import Ganeti.Cpu.Types
+
+-- * Parser implementation
+
+-- | The parser for one line of the CPU status file.
+oneCPUstatParser :: Parser CPUstat
+oneCPUstatParser =
+  let nameP = stringP
+      userP = numberP
+      niceP = numberP
+      systemP = numberP
+      idleP = numberP
+      iowaitP = numberP
+      irqP = numberP
+      softirqP = numberP
+      stealP = numberP
+      guestP = numberP
+      guest_niceP = numberP
+  in
+    CPUstat <$> nameP <*> userP <*> niceP <*> systemP <*> idleP <*> iowaitP
+            <*> irqP <*> softirqP <*> stealP <*> guestP <*> guest_niceP
+            <* A.endOfLine
+
+-- | When this is satisfied all the lines containing information about
+-- the CPU load are parsed.
+intrFound :: Parser ()
+intrFound = (A.string "intr" *> return ())
+             <|> (A.string "page" *> return ())
+             <|> (A.string "swap" *> return ())
+
+-- | The parser for the fragment of CPU status file containing
+-- information about the CPU load.
+cpustatParser :: Parser [CPUstat]
+cpustatParser = oneCPUstatParser `AC.manyTill` intrFound
diff --git a/src/Ganeti/Cpu/Types.hs b/src/Ganeti/Cpu/Types.hs
new file mode 100644 (file)
index 0000000..61c4137
--- /dev/null
@@ -0,0 +1,56 @@
+{-# LANGUAGE TemplateHaskell #-}
+{-| CPUload data types
+
+This module holds the definition of the data types describing the CPU
+load according to information collected periodically from @/proc/stat@.
+
+-}
+{-
+
+Copyright (C) 2013 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.Cpu.Types
+  ( CPUstat(..)
+  , CPUavgload(..)
+  ) where
+
+import Ganeti.THH
+
+-- | This is the format of the report produced by the cpu load
+-- collector.
+$(buildObject "CPUavgload" "cav"
+  [ simpleField "cpu_number" [t| Int |]
+  , simpleField "cpus"       [t| [Double] |]
+  , simpleField "cpu_total"  [t| Double |]
+  ])
+
+-- | This is the format of the data parsed by the input file.
+$(buildObject "CPUstat" "cs"
+  [ simpleField "name"       [t| String |]
+  , simpleField "user"       [t| Int |]
+  , simpleField "nice"       [t| Int |]
+  , simpleField "system"     [t| Int |]
+  , simpleField "idle"       [t| Int |]
+  , simpleField "iowait"     [t| Int |]
+  , simpleField "irq"        [t| Int |]
+  , simpleField "softirq"    [t| Int |]
+  , simpleField "steal"      [t| Int |]
+  , simpleField "guest"      [t| Int |]
+  , simpleField "guest_nice" [t| Int |]
+  ])
diff --git a/src/Ganeti/DataCollectors/CPUload.hs b/src/Ganeti/DataCollectors/CPUload.hs
new file mode 100644 (file)
index 0000000..edcd40e
--- /dev/null
@@ -0,0 +1,183 @@
+{-| @/proc/stat@ data collector.
+
+-}
+
+{-
+
+Copyright (C) 2013 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.DataCollectors.CPUload
+  ( dcName
+  , dcVersion
+  , dcFormatVersion
+  , dcCategory
+  , dcKind
+  , dcReport
+  , dcUpdate
+  ) where
+
+import qualified Control.Exception as E
+import Data.Attoparsec.Text.Lazy as A
+import Data.Text.Lazy (pack, unpack)
+import qualified Text.JSON as J
+import qualified Data.Sequence as Seq
+import System.Posix.Unistd (getSysVar, SysVar(ClockTick))
+
+import qualified Ganeti.BasicTypes as BT
+import qualified Ganeti.Constants as C
+import Ganeti.Cpu.LoadParser(cpustatParser)
+import Ganeti.DataCollectors.Types
+import Ganeti.Utils
+import Ganeti.Cpu.Types
+
+-- | The default path of the CPU status file.
+-- It is hardcoded because it is not likely to change.
+defaultFile :: FilePath
+defaultFile = C.statFile
+
+-- | The buffer size of the values kept in the map.
+bufferSize :: Int
+bufferSize = C.cpuavgloadBufferSize
+
+-- | The window size of the values that will export the average load.
+windowSize :: Integer
+windowSize = toInteger C.cpuavgloadWindowSize
+
+-- | The default setting for the maximum amount of not parsed character to
+-- print in case of error.
+-- It is set to use most of the screen estate on a standard 80x25 terminal.
+-- TODO: add the possibility to set this with a command line parameter.
+defaultCharNum :: Int
+defaultCharNum = 80*20
+
+-- | The name of this data collector.
+dcName :: String
+dcName = "cpu-avg-load"
+
+-- | The version of this data collector.
+dcVersion :: DCVersion
+dcVersion = DCVerBuiltin
+
+-- | The version number for the data format of this data collector.
+dcFormatVersion :: Int
+dcFormatVersion = 1
+
+-- | The category of this data collector.
+dcCategory :: Maybe DCCategory
+dcCategory = Nothing
+
+-- | The kind of this data collector.
+dcKind :: DCKind
+dcKind = DCKPerf
+
+-- | The data exported by the data collector, taken from the default location.
+dcReport :: Maybe CollectorData -> IO DCReport
+dcReport colData =
+  let cpuLoadData =
+        case colData of
+          Nothing -> Seq.empty
+          Just colData' ->
+            case colData' of
+              CPULoadData v -> v
+  in buildDCReport cpuLoadData
+-- | Data stored by the collector in mond's memory.
+type Buffer = Seq.Seq (Integer, [Int])
+
+-- | Compute the load from a CPU.
+computeLoad :: CPUstat -> Int
+computeLoad cpuData =
+  csUser cpuData + csNice cpuData + csSystem cpuData
+  + csIowait cpuData + csIrq cpuData + csSoftirq cpuData
+  + csSteal cpuData + csGuest cpuData + csGuestNice cpuData
+
+-- | Reads and Computes the load for each CPU.
+dcCollectFromFile :: FilePath -> IO (Integer, [Int])
+dcCollectFromFile inputFile = do
+  contents <-
+    ((E.try $ readFile inputFile) :: IO (Either IOError String)) >>=
+      exitIfBad "reading from file" . either (BT.Bad . show) BT.Ok
+  cpustatData <-
+    case A.parse cpustatParser $ pack contents of
+      A.Fail unparsedText contexts errorMessage -> exitErr $
+        show (Prelude.take defaultCharNum $ unpack unparsedText) ++ "\n"
+          ++ show contexts ++ "\n" ++ errorMessage
+      A.Done _ cpustatD -> return cpustatD
+  now <- getCurrentTime
+  let timestamp = now :: Integer
+  return (timestamp, map computeLoad cpustatData)
+
+-- | Returns the collected data in the appropriate type.
+dcCollect :: IO Buffer
+dcCollect  = do
+  l <- dcCollectFromFile defaultFile
+  return (Seq.singleton l)
+
+-- | Formats data for JSON transformation.
+formatData :: [Double] -> CPUavgload
+formatData [] = CPUavgload (0 :: Int) [] (0 :: Double)
+formatData l@(x:xs) = CPUavgload (length l - 1) xs x
+
+-- | Update a Map Entry.
+updateEntry :: Buffer -> Buffer -> Buffer
+updateEntry newBuffer mapEntry =
+  (Seq.><) newBuffer
+  (if Seq.length mapEntry < bufferSize
+    then mapEntry
+    else Seq.drop 1 mapEntry)
+
+-- | Updates the given Collector data.
+dcUpdate :: Maybe CollectorData -> IO CollectorData
+dcUpdate mcd = do
+  v <- dcCollect
+  let new_v =
+        case mcd of
+          Nothing -> v
+          Just cd ->
+            case cd of
+              CPULoadData old_v -> updateEntry v old_v
+  new_v `seq` return $ CPULoadData new_v
+
+-- | Computes the average load for every CPU and the overall from data read
+-- from the map.
+computeAverage :: Buffer -> Integer -> Integer -> [Double]
+computeAverage s w ticks =
+  let window = Seq.takeWhileL ((> w) . fst) s
+      go Seq.EmptyL          _                    = []
+      go _                   Seq.EmptyR           = []
+      go (leftmost Seq.:< _) (_ Seq.:> rightmost) = do
+        let (timestampL, listL) = leftmost
+            (timestampR, listR) = rightmost
+            work = zipWith (-) listL listR
+            overall = (timestampL - timestampR) * ticks
+        map (\x -> fromIntegral x / fromIntegral overall) work
+  in go (Seq.viewl window) (Seq.viewr window)
+
+-- | This function computes the JSON representation of the CPU load.
+buildJsonReport :: Buffer -> IO J.JSValue
+buildJsonReport v = do
+  ticks <- getSysVar ClockTick
+  let res = computeAverage v windowSize ticks
+  return . J.showJSON $ formatData res
+
+-- | This function computes the DCReport for the CPU load.
+buildDCReport :: Buffer -> IO DCReport
+buildDCReport v  =
+  buildJsonReport v >>=
+    buildReport dcName dcVersion dcFormatVersion dcCategory dcKind
index 5803c3d..8fd0e0c 100644 (file)
@@ -32,7 +32,6 @@ module Ganeti.DataCollectors.InstStatusTypes
 
 import Ganeti.DataCollectors.Types
 import Ganeti.Hypervisor.Xen.Types
-import Ganeti.Objects
 import Ganeti.THH
 import Ganeti.Types
 
index 80df40c..e43dfd6 100644 (file)
@@ -33,11 +33,17 @@ module Ganeti.DataCollectors.Types
   , DCStatus(..)
   , DCStatusCode(..)
   , DCVersion(..)
+  , CollectorData(..)
+  , CollectorMap
   , buildReport
   , mergeStatuses
+  , getCategoryName
   ) where
 
 import Data.Char
+import Data.Ratio
+import qualified Data.Map as Map
+import qualified Data.Sequence as Seq
 import Text.JSON
 
 import Ganeti.Constants as C
@@ -46,13 +52,27 @@ import Ganeti.Utils (getCurrentTime)
 
 -- | The possible classes a data collector can belong to.
 data DCCategory = DCInstance | DCStorage | DCDaemon | DCHypervisor
-  deriving (Show, Eq)
+  deriving (Show, Eq, Read)
+
+-- | Get the category name and return it as a string.
+getCategoryName :: DCCategory -> String
+getCategoryName dcc = map toLower . drop 2 . show $ dcc
+
+categoryNames :: Map.Map String DCCategory
+categoryNames =
+  let l = [DCInstance, DCStorage, DCDaemon, DCHypervisor]
+  in Map.fromList $ zip (map getCategoryName l) l
 
 -- | The JSON instance for DCCategory.
 instance JSON DCCategory where
-  showJSON = showJSON . map toLower . drop 2 . show
-  readJSON =
-    error "JSON read instance not implemented for type DCCategory"
+  showJSON = showJSON . getCategoryName
+  readJSON (JSString s) =
+    let s' = fromJSString s
+    in case Map.lookup s' categoryNames of
+         Just category -> Ok category
+         Nothing -> fail $ "Invalid category name " ++ s' ++ " for type\
+                           \ DCCategory"
+  readJSON v = fail $ "Invalid JSON value " ++ show v ++ " for type DCCategory"
 
 -- | The possible status codes of a data collector.
 data DCStatusCode = DCSCOk      -- ^ Everything is OK
@@ -84,7 +104,15 @@ data DCKind = DCKPerf   -- ^ Performance reporting collector
 instance JSON DCKind where
   showJSON DCKPerf   = showJSON (0 :: Int)
   showJSON DCKStatus = showJSON (1 :: Int)
-  readJSON = error "JSON read instance not implemented for type DCKind"
+  readJSON (JSRational _ x) =
+    if denominator x /= 1
+    then fail $ "Invalid JSON value " ++ show x ++ " for type DCKind"
+    else
+      let x' = (fromIntegral . numerator $ x) :: Int
+      in if x' == 0 then Ok DCKPerf
+         else if x' == 1 then Ok DCKStatus
+         else fail $ "Invalid JSON value " ++ show x' ++ " for type DCKind"
+  readJSON v = fail $ "Invalid JSON value " ++ show v ++ " for type DCKind"
 
 -- | Type representing the version number of a data collector.
 data DCVersion = DCVerBuiltin | DCVersion String deriving (Show, Eq)
@@ -93,7 +121,16 @@ data DCVersion = DCVerBuiltin | DCVersion String deriving (Show, Eq)
 instance JSON DCVersion where
   showJSON DCVerBuiltin = showJSON C.builtinDataCollectorVersion
   showJSON (DCVersion v) = showJSON v
-  readJSON = error "JSON read instance not implemented for type DCVersion"
+  readJSON (JSString s) =
+    if fromJSString s == C.builtinDataCollectorVersion
+    then Ok DCVerBuiltin else Ok . DCVersion $ fromJSString s
+  readJSON v = fail $ "Invalid JSON value " ++ show v ++ " for type DCVersion"
+
+-- | Type for the value field of the above map.
+data CollectorData = CPULoadData (Seq.Seq (Integer, [Int]))
+
+-- | Type for the map storing the data of the statefull DataCollectors.
+type CollectorMap = Map.Map String CollectorData
 
 -- | This is the format of the report produced by each data collector.
 $(buildObject "DCReport" "dcReport"
index dd92497..27dc2e6 100644 (file)
@@ -50,6 +50,7 @@ import Ganeti.HTools.CLI
 import Ganeti.HTools.Loader
 import Ganeti.HTools.Types
 import Ganeti.JSON
+import Ganeti.Types (EvacMode(ChangePrimary, ChangeSecondary))
 import Ganeti.Utils
 
 {-# ANN module "HLint: ignore Eta reduce" #-}
index 314fb79..560c73d 100644 (file)
@@ -48,6 +48,8 @@ module Ganeti.HTools.CLI
   , oDiskTemplate
   , oSpindleUse
   , oDynuFile
+  , oMonD
+  , oMonDDataFile
   , oEvacMode
   , oExInst
   , oExTags
@@ -56,6 +58,7 @@ module Ganeti.HTools.CLI
   , oFullEvacuation
   , oGroup
   , oIAllocSrc
+  , oIgnoreDyn 
   , oIgnoreNonRedundant
   , oInstMoves
   , oJobDelay
@@ -121,6 +124,10 @@ data Options = Options
   , 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
+  , optIgnoreDynu  :: Bool           -- ^ Do not use dynamic use data
+  , optMonD        :: Bool           -- ^ Query MonDs
+  , optMonDFile    :: Maybe FilePath -- ^ Optional file with data provided
+                                     -- ^ by MonDs
   , optEvacMode    :: Bool           -- ^ Enable evacuation mode
   , optExInst      :: [String]       -- ^ Instances to be excluded
   , optExTags      :: Maybe [String] -- ^ Tags to use for exclusion
@@ -174,7 +181,10 @@ defaultOptions  = Options
   , optInstMoves   = True
   , optDiskTemplate = Nothing
   , optSpindleUse  = Nothing
+  , optIgnoreDynu  = False
   , optDynuFile    = Nothing
+  , optMonD        = False
+  , optMonDFile = Nothing
   , optEvacMode    = False
   , optExInst      = []
   , optExTags      = Nothing
@@ -277,6 +287,20 @@ oDiskMoves =
    \ thus allowing only the 'cheap' failover/migrate operations",
    OptComplNone)
 
+oMonD :: OptType
+oMonD =
+  (Option "" ["mond"]
+   (NoArg (\ opts -> Ok opts {optMonD = True}))
+   "Query MonDs",
+   OptComplNone)
+
+oMonDDataFile :: OptType
+oMonDDataFile =
+  (Option "" ["mond-data"]
+   (ReqArg (\ f opts -> Ok opts { optMonDFile = Just f }) "FILE")
+   "Import data provided by MonDs from the given FILE",
+   OptComplFile)
+
 oDiskTemplate :: OptType
 oDiskTemplate =
   (Option "" ["disk-template"]
@@ -320,6 +344,13 @@ oDynuFile =
    "Import dynamic utilisation data from the given FILE",
    OptComplFile)
 
+oIgnoreDyn :: OptType
+oIgnoreDyn =
+  (Option "" ["ignore-dynu"]
+   (NoArg (\ opts -> Ok opts {optIgnoreDynu = True}))
+   "Ignore any dynamic utilisation information",
+   OptComplNone)
+
 oEvacMode :: OptType
 oEvacMode =
   (Option "E" ["evac-mode"]
index 88891a4..fe3432c 100644 (file)
@@ -94,7 +94,7 @@ import Ganeti.HTools.Types
 import Ganeti.Compat
 import qualified Ganeti.OpCodes as OpCodes
 import Ganeti.Utils
-import Ganeti.Types (mkNonEmpty)
+import Ganeti.Types (EvacMode(..), mkNonEmpty)
 
 -- * Types
 
index a6b19a2..8860a04 100644 (file)
@@ -1,3 +1,5 @@
+{-# LANGUAGE BangPatterns #-}
+
 {-| External data loader.
 
 This module holds the external data loading, and thus is the only one
@@ -31,27 +33,43 @@ module Ganeti.HTools.ExtLoader
   ( loadExternalData
   , commonSuffix
   , maybeSaveData
+  , queryAllMonDDCs
+  , pMonDData
   ) where
 
 import Control.Monad
 import Control.Exception
-import Data.Maybe (isJust, fromJust)
+import Data.Maybe (isJust, fromJust, catMaybes)
+import Network.Curl
 import System.FilePath
 import System.IO
 import System.Time (getClockTime)
 import Text.Printf (hPrintf)
 
+import qualified Text.JSON as J
+import qualified Data.Map as Map
+import qualified Data.List as L
+
+import qualified Ganeti.Constants as C
+import qualified Ganeti.DataCollectors.CPUload as CPUload
+import qualified Ganeti.HTools.Container as Container
 import qualified Ganeti.HTools.Backend.Luxi as Luxi
 import qualified Ganeti.HTools.Backend.Rapi as Rapi
 import qualified Ganeti.HTools.Backend.Simu as Simu
 import qualified Ganeti.HTools.Backend.Text as Text
 import qualified Ganeti.HTools.Backend.IAlloc as IAlloc
+import qualified Ganeti.HTools.Node as Node
+import qualified Ganeti.HTools.Instance as Instance
 import Ganeti.HTools.Loader (mergeData, checkData, ClusterData(..)
-                            , commonSuffix)
+                            , commonSuffix, clearDynU)
 
 import Ganeti.BasicTypes
+import Ganeti.Cpu.Types
+import Ganeti.DataCollectors.Types
 import Ganeti.HTools.Types
 import Ganeti.HTools.CLI
+import Ganeti.JSON
+import Ganeti.Logging (logWarning)
 import Ganeti.Utils (sepSplit, tryRead, exitIfBad, exitWhen)
 
 -- | Error beautifier.
@@ -110,13 +128,17 @@ loadExternalData opts = do
         | otherwise -> return $ Bad "No backend selected! Exiting."
   now <- getClockTime
 
-  let ldresult = input_data >>= mergeData util_data exTags selInsts exInsts now
+  let ignoreDynU = optIgnoreDynu opts
+      eff_u = if ignoreDynU then [] else util_data
+      ldresult = input_data >>= (if ignoreDynU then clearDynU else return)
+                            >>= mergeData eff_u exTags selInsts exInsts now
   cdata <- exitIfBad "failed to load data, aborting" ldresult
-  let (fix_msgs, nl) = checkData (cdNodes cdata) (cdInstances cdata)
+  cdata' <- if optMonD opts then queryAllMonDDCs cdata opts else return cdata
+  let (fix_msgs, nl) = checkData (cdNodes cdata') (cdInstances cdata')
 
   unless (optVerbose opts == 0) $ maybeShowWarnings fix_msgs
 
-  return cdata {cdNodes = nl}
+  return cdata' {cdNodes = nl}
 
 -- | Function to save the cluster data to a file.
 maybeSaveData :: Maybe FilePath -- ^ The file prefix to save to
@@ -131,3 +153,152 @@ maybeSaveData (Just path) ext msg cdata = do
   writeFile out_path adata
   hPrintf stderr "The cluster state %s has been written to file '%s'\n"
           msg out_path
+
+-- | Type describing a data collector basic information.
+data DataCollector = DataCollector
+  { dName     :: String           -- ^ Name of the data collector
+  , dCategory :: Maybe DCCategory -- ^ The name of the category
+  }
+
+-- | The actual data types for MonD's Data Collectors.
+data Report = CPUavgloadReport CPUavgload
+
+-- | The list of Data Collectors used by hail and hbal.
+collectors :: Options -> [DataCollector]
+collectors opts =
+  if optIgnoreDynu opts
+    then []
+    else [ DataCollector CPUload.dcName CPUload.dcCategory ]
+
+-- | MonDs Data parsed by a mock file. Representing (node name, list of reports
+-- produced by MonDs Data Collectors).
+type MonDData = (String, [DCReport])
+
+-- | A map storing MonDs data.
+type MapMonDData = Map.Map String [DCReport]
+
+-- | Parse MonD data file contents.
+pMonDData :: String -> Result [MonDData]
+pMonDData input =
+  loadJSArray "Parsing MonD's answer" input >>=
+  mapM (pMonDN . J.fromJSObject)
+
+-- | Parse a node's JSON record.
+pMonDN :: JSRecord -> Result MonDData
+pMonDN a = do
+  node <- tryFromObj "Parsing node's name" a "node"
+  reports <- tryFromObj "Parsing node's reports" a "reports"
+  return (node, reports)
+
+-- | Query all MonDs for all Data Collector.
+queryAllMonDDCs :: ClusterData -> Options -> IO ClusterData
+queryAllMonDDCs cdata opts = do
+  map_mDD <-
+    case optMonDFile opts of
+      Nothing -> return Nothing
+      Just fp -> do
+        monDData_contents <- readFile fp
+        monDData <- exitIfBad "can't parse MonD data"
+                    . pMonDData $ monDData_contents
+        return . Just $ Map.fromList monDData
+  let (ClusterData _ nl il _ _) = cdata
+  (nl', il') <- foldM (queryAllMonDs map_mDD) (nl, il) (collectors opts)
+  return $ cdata {cdNodes = nl', cdInstances = il'}
+
+-- | Query all MonDs for a single Data Collector.
+queryAllMonDs :: Maybe MapMonDData -> (Node.List, Instance.List)
+                 -> DataCollector -> IO (Node.List, Instance.List)
+queryAllMonDs m (nl, il) dc = do
+  elems <- mapM (queryAMonD m dc) (Container.elems nl)
+  let elems' = catMaybes elems
+  if length elems == length elems'
+    then
+      let il' = foldl updateUtilData il elems'
+          nl' = zip (Container.keys nl) elems'
+      in return (Container.fromList nl', il')
+    else do
+      logWarning $ "Didn't receive an answer by all MonDs, " ++ dName dc
+                   ++ "'s data will be ignored."
+      return (nl,il)
+
+-- | Query a specified MonD for a Data Collector.
+fromCurl :: DataCollector -> Node.Node -> IO (Maybe DCReport)
+fromCurl dc node = do
+  (code, !body) <-  curlGetString (prepareUrl dc node) []
+  case code of
+    CurlOK ->
+      case J.decodeStrict body :: J.Result DCReport of
+        J.Ok r -> return $ Just r
+        J.Error _ -> return Nothing
+    _ -> do
+      logWarning $ "Failed to contact node's " ++ Node.name node
+                   ++ " MonD for DC " ++ dName dc
+      return Nothing
+
+-- | Return the data from correct combination of a Data Collector
+-- and a DCReport.
+mkReport :: DataCollector -> Maybe DCReport -> Maybe Report
+mkReport dc dcr =
+  case dcr of
+    Nothing -> Nothing
+    Just dcr' ->
+      case () of
+           _ | CPUload.dcName == dName dc ->
+                 case fromJVal (dcReportData dcr') :: Result CPUavgload of
+                   Ok cav -> Just $ CPUavgloadReport cav
+                   Bad _ -> Nothing
+             | otherwise -> Nothing
+
+-- | Get data report for the specified Data Collector and Node from the map.
+fromFile :: DataCollector -> Node.Node -> MapMonDData -> Maybe DCReport
+fromFile dc node m =
+  let matchDCName dcr = dName dc == dcReportName dcr
+  in maybe Nothing (L.find matchDCName) $ Map.lookup (Node.name node) m
+
+-- | Query a MonD for a single Data Collector.
+queryAMonD :: Maybe MapMonDData -> DataCollector -> Node.Node
+              -> IO (Maybe Node.Node)
+queryAMonD m dc node = do
+  dcReport <-
+    case m of
+      Nothing -> fromCurl dc node
+      Just m' -> return $ fromFile dc node m'
+  case mkReport dc dcReport of
+    Nothing -> return Nothing
+    Just report ->
+      case report of
+        CPUavgloadReport cav ->
+          let ct = cavCpuTotal cav
+              du = Node.utilLoad node
+              du' = du {cpuWeight = ct}
+          in return $ Just node {Node.utilLoad = du'}
+
+-- | Update utilization data.
+updateUtilData :: Instance.List -> Node.Node -> Instance.List
+updateUtilData il node =
+  let ct = cpuWeight (Node.utilLoad node)
+      n_uCpu = Node.uCpu node
+      upd inst =
+        if Node.idx node == Instance.pNode inst
+          then
+            let i_vcpus = Instance.vcpus inst
+                i_util = ct / fromIntegral n_uCpu * fromIntegral i_vcpus
+                i_du = Instance.util inst
+                i_du' = i_du {cpuWeight = i_util}
+            in inst {Instance.util = i_du'}
+          else inst
+  in Container.map upd il
+
+-- | Prepare url to query a single collector.
+prepareUrl :: DataCollector -> Node.Node -> URLString
+prepareUrl dc node =
+  Node.name node ++ ":" ++ show C.defaultMondPort ++ "/"
+  ++ show C.mondLatestApiVersion ++ "/report/" ++
+  getDCCName (dCategory dc) ++ "/" ++ dName dc
+
+-- | Get Category Name.
+getDCCName :: Maybe DCCategory -> String
+getDCCName dcc =
+  case dcc of
+    Nothing -> "default"
+    Just c -> getCategoryName c
index 07c7de7..7049ff6 100644 (file)
@@ -28,6 +28,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 
 module Ganeti.HTools.Loader
   ( mergeData
+  , clearDynU
   , checkData
   , assignIndices
   , setMaster
@@ -59,6 +60,7 @@ import Ganeti.BasicTypes
 import qualified Ganeti.Constants as C
 import Ganeti.HTools.Types
 import Ganeti.Utils
+import Ganeti.Types (EvacMode)
 
 -- * Constants
 
@@ -288,6 +290,12 @@ mergeData um extags selinsts exinsts time cdata@(ClusterData gl nl il ctags _) =
          (Ok cdata { cdNodes = nl3, cdInstances = il5 })
          (Bad $ "Unknown instance(s): " ++ show(map lrContent lkp_unknown))
 
+-- | In a cluster description, clear dynamic utilisation information.
+clearDynU :: ClusterData -> Result ClusterData
+clearDynU cdata@(ClusterData _ _ il _ _) =
+  let il2 = Container.map (\ inst -> inst {Instance.util = zeroUtil }) il
+  in Ok cdata { cdInstances = il2 }
+
 -- | Checks the cluster data for consistency.
 checkData :: Node.List -> Instance.List
           -> ([String], Node.List)
index 50009a3..13f5814 100644 (file)
@@ -39,7 +39,8 @@ import Ganeti.Common
 import Ganeti.HTools.CLI
 import Ganeti.HTools.Backend.IAlloc
 import Ganeti.HTools.Loader (Request(..), ClusterData(..))
-import Ganeti.HTools.ExtLoader (maybeSaveData, loadExternalData)
+import Ganeti.HTools.ExtLoader (maybeSaveData, loadExternalData
+                               , queryAllMonDDCs)
 import Ganeti.Utils
 
 -- | Options list and functions.
@@ -51,6 +52,8 @@ options =
     , oDataFile
     , oNodeSim
     , oVerbose
+    , oIgnoreDyn
+    , oMonD
     ]
 
 -- | The list of arguments supported by the program.
@@ -69,8 +72,11 @@ wrapReadRequest opts args = do
       cdata <- loadExternalData opts
       let Request rqt _ = r1
       return $ Request rqt cdata
-    else return r1
-
+    else do
+      let Request rqt cdata = r1
+      cdata' <-
+        if optMonD opts then queryAllMonDDCs cdata opts else return cdata
+      return $ Request rqt cdata'
 
 -- | Main function.
 main :: Options -> [String] -> IO ()
index b278af0..94fb781 100644 (file)
@@ -268,12 +268,12 @@ commitChange client instData = do
   when (isJust arData) $ do
     let tag = arTag $ fromJust arData
     putStrLn (">>> Adding the following tag to " ++ iname ++ ":\n" ++ show tag)
-    execJobsWaitOk' [OpTagsSet (TagInstance iname) [tag]]
+    execJobsWaitOk' [OpTagsSet TagKindInstance [tag] (Just iname)]
 
   unless (null rmTags) $ do
     putStr (">>> Removing the following tags from " ++ iname ++ ":\n" ++
             unlines (map show rmTags))
-    execJobsWaitOk' [OpTagsDel (TagInstance iname) rmTags]
+    execJobsWaitOk' [OpTagsDel TagKindInstance rmTags (Just iname)]
 
   return instData { tagsToRemove = [] }
 
index ece465f..3fdc30a 100644 (file)
@@ -91,6 +91,9 @@ options = do
     , oSelInst
     , oInstMoves
     , oDynuFile
+    , oIgnoreDyn 
+    , oMonD
+    , oMonDDataFile
     , oExTags
     , oExInst
     , oSaveCluster
index f15977c..08db777 100644 (file)
@@ -61,6 +61,9 @@ options = do
     , oVerbose
     , oQuiet
     , oOfflineNode
+    , oIgnoreDyn
+    , oMonD
+    , oMonDDataFile
     ]
 
 -- | The list of arguments supported by the program.
index 764c56c..141164d 100644 (file)
@@ -69,8 +69,12 @@ module Ganeti.HTools.Types
   , FailStats
   , OpResult
   , opToResult
-  , EvacMode(..)
   , ISpec(..)
+  , defMinISpec
+  , defStdISpec
+  , maxDisks
+  , maxNics
+  , defMaxISpec
   , MinMaxISpecs(..)
   , IPolicy(..)
   , defIPolicy
@@ -90,7 +94,7 @@ module Ganeti.HTools.Types
 import qualified Data.Map as M
 import System.Time (ClockTime)
 
-import qualified Ganeti.Constants as C
+import qualified Ganeti.ConstantUtils as ConstantUtils
 import qualified Ganeti.THH as THH
 import Ganeti.BasicTypes
 import Ganeti.Types
@@ -166,42 +170,54 @@ type NetworkID = String
 
 -- | 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 |]
+  [ THH.renameField "MemorySize" $
+    THH.simpleField ConstantUtils.ispecMemSize    [t| Int |]
+  , THH.renameField "CpuCount"   $
+    THH.simpleField ConstantUtils.ispecCpuCount   [t| Int |]
+  , THH.renameField "DiskSize"   $
+    THH.simpleField ConstantUtils.ispecDiskSize   [t| Int |]
+  , THH.renameField "DiskCount"  $
+    THH.simpleField ConstantUtils.ispecDiskCount  [t| Int |]
+  , THH.renameField "NicCount"   $
+    THH.simpleField ConstantUtils.ispecNicCount   [t| Int |]
+  , THH.renameField "SpindleUse" $
+    THH.simpleField ConstantUtils.ispecSpindleUse [t| Int |]
   ])
 
 -- | The default minimum ispec.
 defMinISpec :: ISpec
-defMinISpec = ISpec { iSpecMemorySize = C.ispecsMinmaxDefaultsMinMemorySize
-                    , iSpecCpuCount   = C.ispecsMinmaxDefaultsMinCpuCount
-                    , iSpecDiskSize   = C.ispecsMinmaxDefaultsMinDiskSize
-                    , iSpecDiskCount  = C.ispecsMinmaxDefaultsMinDiskCount
-                    , iSpecNicCount   = C.ispecsMinmaxDefaultsMinNicCount
-                    , iSpecSpindleUse = C.ispecsMinmaxDefaultsMinSpindleUse
+defMinISpec = ISpec { iSpecMemorySize = 128
+                    , iSpecCpuCount   = 1
+                    , iSpecDiskCount  = 1
+                    , iSpecDiskSize   = 1024
+                    , iSpecNicCount   = 1
+                    , iSpecSpindleUse = 1
                     }
 
 -- | 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
+defStdISpec = ISpec { iSpecMemorySize = 128
+                    , iSpecCpuCount   = 1
+                    , iSpecDiskCount  = 1
+                    , iSpecDiskSize   = 1024
+                    , iSpecNicCount   = 1
+                    , iSpecSpindleUse = 1
                     }
 
+maxDisks :: Int
+maxDisks = 16
+
+maxNics :: Int
+maxNics = 8
+
 -- | The default max ispec.
 defMaxISpec :: ISpec
-defMaxISpec = ISpec { iSpecMemorySize = C.ispecsMinmaxDefaultsMaxMemorySize
-                    , iSpecCpuCount   = C.ispecsMinmaxDefaultsMaxCpuCount
-                    , iSpecDiskSize   = C.ispecsMinmaxDefaultsMaxDiskSize
-                    , iSpecDiskCount  = C.ispecsMinmaxDefaultsMaxDiskCount
-                    , iSpecNicCount   = C.ispecsMinmaxDefaultsMaxNicCount
-                    , iSpecSpindleUse = C.ispecsMinmaxDefaultsMaxSpindleUse
+defMaxISpec = ISpec { iSpecMemorySize = 32768
+                    , iSpecCpuCount   = 8
+                    , iSpecDiskCount  = maxDisks
+                    , iSpecDiskSize   = 1024 * 1024
+                    , iSpecNicCount   = maxNics
+                    , iSpecSpindleUse = 12
                     }
 
 -- | Minimum and maximum instance specs type.
@@ -219,14 +235,15 @@ defMinMaxISpecs = [MinMaxISpecs { minMaxISpecsMinSpec = defMinISpec
 -- | Instance policy type.
 $(THH.buildObject "IPolicy" "iPolicy"
   [ THH.renameField "MinMaxISpecs" $
-      THH.simpleField C.ispecsMinmax [t| [MinMaxISpecs] |]
-  , THH.renameField "StdSpec" $ THH.simpleField C.ispecsStd [t| ISpec |]
+      THH.simpleField ConstantUtils.ispecsMinmax [t| [MinMaxISpecs] |]
+  , THH.renameField "StdSpec" $
+      THH.simpleField ConstantUtils.ispecsStd [t| ISpec |]
   , THH.renameField "DiskTemplates" $
-      THH.simpleField C.ipolicyDts [t| [DiskTemplate] |]
+      THH.simpleField ConstantUtils.ipolicyDts [t| [DiskTemplate] |]
   , THH.renameField "VcpuRatio" $
-      THH.simpleField C.ipolicyVcpuRatio [t| Double |]
+      THH.simpleField ConstantUtils.ipolicyVcpuRatio [t| Double |]
   , THH.renameField "SpindleRatio" $
-      THH.simpleField C.ipolicySpindleRatio [t| Double |]
+      THH.simpleField ConstantUtils.ipolicySpindleRatio [t| Double |]
   ])
 
 -- | Converts an ISpec type to a RSpec one.
@@ -239,15 +256,16 @@ rspecFromISpec ispec = RSpec { rspecCpu = iSpecCpuCount ispec
 
 -- | The default instance policy.
 defIPolicy :: IPolicy
-defIPolicy = IPolicy { iPolicyMinMaxISpecs = defMinMaxISpecs
-                     , iPolicyStdSpec = defStdISpec
-                     -- 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
-                     }
+defIPolicy =
+  IPolicy { iPolicyMinMaxISpecs = defMinMaxISpecs
+          , iPolicyStdSpec = defStdISpec
+          -- 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 = ConstantUtils.ipolicyDefaultsVcpuRatio
+          , iPolicySpindleRatio = ConstantUtils.ipolicyDefaultsSpindleRatio
+          }
 
 -- | The dynamic resource specs of a machine (i.e. load or load
 -- capacity, as opposed to size).
@@ -378,31 +396,23 @@ class Element a where
   -- | Updates the index of the element
   setIdx  :: a -> Int -> a
 
--- | The iallocator node-evacuate evac_mode type.
-$(THH.declareSADT "EvacMode"
-       [ ("ChangePrimary",   'C.iallocatorNevacPri)
-       , ("ChangeSecondary", 'C.iallocatorNevacSec)
-       , ("ChangeAll",       'C.iallocatorNevacAll)
-       ])
-$(THH.makeJSONInstance ''EvacMode)
-
 -- | The repair modes for the auto-repair tool.
-$(THH.declareSADT "AutoRepairType"
-       -- Order is important here: from least destructive to most.
-       [ ("ArFixStorage", 'C.autoRepairFixStorage)
-       , ("ArMigrate",    'C.autoRepairMigrate)
-       , ("ArFailover",   'C.autoRepairFailover)
-       , ("ArReinstall",  'C.autoRepairReinstall)
-       ])
+$(THH.declareLADT ''String "AutoRepairType"
+  -- Order is important here: from least destructive to most.
+  [ ("ArFixStorage", "fix-storage")
+  , ("ArMigrate",    "migrate")
+  , ("ArFailover",   "failover")
+  , ("ArReinstall",  "reinstall")
+  ])
 
 -- | The possible auto-repair results.
-$(THH.declareSADT "AutoRepairResult"
-       -- Order is important here: higher results take precedence when an object
-       -- has several result annotations attached.
-       [ ("ArEnoperm", 'C.autoRepairEnoperm)
-       , ("ArSuccess", 'C.autoRepairSuccess)
-       , ("ArFailure", 'C.autoRepairFailure)
-       ])
+$(THH.declareLADT ''String "AutoRepairResult"
+  -- Order is important here: higher results take precedence when an object
+  -- has several result annotations attached.
+  [ ("ArEnoperm", "enoperm")
+  , ("ArSuccess", "success")
+  , ("ArFailure", "failure")
+  ])
 
 -- | The possible auto-repair policy for a given instance.
 data AutoRepairPolicy
diff --git a/src/Ganeti/Hs2Py/GenConstants.hs b/src/Ganeti/Hs2Py/GenConstants.hs
new file mode 100644 (file)
index 0000000..77406f7
--- /dev/null
@@ -0,0 +1,53 @@
+{-| Template Haskell code for Haskell to Python constants.
+
+-}
+
+{-
+
+Copyright (C) 2013 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.
+
+-}
+{-# LANGUAGE TemplateHaskell #-}
+module Ganeti.Hs2Py.GenConstants (genPyConstants) where
+
+import Language.Haskell.TH
+
+import Ganeti.THH
+
+fileFromModule :: Maybe String -> String
+fileFromModule Nothing = ""
+fileFromModule (Just name) = "src/" ++ map dotToSlash name ++ ".hs"
+  where dotToSlash '.' = '/'
+        dotToSlash c = c
+
+comment :: Name -> String
+comment name =
+  "# Generated automatically from Haskell constant '" ++ nameBase name ++
+  "' in file '" ++ fileFromModule (nameModule name) ++ "'"
+
+genList :: Name -> [Name] -> Q [Dec]
+genList name consNames = do
+  let cons = listE $ map (\n -> tupE [mkString n, mkPyValueEx n]) consNames
+  sig <- sigD name [t| [(String, String)] |]
+  fun <- funD name [clause [] (normalB cons) []]
+  return [sig, fun]
+  where mkString n = stringE (comment n ++ "\n" ++ deCamelCase (nameBase n))
+        mkPyValueEx n = [| showValue $(varE n) |]
+
+genPyConstants :: String -> [Name] -> Q [Dec]
+genPyConstants name = genList (mkName name)
diff --git a/src/Ganeti/Hs2Py/GenOpCodes.hs b/src/Ganeti/Hs2Py/GenOpCodes.hs
new file mode 100644 (file)
index 0000000..0683b5d
--- /dev/null
@@ -0,0 +1,81 @@
+{-| GenOpCodes handles Python opcode generation.
+
+GenOpCodes contains the helper functions that generate the Python
+opcodes as strings from the Haskell opcode description.
+
+-}
+
+{-
+
+Copyright (C) 2011, 2012, 2013 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.Hs2Py.GenOpCodes (showPyClasses) where
+
+import Data.List (intercalate, zipWith4)
+
+import Ganeti.OpCodes
+import Ganeti.THH
+
+-- | Generates the Python class docstring.
+pyClassDoc :: String -> String
+pyClassDoc doc
+  | length (lines doc) > 1 =
+    "  \"\"\"" ++ doc ++ "\n\n" ++ "  \"\"\"" ++ "\n"
+  | otherwise =
+    "  \"\"\"" ++ doc ++ "\"\"\"" ++ "\n"
+
+-- | Generates an opcode parameter in Python.
+pyClassField :: String -> String -> Maybe PyValueEx -> String -> String
+pyClassField name typ Nothing doc =
+  "(" ++ intercalate ", " [show name, "None", typ, show doc] ++ ")"
+  
+pyClassField name typ (Just (PyValueEx def)) doc =
+  "(" ++ intercalate ", " [show name, showValue def, typ, show doc] ++ ")"
+  
+-- | Comma intercalates and indents opcode parameters in Python.
+intercalateIndent :: [String] -> String
+intercalateIndent xs = intercalate "," (map ("\n    " ++) xs)
+
+-- | Generates an opcode as a Python class.
+showPyClass :: OpCodeDescriptor -> String
+showPyClass (name, typ, doc, fields, types, defs, docs, dsc) =
+  let
+    baseclass
+      | name == "OpInstanceMultiAlloc" = "OpInstanceMultiAllocBase"
+      | otherwise = "OpCode"
+    opDscField
+      | null dsc = ""
+      | otherwise = "  OP_DSC_FIELD = " ++ show dsc ++ "\n"
+    withLU
+      | name == "OpTestDummy" = "\n  WITH_LU = False"
+      | otherwise = ""
+  in
+   "class " ++ name ++ "(" ++ baseclass ++ "):" ++ "\n" ++
+   pyClassDoc doc ++
+   opDscField ++
+   "  OP_PARAMS = [" ++
+   intercalateIndent (zipWith4 pyClassField fields types defs docs) ++
+   "\n    ]" ++ "\n" ++
+   "  OP_RESULT = " ++ typ ++
+   withLU ++ "\n\n"
+
+-- | Generates all opcodes as Python classes.
+showPyClasses :: String
+showPyClasses = concatMap showPyClass pyClasses
diff --git a/src/Ganeti/Hs2Py/ListConstants.hs.in b/src/Ganeti/Hs2Py/ListConstants.hs.in
new file mode 100644 (file)
index 0000000..ba83955
--- /dev/null
@@ -0,0 +1,41 @@
+{-| Contains the list of the Haskell to Python constants.
+
+Note that this file is autogenerated by the Makefile with a header
+from @ListConstants.hs.in@.
+
+-}
+
+{-
+
+Copyright (C) 2013 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.
+
+-}
+{-# LANGUAGE TemplateHaskell #-}
+module Ganeti.Hs2Py.ListConstants where
+
+import Ganeti.Hs2Py.GenConstants
+import Ganeti.HsConstants
+import Ganeti.PyValueInstances ()
+
+$(genPyConstants "pyConstants"
+  (
+PY_CONSTANT_NAMES[]))
+
+putConstants :: IO ()
+putConstants =
+  sequence_ [ putStrLn (k ++ " = " ++ v) | (k, v) <- pyConstants ]
diff --git a/src/Ganeti/Hs2Py/OpDoc.hs b/src/Ganeti/Hs2Py/OpDoc.hs
new file mode 100644 (file)
index 0000000..f8e6007
--- /dev/null
@@ -0,0 +1,488 @@
+{-| Implementation of the doc strings for the opcodes.
+
+-}
+
+{-
+
+Copyright (C) 2009, 2010, 2011, 2012, 2013 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.Hs2Py.OpDoc where
+
+
+opClusterPostInit :: String
+opClusterPostInit =
+  "Post cluster initialization.\n\
+\\n\
+\  This opcode does not touch the cluster at all. Its purpose is to run hooks\n\
+\  after the cluster has been initialized."
+
+opClusterDestroy :: String
+opClusterDestroy =
+  "Destroy the cluster.\n\
+\\n\
+\  This opcode has no other parameters. All the state is irreversibly\n\
+\  lost after the execution of this opcode."
+
+opClusterQuery :: String
+opClusterQuery =
+  "Query cluster information."
+
+opClusterVerify :: String
+opClusterVerify =
+  "Submits all jobs necessary to verify the cluster."
+
+opClusterVerifyConfig :: String
+opClusterVerifyConfig =
+  "Verify the cluster config."
+
+opClusterVerifyGroup :: String
+opClusterVerifyGroup =
+  "Run verify on a node group from the cluster.\n\
+\\n\
+\  @type skip_checks: C{list}\n\
+\  @ivar skip_checks: steps to be skipped from the verify process; this\n\
+\                     needs to be a subset of\n\
+\                     L{constants.VERIFY_OPTIONAL_CHECKS}; currently\n\
+\                     only L{constants.VERIFY_NPLUSONE_MEM} can be passed"
+
+opClusterVerifyDisks :: String
+opClusterVerifyDisks =
+  "Verify the cluster disks."
+
+opGroupVerifyDisks :: String
+opGroupVerifyDisks =
+  "Verifies the status of all disks in a node group.\n\
+\\n\
+\  Result: a tuple of three elements:\n\
+\    - dict of node names with issues (values: error msg)\n\
+\    - list of instances with degraded disks (that should be activated)\n\
+\    - dict of instances with missing logical volumes (values: (node, vol)\n\
+\      pairs with details about the missing volumes)\n\
+\\n\
+\  In normal operation, all lists should be empty. A non-empty instance\n\
+\  list (3rd element of the result) is still ok (errors were fixed) but\n\
+\  non-empty node list means some node is down, and probably there are\n\
+\  unfixable drbd errors.\n\
+\\n\
+\  Note that only instances that are drbd-based are taken into\n\
+\  consideration. This might need to be revisited in the future."
+
+opClusterRepairDiskSizes :: String
+opClusterRepairDiskSizes =
+  "Verify the disk sizes of the instances and fixes configuration\n\
+\  mismatches.\n\
+\\n\
+\  Parameters: optional instances list, in case we want to restrict the\n\
+\  checks to only a subset of the instances.\n\
+\\n\
+\  Result: a list of tuples, (instance, disk, parameter, new-size) for\n\
+\  changed configurations.\n\
+\\n\
+\  In normal operation, the list should be empty.\n\
+\\n\
+\  @type instances: list\n\
+\  @ivar instances: the list of instances to check, or empty for all instances"
+
+opClusterConfigQuery :: String
+opClusterConfigQuery =
+  "Query cluster configuration values."
+
+opClusterRename :: String
+opClusterRename =
+  "Rename the cluster.\n\
+\\n\
+\  @type name: C{str}\n\
+\  @ivar name: The new name of the cluster. The name and/or the master IP\n\
+\              address will be changed to match the new name and its IP\n\
+\              address."
+
+opClusterSetParams :: String
+opClusterSetParams =
+  "Change the parameters of the cluster.\n\
+\\n\
+\  @type vg_name: C{str} or C{None}\n\
+\  @ivar vg_name: The new volume group name or None to disable LVM usage."
+
+opClusterRedistConf :: String
+opClusterRedistConf =
+  "Force a full push of the cluster configuration."
+
+opClusterActivateMasterIp :: String
+opClusterActivateMasterIp =
+  "Activate the master IP on the master node."
+
+opClusterDeactivateMasterIp :: String
+opClusterDeactivateMasterIp =
+  "Deactivate the master IP on the master node."
+
+opQuery :: String
+opQuery =
+  "Query for resources/items.\n\
+\\n\
+\  @ivar what: Resources to query for, must be one of L{constants.QR_VIA_OP}\n\
+\  @ivar fields: List of fields to retrieve\n\
+\  @ivar qfilter: Query filter"
+
+opQueryFields :: String
+opQueryFields =
+  "Query for available resource/item fields.\n\
+\\n\
+\  @ivar what: Resources to query for, must be one of L{constants.QR_VIA_OP}\n\
+\  @ivar fields: List of fields to retrieve"
+
+opOobCommand :: String
+opOobCommand =
+  "Interact with OOB."
+
+opRestrictedCommand :: String
+opRestrictedCommand =
+  "Runs a restricted command on node(s)."
+
+opNodeRemove :: String
+opNodeRemove =
+  "Remove a node.\n\
+\\n\
+\  @type node_name: C{str}\n\
+\  @ivar node_name: The name of the node to remove. If the node still has\n\
+\                   instances on it, the operation will fail."
+
+opNodeAdd :: String
+opNodeAdd =
+  "Add a node to the cluster.\n\
+\\n\
+\  @type node_name: C{str}\n\
+\  @ivar node_name: The name of the node to add. This can be a short name,\n\
+\                   but it will be expanded to the FQDN.\n\
+\  @type primary_ip: IP address\n\
+\  @ivar primary_ip: The primary IP of the node. This will be ignored when\n\
+\                    the opcode is submitted, but will be filled during the\n\
+\                    node add (so it will be visible in the job query).\n\
+\  @type secondary_ip: IP address\n\
+\  @ivar secondary_ip: The secondary IP of the node. This needs to be passed\n\
+\                      if the cluster has been initialized in 'dual-network'\n\
+\                      mode, otherwise it must not be given.\n\
+\  @type readd: C{bool}\n\
+\  @ivar readd: Whether to re-add an existing node to the cluster. If\n\
+\               this is not passed, then the operation will abort if the node\n\
+\               name is already in the cluster; use this parameter to\n\
+\               'repair' a node that had its configuration broken, or was\n\
+\               reinstalled without removal from the cluster.\n\
+\  @type group: C{str}\n\
+\  @ivar group: The node group to which this node will belong.\n\
+\  @type vm_capable: C{bool}\n\
+\  @ivar vm_capable: The vm_capable node attribute\n\
+\  @type master_capable: C{bool}\n\
+\  @ivar master_capable: The master_capable node attribute"
+
+opNodeQuery :: String
+opNodeQuery =
+  "Compute the list of nodes."
+
+opNodeQueryvols :: String
+opNodeQueryvols =
+  "Get list of volumes on node."
+
+opNodeQueryStorage :: String
+opNodeQueryStorage =
+  "Get information on storage for node(s)."
+
+opNodeModifyStorage :: String
+opNodeModifyStorage =
+  "Modifies the properies of a storage unit"
+
+opRepairNodeStorage :: String
+opRepairNodeStorage =
+  "Repairs the volume group on a node."
+
+opNodeSetParams :: String
+opNodeSetParams =
+  "Change the parameters of a node."
+
+opNodePowercycle :: String
+opNodePowercycle =
+  "Tries to powercycle a node."
+
+opNodeMigrate :: String
+opNodeMigrate =
+  "Migrate all instances from a node."
+
+opNodeEvacuate :: String
+opNodeEvacuate =
+  "Evacuate instances off a number of nodes."
+
+opInstanceCreate :: String
+opInstanceCreate =
+  "Create an instance.\n\
+\\n\
+\  @ivar instance_name: Instance name\n\
+\  @ivar mode: Instance creation mode (one of\
+\ L{constants.INSTANCE_CREATE_MODES})\n\
+\  @ivar source_handshake: Signed handshake from source (remote import only)\n\
+\  @ivar source_x509_ca: Source X509 CA in PEM format (remote import only)\n\
+\  @ivar source_instance_name: Previous name of instance (remote import only)\n\
+\  @ivar source_shutdown_timeout: Shutdown timeout used for source instance\n\
+\    (remote import only)"
+
+opInstanceMultiAlloc :: String
+opInstanceMultiAlloc =
+  "Allocates multiple instances."
+
+opInstanceReinstall :: String
+opInstanceReinstall =
+  "Reinstall an instance's OS."
+
+opInstanceRemove :: String
+opInstanceRemove =
+  "Remove an instance."
+
+opInstanceRename :: String
+opInstanceRename =
+  "Rename an instance."
+
+opInstanceStartup :: String
+opInstanceStartup =
+  "Startup an instance."
+
+opInstanceShutdown :: String
+opInstanceShutdown =
+  "Shutdown an instance."
+
+opInstanceReboot :: String
+opInstanceReboot =
+  "Reboot an instance."
+
+opInstanceReplaceDisks :: String
+opInstanceReplaceDisks =
+  "Replace the disks of an instance."
+
+opInstanceFailover :: String
+opInstanceFailover =
+  "Failover an instance."
+
+opInstanceMigrate :: String
+opInstanceMigrate =
+  "Migrate an instance.\n\
+\\n\
+\  This migrates (without shutting down an instance) to its secondary\n\
+\  node.\n\
+\\n\
+\  @ivar instance_name: the name of the instance\n\
+\  @ivar mode: the migration mode (live, non-live or None for auto)"
+
+opInstanceMove :: String
+opInstanceMove =
+  "Move an instance.\n\
+\\n\
+\  This move (with shutting down an instance and data copying) to an\n\
+\  arbitrary node.\n\
+\\n\
+\  @ivar instance_name: the name of the instance\n\
+\  @ivar target_node: the destination node"
+
+opInstanceConsole :: String
+opInstanceConsole =
+  "Connect to an instance's console."
+
+opInstanceActivateDisks :: String
+opInstanceActivateDisks =
+  "Activate an instance's disks."
+
+opInstanceDeactivateDisks :: String
+opInstanceDeactivateDisks =
+  "Deactivate an instance's disks."
+
+opInstanceRecreateDisks :: String
+opInstanceRecreateDisks =
+  "Recreate an instance's disks."
+
+opInstanceQuery :: String
+opInstanceQuery =
+  "Compute the list of instances."
+
+opInstanceQueryData :: String
+opInstanceQueryData =
+  "Compute the run-time status of instances."
+
+opInstanceSetParams :: String
+opInstanceSetParams =
+  "Change the parameters of an instance."
+
+opInstanceGrowDisk :: String
+opInstanceGrowDisk =
+  "Grow a disk of an instance."
+
+opInstanceChangeGroup :: String
+opInstanceChangeGroup =
+  "Moves an instance to another node group."
+
+opGroupAdd :: String
+opGroupAdd =
+  "Add a node group to the cluster."
+
+opGroupAssignNodes :: String
+opGroupAssignNodes =
+  "Assign nodes to a node group."
+
+opGroupQuery :: String
+opGroupQuery =
+  "Compute the list of node groups."
+
+opGroupSetParams :: String
+opGroupSetParams =
+  "Change the parameters of a node group."
+
+opGroupRemove :: String
+opGroupRemove =
+  "Remove a node group from the cluster."
+
+opGroupRename :: String
+opGroupRename =
+  "Rename a node group in the cluster."
+
+opGroupEvacuate :: String
+opGroupEvacuate =
+  "Evacuate a node group in the cluster."
+
+opOsDiagnose :: String
+opOsDiagnose =
+  "Compute the list of guest operating systems."
+
+opExtStorageDiagnose :: String
+opExtStorageDiagnose =
+  "Compute the list of external storage providers."
+
+opBackupQuery :: String
+opBackupQuery =
+  "Compute the list of exported images."
+
+opBackupPrepare :: String
+opBackupPrepare =
+  "Prepares an instance export.\n\
+\\n\
+\  @ivar instance_name: Instance name\n\
+\  @ivar mode: Export mode (one of L{constants.EXPORT_MODES})"
+
+opBackupExport :: String
+opBackupExport =
+  "Export an instance.\n\
+\\n\
+\  For local exports, the export destination is the node name. For\n\
+\  remote exports, the export destination is a list of tuples, each\n\
+\  consisting of hostname/IP address, port, magic, HMAC and HMAC\n\
+\  salt. The HMAC is calculated using the cluster domain secret over\n\
+\  the value \"${index}:${hostname}:${port}\". The destination X509 CA\n\
+\  must be a signed certificate.\n\
+\\n\
+\  @ivar mode: Export mode (one of L{constants.EXPORT_MODES})\n\
+\  @ivar target_node: Export destination\n\
+\  @ivar x509_key_name: X509 key to use (remote export only)\n\
+\  @ivar destination_x509_ca: Destination X509 CA in PEM format (remote\n\
+\                             export only)"
+
+opBackupRemove :: String
+opBackupRemove =
+  "Remove an instance's export."
+
+opTagsGet :: String
+opTagsGet =
+  "Returns the tags of the given object."
+
+opTagsSearch :: String
+opTagsSearch =
+  "Searches the tags in the cluster for a given pattern."
+
+opTagsSet :: String
+opTagsSet =
+  "Add a list of tags on a given object."
+
+opTagsDel :: String
+opTagsDel =
+  "Remove a list of tags from a given object."
+
+opTestDelay :: String
+opTestDelay =
+  "Sleeps for a configured amount of time.\n\
+\\n\
+\  This is used just for debugging and testing.\n\
+\\n\
+\  Parameters:\n\
+\    - duration: the time to sleep, in seconds\n\
+\    - on_master: if true, sleep on the master\n\
+\    - on_nodes: list of nodes in which to sleep\n\
+\\n\
+\  If the on_master parameter is true, it will execute a sleep on the\n\
+\  master (before any node sleep).\n\
+\\n\
+\  If the on_nodes list is not empty, it will sleep on those nodes\n\
+\  (after the sleep on the master, if that is enabled).\n\
+\\n\
+\  As an additional feature, the case of duration < 0 will be reported\n\
+\  as an execution error, so this opcode can be used as a failure\n\
+\  generator. The case of duration == 0 will not be treated specially."
+
+opTestAllocator :: String
+opTestAllocator =
+  "Allocator framework testing.\n\
+\\n\
+\  This opcode has two modes:\n\
+\    - gather and return allocator input for a given mode (allocate new\n\
+\      or replace secondary) and a given instance definition (direction\n\
+\      'in')\n\
+\    - run a selected allocator for a given operation (as above) and\n\
+\      return the allocator output (direction 'out')"
+
+opTestJqueue :: String
+opTestJqueue =
+  "Utility opcode to test some aspects of the job queue."
+
+opTestDummy :: String
+opTestDummy =
+  "Utility opcode used by unittests."
+
+opNetworkAdd :: String
+opNetworkAdd =
+  "Add an IP network to the cluster."
+
+opNetworkRemove :: String
+opNetworkRemove =
+  "Remove an existing network from the cluster.\n\
+\     Must not be connected to any nodegroup."
+
+opNetworkSetParams :: String
+opNetworkSetParams =
+  "Modify Network's parameters except for IPv4 subnet"
+
+opNetworkConnect :: String
+opNetworkConnect =
+  "Connect a Network to a specific Nodegroup with the defined netparams\n\
+\     (mode, link). Nics in this Network will inherit those params.\n\
+\     Produce errors if a NIC (that its not already assigned to a network)\n\
+\     has an IP that is contained in the Network this will produce error\
+\ unless\n\
+\     --no-conflicts-check is passed."
+
+opNetworkDisconnect :: String
+opNetworkDisconnect =
+  "Disconnect a Network from a Nodegroup. Produce errors if NICs are\n\
+\     present in the Network unless --no-conficts-check option is passed."
+
+opNetworkQuery :: String
+opNetworkQuery =
+  "Compute the list of networks."
diff --git a/src/Ganeti/HsConstants.hs b/src/Ganeti/HsConstants.hs
new file mode 100644 (file)
index 0000000..581ab30
--- /dev/null
@@ -0,0 +1,4232 @@
+{-# OPTIONS -fno-warn-type-defaults #-}
+{-| HsConstants contains the Haskell constants
+
+This is a transitional module complementary to 'Ganeti.Constants'.  It
+is intended to contain the Haskell constants that are meant to be
+generated in Python.
+
+Do not write any definitions in this file other than constants.  Do
+not even write helper functions.  The definitions in this module are
+automatically stripped to build the Makefile.am target
+'ListConstants.hs'.  If there are helper functions in this module,
+they will also be dragged and it will cause compilation to fail.
+Therefore, all helper functions should go to a separate module and
+imported.
+
+-}
+
+{-
+
+Copyright (C) 2013 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.HsConstants where
+
+import Control.Arrow ((***))
+import Data.List ((\\))
+import Data.Map (Map)
+import qualified Data.Map as Map (empty, fromList, keys, insert)
+
+import qualified AutoConf
+import Ganeti.ConstantUtils (PythonChar(..), PythonNone(..), FrozenSet,
+                             Protocol(..), buildVersion)
+import qualified Ganeti.ConstantUtils as ConstantUtils
+import Ganeti.HTools.Types (AutoRepairResult(..), AutoRepairType(..))
+import qualified Ganeti.HTools.Types as Types
+import Ganeti.Logging (SyslogUsage(..))
+import qualified Ganeti.Logging as Logging (syslogUsageToRaw)
+import qualified Ganeti.Runtime as Runtime
+import Ganeti.Runtime (GanetiDaemon(..), MiscGroup(..), GanetiGroup(..),
+                       ExtraLogReason(..))
+import Ganeti.THH (PyValueEx(..))
+import Ganeti.Types
+import qualified Ganeti.Types as Types
+import Ganeti.Confd.Types (ConfdRequestType(..), ConfdReqField(..),
+                           ConfdReplyStatus(..), ConfdNodeRole(..),
+                           ConfdErrorType(..))
+import qualified Ganeti.Confd.Types as Types
+
+{-# ANN module "HLint: ignore Use camelCase" #-}
+
+-- * 'autoconf' constants for Python only ('autotools/build-bash-completion')
+
+htoolsProgs :: [String]
+htoolsProgs = AutoConf.htoolsProgs
+
+-- * 'autoconf' constants for Python only ('lib/constants.py')
+
+drbdBarriers :: String
+drbdBarriers = AutoConf.drbdBarriers
+
+drbdNoMetaFlush :: Bool
+drbdNoMetaFlush = AutoConf.drbdNoMetaFlush
+
+lvmStripecount :: Int
+lvmStripecount = AutoConf.lvmStripecount
+
+hasGnuLn :: Bool
+hasGnuLn = AutoConf.hasGnuLn
+
+-- * 'autoconf' constants for Python only ('lib/pathutils.py')
+
+-- ** Build-time constants
+
+exportDir :: String
+exportDir = AutoConf.exportDir
+
+osSearchPath :: [String]
+osSearchPath = AutoConf.osSearchPath
+
+esSearchPath :: [String]
+esSearchPath = AutoConf.esSearchPath
+
+sshConfigDir :: String
+sshConfigDir = AutoConf.sshConfigDir
+
+xenConfigDir :: String
+xenConfigDir = AutoConf.xenConfigDir
+
+sysconfdir :: String
+sysconfdir = AutoConf.sysconfdir
+
+toolsdir :: String
+toolsdir = AutoConf.toolsdir
+
+localstatedir :: String
+localstatedir = AutoConf.localstatedir
+
+-- ** Paths which don't change for a virtual cluster
+
+pkglibdir :: String
+pkglibdir = AutoConf.pkglibdir
+
+sharedir :: String
+sharedir = AutoConf.sharedir
+
+-- * 'autoconf' constants for Python only ('lib/build/sphinx_ext.py')
+
+manPages :: Map String Int
+manPages = Map.fromList AutoConf.manPages
+
+-- * 'autoconf' constants for QA cluster only ('qa/qa_cluster.py')
+
+versionedsharedir :: String
+versionedsharedir = AutoConf.versionedsharedir
+
+-- * 'autoconf' constants for Python only ('tests/py/docs_unittest.py')
+
+gntScripts :: [String]
+gntScripts = AutoConf.gntScripts
+
+-- * Various versions
+
+releaseVersion :: String
+releaseVersion = AutoConf.packageVersion
+
+versionMajor :: Int
+versionMajor = AutoConf.versionMajor
+
+versionMinor :: Int
+versionMinor = AutoConf.versionMinor
+
+versionRevision :: Int
+versionRevision = AutoConf.versionRevision
+
+dirVersion :: String
+dirVersion = AutoConf.dirVersion
+
+osApiV10 :: Int
+osApiV10 = 10
+
+osApiV15 :: Int
+osApiV15 = 15
+
+osApiV20 :: Int
+osApiV20 = 20
+
+osApiVersions :: FrozenSet Int
+osApiVersions = ConstantUtils.mkSet [osApiV10, osApiV15, osApiV20]
+
+exportVersion :: Int
+exportVersion = 0
+
+rapiVersion :: Int
+rapiVersion = 2
+
+configMajor :: Int
+configMajor = AutoConf.versionMajor
+
+configMinor :: Int
+configMinor = AutoConf.versionMinor
+
+-- | The configuration is supposed to remain stable across
+-- revisions. Therefore, the revision number is cleared to '0'.
+configRevision :: Int
+configRevision = 0
+
+configVersion :: Int
+configVersion = buildVersion configMajor configMinor configRevision
+
+-- | Similarly to the configuration (see 'configRevision'), the
+-- protocols are supposed to remain stable across revisions.
+protocolVersion :: Int
+protocolVersion = buildVersion configMajor configMinor configRevision
+
+-- * User separation
+
+daemonsGroup :: String
+daemonsGroup = Runtime.daemonGroup (ExtraGroup DaemonsGroup)
+
+adminGroup :: String
+adminGroup = Runtime.daemonGroup (ExtraGroup AdminGroup)
+
+masterdUser :: String
+masterdUser = Runtime.daemonUser GanetiMasterd
+
+masterdGroup :: String
+masterdGroup = Runtime.daemonGroup (DaemonGroup GanetiMasterd)
+
+rapiUser :: String
+rapiUser = Runtime.daemonUser GanetiRapi
+
+rapiGroup :: String
+rapiGroup = Runtime.daemonGroup (DaemonGroup GanetiRapi)
+
+confdUser :: String
+confdUser = Runtime.daemonUser GanetiConfd
+
+confdGroup :: String
+confdGroup = Runtime.daemonGroup (DaemonGroup GanetiConfd)
+
+luxidUser :: String
+luxidUser = Runtime.daemonUser GanetiLuxid
+
+luxidGroup :: String
+luxidGroup = Runtime.daemonGroup (DaemonGroup GanetiLuxid)
+
+nodedUser :: String
+nodedUser = Runtime.daemonUser GanetiNoded
+
+nodedGroup :: String
+nodedGroup = Runtime.daemonGroup (DaemonGroup GanetiNoded)
+
+mondUser :: String
+mondUser = Runtime.daemonUser GanetiMond
+
+mondGroup :: String
+mondGroup = Runtime.daemonGroup (DaemonGroup GanetiMond)
+
+sshLoginUser :: String
+sshLoginUser = AutoConf.sshLoginUser
+
+sshConsoleUser :: String
+sshConsoleUser = AutoConf.sshConsoleUser
+
+-- * Cpu pinning separators and constants
+
+cpuPinningSep :: String
+cpuPinningSep = ":"
+
+cpuPinningAll :: String
+cpuPinningAll = "all"
+
+-- | Internal representation of "all"
+cpuPinningAllVal :: Int
+cpuPinningAllVal = -1
+
+-- | One "all" entry in a CPU list means CPU pinning is off
+cpuPinningOff :: [Int]
+cpuPinningOff = [cpuPinningAllVal]
+
+-- | A Xen-specific implementation detail is that 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".
+cpuPinningAllXen :: String
+cpuPinningAllXen = "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
+cpuPinningAllKvm :: Int
+cpuPinningAllKvm = 0xFFFFFFFF
+
+-- * Wipe
+
+ddCmd :: String
+ddCmd = "dd"
+
+-- | 1GB
+maxWipeChunk :: Int
+maxWipeChunk = 1024
+
+minWipeChunkPercent :: Int
+minWipeChunkPercent = 10
+
+-- * Directories
+
+runDirsMode :: Int
+runDirsMode = 0o775
+
+secureDirMode :: Int
+secureDirMode = 0o700
+
+secureFileMode :: Int
+secureFileMode = 0o600
+
+adoptableBlockdevRoot :: String
+adoptableBlockdevRoot = "/dev/disk/"
+
+-- * 'autoconf' enable/disable
+
+enableConfd :: Bool
+enableConfd = AutoConf.enableConfd
+
+enableMond :: Bool
+enableMond = AutoConf.enableMond
+
+enableRestrictedCommands :: Bool
+enableRestrictedCommands = AutoConf.enableRestrictedCommands
+
+enableSplitQuery :: Bool
+enableSplitQuery = AutoConf.enableSplitQuery
+
+-- * SSH constants
+
+ssh :: String
+ssh = "ssh"
+
+scp :: String
+scp = "scp"
+
+-- * Daemons
+
+confd :: String
+confd = Runtime.daemonName GanetiConfd
+
+masterd :: String
+masterd = Runtime.daemonName GanetiMasterd
+
+mond :: String
+mond = Runtime.daemonName GanetiMond
+
+noded :: String
+noded = Runtime.daemonName GanetiNoded
+
+luxid :: String
+luxid = Runtime.daemonName GanetiLuxid
+
+rapi :: String
+rapi = Runtime.daemonName GanetiRapi
+
+daemons :: FrozenSet String
+daemons =
+  ConstantUtils.mkSet [confd,
+                       luxid,
+                       masterd,
+                       mond,
+                       noded,
+                       rapi]
+
+defaultConfdPort :: Int
+defaultConfdPort = 1814
+
+defaultMondPort :: Int
+defaultMondPort = 1815
+
+defaultNodedPort :: Int
+defaultNodedPort = 1811
+
+defaultRapiPort :: Int
+defaultRapiPort = 5080
+
+daemonsPorts :: Map String (Protocol, Int)
+daemonsPorts =
+  Map.fromList [(confd, (Udp, defaultConfdPort)),
+                (mond, (Tcp, defaultMondPort)),
+                (noded, (Tcp, defaultNodedPort)),
+                (rapi, (Tcp, defaultRapiPort)),
+                (ssh, (Tcp, 22))]
+
+firstDrbdPort :: Int
+firstDrbdPort = 11000
+
+lastDrbdPort :: Int
+lastDrbdPort = 14999
+
+daemonsLogbase :: Map String String
+daemonsLogbase =
+  Map.fromList
+  [ (Runtime.daemonName d, Runtime.daemonLogBase d) | d <- [minBound..] ]
+
+daemonsExtraLogbase :: Map String (Map String String)
+daemonsExtraLogbase =
+  Map.fromList $
+  map (Runtime.daemonName *** id)
+  [ (GanetiMond, Map.fromList
+                 [ ("access", Runtime.daemonsExtraLogbase GanetiMond AccessLog)
+                 , ("error", Runtime.daemonsExtraLogbase GanetiMond ErrorLog)
+                 ])
+  ]
+
+extraLogreasonAccess :: String
+extraLogreasonAccess = Runtime.daemonsExtraLogbase GanetiMond AccessLog
+
+extraLogreasonError :: String
+extraLogreasonError = Runtime.daemonsExtraLogbase GanetiMond ErrorLog
+
+devConsole :: String
+devConsole = ConstantUtils.devConsole
+
+procMounts :: String
+procMounts = "/proc/mounts"
+
+-- * Luxi (Local UniX Interface) related constants
+
+luxiEom :: PythonChar
+luxiEom = PythonChar '\x03'
+
+-- | Environment variable for the luxi override socket
+luxiOverride :: String
+luxiOverride = "FORCE_LUXI_SOCKET"
+
+luxiOverrideMaster :: String
+luxiOverrideMaster = "master"
+
+luxiOverrideQuery :: String
+luxiOverrideQuery = "query"
+
+luxiVersion :: Int
+luxiVersion = configVersion
+
+-- * Syslog
+
+syslogUsage :: String
+syslogUsage = AutoConf.syslogUsage
+
+syslogNo :: String
+syslogNo = Logging.syslogUsageToRaw SyslogNo
+
+syslogYes :: String
+syslogYes = Logging.syslogUsageToRaw SyslogYes
+
+syslogOnly :: String
+syslogOnly = Logging.syslogUsageToRaw SyslogOnly
+
+syslogSocket :: String
+syslogSocket = "/dev/log"
+
+exportConfFile :: String
+exportConfFile = "config.ini"
+
+-- * Xen
+
+xenBootloader :: String
+xenBootloader = AutoConf.xenBootloader
+
+xenCmdXl :: String
+xenCmdXl = "xl"
+
+xenCmdXm :: String
+xenCmdXm = "xm"
+
+xenInitrd :: String
+xenInitrd = AutoConf.xenInitrd
+
+xenKernel :: String
+xenKernel = AutoConf.xenKernel
+
+-- FIXME: perhaps rename to 'validXenCommands' for consistency with
+-- other constants
+knownXenCommands :: FrozenSet String
+knownXenCommands = ConstantUtils.mkSet [xenCmdXl, xenCmdXm]
+
+-- * KVM and socat
+
+kvmPath :: String
+kvmPath = AutoConf.kvmPath
+
+kvmKernel :: String
+kvmKernel = AutoConf.kvmKernel
+
+socatEscapeCode :: String
+socatEscapeCode = "0x1d"
+
+socatPath :: String
+socatPath = AutoConf.socatPath
+
+socatUseCompress :: Bool
+socatUseCompress = AutoConf.socatUseCompress
+
+socatUseEscape :: Bool
+socatUseEscape = AutoConf.socatUseEscape
+
+-- * Console types
+
+-- | Display a message for console access
+consMessage :: String
+consMessage = "msg"
+
+-- | Console as SPICE server
+consSpice :: String
+consSpice = "spice"
+
+-- | Console as SSH command
+consSsh :: String
+consSsh = "ssh"
+
+-- | Console as VNC server
+consVnc :: String
+consVnc = "vnc"
+
+consAll :: FrozenSet String
+consAll = ConstantUtils.mkSet [consMessage, consSpice, consSsh, consVnc]
+
+-- | RSA key bit length
+--
+-- 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 2010 on.
+rsaKeyBits :: Int
+rsaKeyBits = 2048
+
+-- | Ciphers allowed for SSL connections.
+--
+-- For the format, see ciphers(1). A better way to disable ciphers
+-- would be to use the exclamation mark (!), but socat versions below
+-- 1.5 can't parse exclamation marks in options properly. When
+-- modifying the ciphers, ensure not to accidentially add something
+-- after it's been removed. Use the "openssl" utility to check the
+-- allowed ciphers, e.g.  "openssl ciphers -v HIGH:-DES".
+opensslCiphers :: String
+opensslCiphers = "HIGH:-DES:-3DES:-EXPORT:-ADH"
+
+-- * X509
+
+-- | commonName (CN) used in certificates
+x509CertCn :: String
+x509CertCn = "ganeti.example.com"
+
+-- | Default validity of certificates in days
+x509CertDefaultValidity :: Int
+x509CertDefaultValidity = 365 * 5
+
+x509CertSignatureHeader :: String
+x509CertSignatureHeader = "X-Ganeti-Signature"
+
+-- | Digest used to sign certificates ("openssl x509" uses SHA1 by default)
+x509CertSignDigest :: String
+x509CertSignDigest = "SHA1"
+
+-- * Import/export daemon mode
+
+iemExport :: String
+iemExport = "export"
+
+iemImport :: String
+iemImport = "import"
+
+-- * Import/export transport compression
+
+iecGzip :: String
+iecGzip = "gzip"
+
+iecNone :: String
+iecNone = "none"
+
+iecAll :: [String]
+iecAll = [iecGzip, iecNone]
+
+ieCustomSize :: String
+ieCustomSize = "fd"
+
+-- * Import/export I/O
+
+-- | Direct file I/O, equivalent to a shell's I/O redirection using
+-- '<' or '>'
+ieioFile :: String
+ieioFile = "file"
+
+-- | Raw block device I/O using "dd"
+ieioRawDisk :: String
+ieioRawDisk = "raw"
+
+-- | OS definition import/export script
+ieioScript :: String
+ieioScript = "script"
+
+-- * Values
+
+valueDefault :: String
+valueDefault = "default"
+
+valueAuto :: String
+valueAuto = "auto"
+
+valueGenerate :: String
+valueGenerate = "generate"
+
+valueNone :: String
+valueNone = "none"
+
+valueTrue :: String
+valueTrue = "true"
+
+valueFalse :: String
+valueFalse = "false"
+
+valueHsNothing :: Map String PythonNone
+valueHsNothing = Map.fromList [("Nothing", PythonNone)]
+
+-- * Hooks
+
+hooksNameCfgupdate :: String
+hooksNameCfgupdate = "config-update"
+
+hooksNameWatcher :: String
+hooksNameWatcher = "watcher"
+
+hooksPath :: String
+hooksPath = "/sbin:/bin:/usr/sbin:/usr/bin"
+
+hooksPhasePost :: String
+hooksPhasePost = "post"
+
+hooksPhasePre :: String
+hooksPhasePre = "pre"
+
+hooksVersion :: Int
+hooksVersion = 2
+
+-- * Hooks subject type (what object type does the LU deal with)
+
+htypeCluster :: String
+htypeCluster = "CLUSTER"
+
+htypeGroup :: String
+htypeGroup = "GROUP"
+
+htypeInstance :: String
+htypeInstance = "INSTANCE"
+
+htypeNetwork :: String
+htypeNetwork = "NETWORK"
+
+htypeNode :: String
+htypeNode = "NODE"
+
+-- * Hkr
+
+hkrSkip :: Int
+hkrSkip = 0
+
+hkrFail :: Int
+hkrFail = 1
+
+hkrSuccess :: Int
+hkrSuccess = 2
+
+-- * Storage types
+
+stBlock :: String
+stBlock = Types.storageTypeToRaw StorageBlock
+
+stDiskless :: String
+stDiskless = Types.storageTypeToRaw StorageDiskless
+
+stExt :: String
+stExt = Types.storageTypeToRaw StorageExt
+
+stFile :: String
+stFile = Types.storageTypeToRaw StorageFile
+
+stLvmPv :: String
+stLvmPv = Types.storageTypeToRaw StorageLvmPv
+
+stLvmVg :: String
+stLvmVg = Types.storageTypeToRaw StorageLvmVg
+
+stRados :: String
+stRados = Types.storageTypeToRaw StorageRados
+
+storageTypes :: FrozenSet String
+storageTypes = ConstantUtils.mkSet $ map Types.storageTypeToRaw [minBound..]
+
+-- | The set of storage types for which storage reporting is available
+--
+-- FIXME: Remove this, once storage reporting is available for all
+-- types.
+stsReport :: FrozenSet String
+stsReport = ConstantUtils.mkSet [stFile, stLvmPv, stLvmVg]
+
+-- * Storage fields
+-- ** First two are valid in LU context only, not passed to backend
+
+sfNode :: String
+sfNode = "node"
+
+sfType :: String
+sfType = "type"
+
+-- ** and the rest are valid in backend
+
+sfAllocatable :: String
+sfAllocatable = Types.storageFieldToRaw SFAllocatable
+
+sfFree :: String
+sfFree = Types.storageFieldToRaw SFFree
+
+sfName :: String
+sfName = Types.storageFieldToRaw SFName
+
+sfSize :: String
+sfSize = Types.storageFieldToRaw SFSize
+
+sfUsed :: String
+sfUsed = Types.storageFieldToRaw SFUsed
+
+validStorageFields :: FrozenSet String
+validStorageFields =
+  ConstantUtils.mkSet $ map Types.storageFieldToRaw [minBound..] ++
+                        [sfNode, sfType]
+
+modifiableStorageFields :: Map String (FrozenSet String)
+modifiableStorageFields =
+  Map.fromList [(Types.storageTypeToRaw StorageLvmPv,
+                 ConstantUtils.mkSet [sfAllocatable])]
+
+-- * Storage operations
+
+soFixConsistency :: String
+soFixConsistency = "fix-consistency"
+
+validStorageOperations :: Map String (FrozenSet String)
+validStorageOperations =
+  Map.fromList [(Types.storageTypeToRaw StorageLvmVg,
+                 ConstantUtils.mkSet [soFixConsistency])]
+
+-- * Volume fields
+
+vfDev :: String
+vfDev = "dev"
+
+vfInstance :: String
+vfInstance = "instance"
+
+vfName :: String
+vfName = "name"
+
+vfNode :: String
+vfNode = "node"
+
+vfPhys :: String
+vfPhys = "phys"
+
+vfSize :: String
+vfSize = "size"
+
+vfVg :: String
+vfVg = "vg"
+
+-- * Local disk status
+
+ldsFaulty :: Int
+ldsFaulty = Types.localDiskStatusToRaw DiskStatusFaulty
+
+ldsOkay :: Int
+ldsOkay = Types.localDiskStatusToRaw DiskStatusOk
+
+ldsUnknown :: Int
+ldsUnknown = Types.localDiskStatusToRaw DiskStatusUnknown
+
+ldsNames :: Map Int String
+ldsNames =
+  Map.fromList [ (Types.localDiskStatusToRaw ds,
+                  localDiskStatusName ds) | ds <- [minBound..] ]
+
+-- * Disk template types
+
+dtDiskless :: String
+dtDiskless = Types.diskTemplateToRaw DTDiskless
+
+dtFile :: String
+dtFile = Types.diskTemplateToRaw DTFile
+
+dtSharedFile :: String
+dtSharedFile = Types.diskTemplateToRaw DTSharedFile
+
+dtPlain :: String
+dtPlain = Types.diskTemplateToRaw DTPlain
+
+dtBlock :: String
+dtBlock = Types.diskTemplateToRaw DTBlock
+
+dtDrbd8 :: String
+dtDrbd8 = Types.diskTemplateToRaw DTDrbd8
+
+dtRbd :: String
+dtRbd = Types.diskTemplateToRaw DTRbd
+
+dtExt :: String
+dtExt = Types.diskTemplateToRaw DTExt
+
+-- | This is used to order determine the default disk template when
+-- the list of enabled disk templates is inferred from the current
+-- state of the cluster.  This only happens on an upgrade from a
+-- version of Ganeti that did not support the 'enabled_disk_templates'
+-- so far.
+diskTemplatePreference :: [String]
+diskTemplatePreference =
+  map Types.diskTemplateToRaw
+  [DTBlock, DTDiskless, DTDrbd8, DTExt, DTFile, DTPlain, DTRbd, DTSharedFile]
+
+diskTemplates :: FrozenSet String
+diskTemplates = ConstantUtils.mkSet $ map Types.diskTemplateToRaw [minBound..]
+
+-- | Disk templates that are enabled by default
+defaultEnabledDiskTemplates :: [String]
+defaultEnabledDiskTemplates = map Types.diskTemplateToRaw [DTDrbd8, DTPlain]
+
+-- | Mapping of disk templates to storage types
+mapDiskTemplateStorageType :: Map String String
+mapDiskTemplateStorageType =
+  Map.fromList $
+  map (Types.diskTemplateToRaw *** Types.storageTypeToRaw)
+  [(DTBlock, StorageBlock),
+   (DTDrbd8, StorageLvmVg),
+   (DTExt, StorageExt),
+   (DTSharedFile, StorageFile),
+   (DTFile, StorageFile),
+   (DTDiskless, StorageDiskless),
+   (DTPlain, StorageLvmVg),
+   (DTRbd, StorageRados)]
+
+-- | The set of network-mirrored disk templates
+dtsIntMirror :: FrozenSet String
+dtsIntMirror = ConstantUtils.mkSet [dtDrbd8]
+
+-- | 'DTDiskless' is 'trivially' externally mirrored
+dtsExtMirror :: FrozenSet String
+dtsExtMirror =
+  ConstantUtils.mkSet $
+  map Types.diskTemplateToRaw [DTDiskless, DTBlock, DTExt, DTSharedFile, DTRbd]
+
+-- | The set of non-lvm-based disk templates
+dtsNotLvm :: FrozenSet String
+dtsNotLvm =
+  ConstantUtils.mkSet $
+  map Types.diskTemplateToRaw
+  [DTSharedFile, DTDiskless, DTBlock, DTExt, DTFile, DTRbd]
+
+-- | The set of disk templates which can be grown
+dtsGrowable :: FrozenSet String
+dtsGrowable =
+  ConstantUtils.mkSet $
+  map Types.diskTemplateToRaw
+  [DTSharedFile, DTDrbd8, DTPlain, DTExt, DTFile, DTRbd]
+
+-- | The set of disk templates that allow adoption
+dtsMayAdopt :: FrozenSet String
+dtsMayAdopt =
+  ConstantUtils.mkSet $ map Types.diskTemplateToRaw [DTBlock, DTPlain]
+
+-- | The set of disk templates that *must* use adoption
+dtsMustAdopt :: FrozenSet String
+dtsMustAdopt = ConstantUtils.mkSet [Types.diskTemplateToRaw DTBlock]
+
+-- | The set of disk templates that allow migrations
+dtsMirrored :: FrozenSet String
+dtsMirrored = dtsIntMirror `ConstantUtils.union` dtsExtMirror
+
+-- | The set of file based disk templates
+dtsFilebased :: FrozenSet String
+dtsFilebased =
+  ConstantUtils.mkSet $ map Types.diskTemplateToRaw [DTSharedFile, DTFile]
+
+-- | The set of disk templates that can be moved by copying
+--
+-- Note: a requirement is that they're not accessed externally or
+-- shared between nodes; in particular, sharedfile is not suitable.
+dtsCopyable :: FrozenSet String
+dtsCopyable =
+  ConstantUtils.mkSet $ map Types.diskTemplateToRaw [DTPlain, DTFile]
+
+-- | The set of disk templates that are supported by exclusive_storage
+dtsExclStorage :: FrozenSet String
+dtsExclStorage = ConstantUtils.mkSet $ map Types.diskTemplateToRaw [DTPlain]
+
+-- | Templates for which we don't perform checks on free space
+dtsNoFreeSpaceCheck :: FrozenSet String
+dtsNoFreeSpaceCheck =
+  ConstantUtils.mkSet $
+  map Types.diskTemplateToRaw [DTExt, DTSharedFile, DTFile, DTRbd]
+
+dtsBlock :: FrozenSet String
+dtsBlock =
+  ConstantUtils.mkSet $
+  map Types.diskTemplateToRaw [DTPlain, DTDrbd8, DTBlock, DTRbd, DTExt]
+
+-- | The set of lvm-based disk templates
+dtsLvm :: FrozenSet String
+dtsLvm = diskTemplates `ConstantUtils.difference` dtsNotLvm
+
+-- * Drbd
+
+drbdHmacAlg :: String
+drbdHmacAlg = "md5"
+
+drbdDefaultNetProtocol :: String
+drbdDefaultNetProtocol = "C"
+
+drbdMigrationNetProtocol :: String
+drbdMigrationNetProtocol = "C"
+
+drbdStatusFile :: String
+drbdStatusFile = "/proc/drbd"
+
+-- | Size of DRBD meta block device
+drbdMetaSize :: Int
+drbdMetaSize = 128
+
+-- * Drbd barrier types
+
+drbdBDiskBarriers :: String
+drbdBDiskBarriers = "b"
+
+drbdBDiskDrain :: String
+drbdBDiskDrain = "d"
+
+drbdBDiskFlush :: String
+drbdBDiskFlush = "f"
+
+drbdBNone :: String
+drbdBNone = "n"
+
+-- | Valid barrier combinations: "n" or any non-null subset of "bfd"
+drbdValidBarrierOpt :: FrozenSet (FrozenSet String)
+drbdValidBarrierOpt =
+  ConstantUtils.mkSet
+  [ ConstantUtils.mkSet [drbdBNone]
+  , ConstantUtils.mkSet [drbdBDiskBarriers]
+  , ConstantUtils.mkSet [drbdBDiskDrain]
+  , ConstantUtils.mkSet [drbdBDiskFlush]
+  , ConstantUtils.mkSet [drbdBDiskDrain, drbdBDiskFlush]
+  , ConstantUtils.mkSet [drbdBDiskBarriers, drbdBDiskDrain]
+  , ConstantUtils.mkSet [drbdBDiskBarriers, drbdBDiskFlush]
+  , ConstantUtils.mkSet [drbdBDiskBarriers, drbdBDiskFlush, drbdBDiskDrain]
+  ]
+
+-- | Rbd tool command
+rbdCmd :: String
+rbdCmd = "rbd"
+
+-- * File backend driver
+
+fdBlktap :: String
+fdBlktap = Types.fileDriverToRaw FileBlktap
+
+fdLoop :: String
+fdLoop = Types.fileDriverToRaw FileLoop
+
+fileDriver :: FrozenSet String
+fileDriver =
+  ConstantUtils.mkSet $
+  map Types.fileDriverToRaw [minBound..]
+
+-- | The set of drbd-like disk types
+dtsDrbd :: FrozenSet String
+dtsDrbd = ConstantUtils.mkSet [Types.diskTemplateToRaw DTDrbd8]
+
+-- * Disk access mode
+
+diskRdonly :: String
+diskRdonly = Types.diskModeToRaw DiskRdOnly
+
+diskRdwr :: String
+diskRdwr = Types.diskModeToRaw DiskRdWr
+
+diskAccessSet :: FrozenSet String
+diskAccessSet = ConstantUtils.mkSet $ map Types.diskModeToRaw [minBound..]
+
+-- * Disk replacement mode
+
+replaceDiskAuto :: String
+replaceDiskAuto = Types.replaceDisksModeToRaw ReplaceAuto
+
+replaceDiskChg :: String
+replaceDiskChg = Types.replaceDisksModeToRaw ReplaceNewSecondary
+
+replaceDiskPri :: String
+replaceDiskPri = Types.replaceDisksModeToRaw ReplaceOnPrimary
+
+replaceDiskSec :: String
+replaceDiskSec = Types.replaceDisksModeToRaw ReplaceOnSecondary
+
+replaceModes :: FrozenSet String
+replaceModes =
+  ConstantUtils.mkSet $ map Types.replaceDisksModeToRaw [minBound..]
+
+-- * Instance export mode
+
+exportModeLocal :: String
+exportModeLocal = Types.exportModeToRaw ExportModeLocal
+
+exportModeRemote :: String
+exportModeRemote = Types.exportModeToRaw ExportModeRemote
+
+exportModes :: FrozenSet String
+exportModes = ConstantUtils.mkSet $ map Types.exportModeToRaw [minBound..]
+
+-- * Instance creation modes
+
+instanceCreate :: String
+instanceCreate = Types.instCreateModeToRaw InstCreate
+
+instanceImport :: String
+instanceImport = Types.instCreateModeToRaw InstImport
+
+instanceRemoteImport :: String
+instanceRemoteImport = Types.instCreateModeToRaw InstRemoteImport
+
+instanceCreateModes :: FrozenSet String
+instanceCreateModes =
+  ConstantUtils.mkSet $ map Types.instCreateModeToRaw [minBound..]
+
+-- * Remote import/export handshake message and version
+
+rieHandshake :: String
+rieHandshake = "Hi, I'm Ganeti"
+
+rieVersion :: Int
+rieVersion = 0
+
+-- | Remote import/export certificate validity in seconds
+rieCertValidity :: Int
+rieCertValidity = 24 * 60 * 60
+
+-- | Export only: how long to wait per connection attempt (seconds)
+rieConnectAttemptTimeout :: Int
+rieConnectAttemptTimeout = 20
+
+-- | Export only: number of attempts to connect
+rieConnectRetries :: Int
+rieConnectRetries = 10
+
+-- | Overall timeout for establishing connection
+rieConnectTimeout :: Int
+rieConnectTimeout = 180
+
+-- | Give child process up to 5 seconds to exit after sending a signal
+childLingerTimeout :: Double
+childLingerTimeout = 5.0
+
+-- * Import/export config options
+
+inisectBep :: String
+inisectBep = "backend"
+
+inisectExp :: String
+inisectExp = "export"
+
+inisectHyp :: String
+inisectHyp = "hypervisor"
+
+inisectIns :: String
+inisectIns = "instance"
+
+inisectOsp :: String
+inisectOsp = "os"
+
+-- * Dynamic device modification
+
+ddmAdd :: String
+ddmAdd = Types.ddmFullToRaw DdmFullAdd
+
+ddmModify :: String
+ddmModify = Types.ddmFullToRaw DdmFullModify
+
+ddmRemove :: String
+ddmRemove = Types.ddmFullToRaw DdmFullRemove
+
+ddmsValues :: FrozenSet String
+ddmsValues = ConstantUtils.mkSet [ddmAdd, ddmRemove]
+
+ddmsValuesWithModify :: FrozenSet String
+ddmsValuesWithModify = ConstantUtils.mkSet $ map Types.ddmFullToRaw [minBound..]
+
+-- * Common exit codes
+
+exitSuccess :: Int
+exitSuccess = 0
+
+exitFailure :: Int
+exitFailure = ConstantUtils.exitFailure
+
+exitNotcluster :: Int
+exitNotcluster = 5
+
+exitNotmaster :: Int
+exitNotmaster = 11
+
+exitNodesetupError :: Int
+exitNodesetupError = 12
+
+-- | Need user confirmation
+exitConfirmation :: Int
+exitConfirmation = 13
+
+-- | Exit code for query operations with unknown fields
+exitUnknownField :: Int
+exitUnknownField = 14
+
+-- * Tags
+
+tagCluster :: String
+tagCluster = Types.tagKindToRaw TagKindCluster
+
+tagInstance :: String
+tagInstance = Types.tagKindToRaw TagKindInstance
+
+tagNetwork :: String
+tagNetwork = Types.tagKindToRaw TagKindNetwork
+
+tagNode :: String
+tagNode = Types.tagKindToRaw TagKindNode
+
+tagNodegroup :: String
+tagNodegroup = Types.tagKindToRaw TagKindGroup
+
+validTagTypes :: FrozenSet String
+validTagTypes = ConstantUtils.mkSet $ map Types.tagKindToRaw [minBound..]
+
+maxTagLen :: Int
+maxTagLen = 128
+
+maxTagsPerObj :: Int
+maxTagsPerObj = 4096
+
+-- * Others
+
+defaultBridge :: String
+defaultBridge = "xen-br0"
+
+defaultOvs :: String
+defaultOvs = "switch1"
+
+-- | 60 MiB/s, expressed in KiB/s
+classicDrbdSyncSpeed :: Int
+classicDrbdSyncSpeed = 60 * 1024
+
+ip4AddressAny :: String
+ip4AddressAny = "0.0.0.0"
+
+ip4AddressLocalhost :: String
+ip4AddressLocalhost = "127.0.0.1"
+
+ip6AddressAny :: String
+ip6AddressAny = "::"
+
+ip6AddressLocalhost :: String
+ip6AddressLocalhost = "::1"
+
+ip4Version :: Int
+ip4Version = 4
+
+ip6Version :: Int
+ip6Version = 6
+
+validIpVersions :: FrozenSet Int
+validIpVersions = ConstantUtils.mkSet [ip4Version, ip6Version]
+
+tcpPingTimeout :: Int
+tcpPingTimeout = 10
+
+defaultVg :: String
+defaultVg = "xenvg"
+
+defaultDrbdHelper :: String
+defaultDrbdHelper = "/bin/true"
+
+minVgSize :: Int
+minVgSize = 20480
+
+defaultMacPrefix :: String
+defaultMacPrefix = "aa:00:00"
+
+-- | Default maximum instance wait time, in seconds.
+defaultShutdownTimeout :: Int
+defaultShutdownTimeout = 120
+
+-- | Node clock skew in seconds
+nodeMaxClockSkew :: Int
+nodeMaxClockSkew = 150
+
+-- | Time for an intra-cluster disk transfer to wait for a connection
+diskTransferConnectTimeout :: Int
+diskTransferConnectTimeout = 60
+
+-- | Disk index separator
+diskSeparator :: String
+diskSeparator = AutoConf.diskSeparator
+
+ipCommandPath :: String
+ipCommandPath = AutoConf.ipPath
+
+-- | Key for job IDs in opcode result
+jobIdsKey :: String
+jobIdsKey = "jobs"
+
+-- * Runparts results
+
+runpartsErr :: Int
+runpartsErr = 2
+
+runpartsRun :: Int
+runpartsRun = 1
+
+runpartsSkip :: Int
+runpartsSkip = 0
+
+runpartsStatus :: [Int]
+runpartsStatus = [runpartsErr, runpartsRun, runpartsSkip]
+
+-- * RPC
+
+rpcEncodingNone :: Int
+rpcEncodingNone = 0
+
+rpcEncodingZlibBase64 :: Int
+rpcEncodingZlibBase64 = 1
+
+-- * Timeout table
+--
+-- Various time constants for the timeout table
+
+rpcTmoUrgent :: Int
+rpcTmoUrgent = Types.rpcTimeoutToRaw Urgent
+
+rpcTmoFast :: Int
+rpcTmoFast = Types.rpcTimeoutToRaw Fast
+
+rpcTmoNormal :: Int
+rpcTmoNormal = Types.rpcTimeoutToRaw Normal
+
+rpcTmoSlow :: Int
+rpcTmoSlow = Types.rpcTimeoutToRaw Slow
+
+-- | 'rpcTmo_4hrs' contains an underscore to circumvent a limitation
+-- in the 'Ganeti.THH.deCamelCase' function and generate the correct
+-- Python name.
+rpcTmo_4hrs :: Int
+rpcTmo_4hrs = Types.rpcTimeoutToRaw FourHours
+
+-- | 'rpcTmo_1day' contains an underscore to circumvent a limitation
+-- in the 'Ganeti.THH.deCamelCase' function and generate the correct
+-- Python name.
+rpcTmo_1day :: Int
+rpcTmo_1day = Types.rpcTimeoutToRaw OneDay
+
+-- | Timeout for connecting to nodes (seconds)
+rpcConnectTimeout :: Int
+rpcConnectTimeout = 5
+
+-- OS
+
+osScriptCreate :: String
+osScriptCreate = "create"
+
+osScriptExport :: String
+osScriptExport = "export"
+
+osScriptImport :: String
+osScriptImport = "import"
+
+osScriptRename :: String
+osScriptRename = "rename"
+
+osScriptVerify :: String
+osScriptVerify = "verify"
+
+osScripts :: [String]
+osScripts = [osScriptCreate, osScriptExport, osScriptImport, osScriptRename,
+             osScriptVerify]
+
+osApiFile :: String
+osApiFile = "ganeti_api_version"
+
+osVariantsFile :: String
+osVariantsFile = "variants.list"
+
+osParametersFile :: String
+osParametersFile = "parameters.list"
+
+osValidateParameters :: String
+osValidateParameters = "parameters"
+
+osValidateCalls :: FrozenSet String
+osValidateCalls = ConstantUtils.mkSet [osValidateParameters]
+
+-- | External Storage (ES) related constants
+
+esActionAttach :: String
+esActionAttach = "attach"
+
+esActionCreate :: String
+esActionCreate = "create"
+
+esActionDetach :: String
+esActionDetach = "detach"
+
+esActionGrow :: String
+esActionGrow = "grow"
+
+esActionRemove :: String
+esActionRemove = "remove"
+
+esActionSetinfo :: String
+esActionSetinfo = "setinfo"
+
+esActionVerify :: String
+esActionVerify = "verify"
+
+esScriptCreate :: String
+esScriptCreate = esActionCreate
+
+esScriptRemove :: String
+esScriptRemove = esActionRemove
+
+esScriptGrow :: String
+esScriptGrow = esActionGrow
+
+esScriptAttach :: String
+esScriptAttach = esActionAttach
+
+esScriptDetach :: String
+esScriptDetach = esActionDetach
+
+esScriptSetinfo :: String
+esScriptSetinfo = esActionSetinfo
+
+esScriptVerify :: String
+esScriptVerify = esActionVerify
+
+esScripts :: FrozenSet String
+esScripts =
+  ConstantUtils.mkSet [esScriptAttach,
+                       esScriptCreate,
+                       esScriptDetach,
+                       esScriptGrow,
+                       esScriptRemove,
+                       esScriptSetinfo,
+                       esScriptVerify]
+
+esParametersFile :: String
+esParametersFile = "parameters.list"
+
+-- * Reboot types
+
+instanceRebootSoft :: String
+instanceRebootSoft = Types.rebootTypeToRaw RebootSoft
+
+instanceRebootHard :: String
+instanceRebootHard = Types.rebootTypeToRaw RebootHard
+
+instanceRebootFull :: String
+instanceRebootFull = Types.rebootTypeToRaw RebootFull
+
+rebootTypes :: FrozenSet String
+rebootTypes = ConstantUtils.mkSet $ map Types.rebootTypeToRaw [minBound..]
+
+-- * Instance reboot behaviors
+
+instanceRebootAllowed :: String
+instanceRebootAllowed = "reboot"
+
+instanceRebootExit :: String
+instanceRebootExit = "exit"
+
+rebootBehaviors :: [String]
+rebootBehaviors = [instanceRebootAllowed, instanceRebootExit]
+
+-- * VTypes
+
+vtypeBool :: VType
+vtypeBool = VTypeBool
+
+vtypeInt :: VType
+vtypeInt = VTypeInt
+
+vtypeMaybeString :: VType
+vtypeMaybeString = VTypeMaybeString
+
+-- | Size in MiBs
+vtypeSize :: VType
+vtypeSize = VTypeSize
+
+vtypeString :: VType
+vtypeString = VTypeString
+
+enforceableTypes :: FrozenSet VType
+enforceableTypes = ConstantUtils.mkSet [minBound..]
+
+-- | Constant representing that the user does not specify any IP version
+ifaceNoIpVersionSpecified :: Int
+ifaceNoIpVersionSpecified = 0
+
+validSerialSpeeds :: [Int]
+validSerialSpeeds =
+  [75,
+   110,
+   300,
+   600,
+   1200,
+   1800,
+   2400,
+   4800,
+   9600,
+   14400,
+   19200,
+   28800,
+   38400,
+   57600,
+   115200,
+   230400,
+   345600,
+   460800]
+
+-- * HV parameter names (global namespace)
+
+hvAcpi :: String
+hvAcpi = "acpi"
+
+hvBlockdevPrefix :: String
+hvBlockdevPrefix = "blockdev_prefix"
+
+hvBootloaderArgs :: String
+hvBootloaderArgs = "bootloader_args"
+
+hvBootloaderPath :: String
+hvBootloaderPath = "bootloader_path"
+
+hvBootOrder :: String
+hvBootOrder = "boot_order"
+
+hvCdromImagePath :: String
+hvCdromImagePath = "cdrom_image_path"
+
+hvCpuCap :: String
+hvCpuCap = "cpu_cap"
+
+hvCpuCores :: String
+hvCpuCores = "cpu_cores"
+
+hvCpuMask :: String
+hvCpuMask = "cpu_mask"
+
+hvCpuSockets :: String
+hvCpuSockets = "cpu_sockets"
+
+hvCpuThreads :: String
+hvCpuThreads = "cpu_threads"
+
+hvCpuType :: String
+hvCpuType = "cpu_type"
+
+hvCpuWeight :: String
+hvCpuWeight = "cpu_weight"
+
+hvDeviceModel :: String
+hvDeviceModel = "device_model"
+
+hvDiskCache :: String
+hvDiskCache = "disk_cache"
+
+hvDiskType :: String
+hvDiskType = "disk_type"
+
+hvInitrdPath :: String
+hvInitrdPath = "initrd_path"
+
+hvInitScript :: String
+hvInitScript = "init_script"
+
+hvKernelArgs :: String
+hvKernelArgs = "kernel_args"
+
+hvKernelPath :: String
+hvKernelPath = "kernel_path"
+
+hvKeymap :: String
+hvKeymap = "keymap"
+
+hvKvmCdrom2ImagePath :: String
+hvKvmCdrom2ImagePath = "cdrom2_image_path"
+
+hvKvmCdromDiskType :: String
+hvKvmCdromDiskType = "cdrom_disk_type"
+
+hvKvmExtra :: String
+hvKvmExtra = "kvm_extra"
+
+hvKvmFlag :: String
+hvKvmFlag = "kvm_flag"
+
+hvKvmFloppyImagePath :: String
+hvKvmFloppyImagePath = "floppy_image_path"
+
+hvKvmMachineVersion :: String
+hvKvmMachineVersion = "machine_version"
+
+hvKvmPath :: String
+hvKvmPath = "kvm_path"
+
+hvKvmSpiceAudioCompr :: String
+hvKvmSpiceAudioCompr = "spice_playback_compression"
+
+hvKvmSpiceBind :: String
+hvKvmSpiceBind = "spice_bind"
+
+hvKvmSpiceIpVersion :: String
+hvKvmSpiceIpVersion = "spice_ip_version"
+
+hvKvmSpiceJpegImgCompr :: String
+hvKvmSpiceJpegImgCompr = "spice_jpeg_wan_compression"
+
+hvKvmSpiceLosslessImgCompr :: String
+hvKvmSpiceLosslessImgCompr = "spice_image_compression"
+
+hvKvmSpicePasswordFile :: String
+hvKvmSpicePasswordFile = "spice_password_file"
+
+hvKvmSpiceStreamingVideoDetection :: String
+hvKvmSpiceStreamingVideoDetection = "spice_streaming_video"
+
+hvKvmSpiceTlsCiphers :: String
+hvKvmSpiceTlsCiphers = "spice_tls_ciphers"
+
+hvKvmSpiceUseTls :: String
+hvKvmSpiceUseTls = "spice_use_tls"
+
+hvKvmSpiceUseVdagent :: String
+hvKvmSpiceUseVdagent = "spice_use_vdagent"
+
+hvKvmSpiceZlibGlzImgCompr :: String
+hvKvmSpiceZlibGlzImgCompr = "spice_zlib_glz_wan_compression"
+
+hvKvmUseChroot :: String
+hvKvmUseChroot = "use_chroot"
+
+hvMemPath :: String
+hvMemPath = "mem_path"
+
+hvMigrationBandwidth :: String
+hvMigrationBandwidth = "migration_bandwidth"
+
+hvMigrationDowntime :: String
+hvMigrationDowntime = "migration_downtime"
+
+hvMigrationMode :: String
+hvMigrationMode = "migration_mode"
+
+hvMigrationPort :: String
+hvMigrationPort = "migration_port"
+
+hvNicType :: String
+hvNicType = "nic_type"
+
+hvPae :: String
+hvPae = "pae"
+
+hvPassthrough :: String
+hvPassthrough = "pci_pass"
+
+hvRebootBehavior :: String
+hvRebootBehavior = "reboot_behavior"
+
+hvRootPath :: String
+hvRootPath = "root_path"
+
+hvSecurityDomain :: String
+hvSecurityDomain = "security_domain"
+
+hvSecurityModel :: String
+hvSecurityModel = "security_model"
+
+hvSerialConsole :: String
+hvSerialConsole = "serial_console"
+
+hvSerialSpeed :: String
+hvSerialSpeed = "serial_speed"
+
+hvSoundhw :: String
+hvSoundhw = "soundhw"
+
+hvUsbDevices :: String
+hvUsbDevices = "usb_devices"
+
+hvUsbMouse :: String
+hvUsbMouse = "usb_mouse"
+
+hvUseBootloader :: String
+hvUseBootloader = "use_bootloader"
+
+hvUseLocaltime :: String
+hvUseLocaltime = "use_localtime"
+
+hvVga :: String
+hvVga = "vga"
+
+hvVhostNet :: String
+hvVhostNet = "vhost_net"
+
+hvVifScript :: String
+hvVifScript = "vif_script"
+
+hvVifType :: String
+hvVifType = "vif_type"
+
+hvViridian :: String
+hvViridian = "viridian"
+
+hvVncBindAddress :: String
+hvVncBindAddress = "vnc_bind_address"
+
+hvVncPasswordFile :: String
+hvVncPasswordFile = "vnc_password_file"
+
+hvVncTls :: String
+hvVncTls = "vnc_tls"
+
+hvVncX509 :: String
+hvVncX509 = "vnc_x509_path"
+
+hvVncX509Verify :: String
+hvVncX509Verify = "vnc_x509_verify"
+
+hvVnetHdr :: String
+hvVnetHdr = "vnet_hdr"
+
+hvXenCmd :: String
+hvXenCmd = "xen_cmd"
+
+hvXenCpuid :: String
+hvXenCpuid = "cpuid"
+
+hvsParameterTitles :: Map String String
+hvsParameterTitles =
+  Map.fromList
+  [(hvAcpi, "ACPI"),
+   (hvBootOrder, "Boot_order"),
+   (hvCdromImagePath, "CDROM_image_path"),
+   (hvCpuType, "cpu_type"),
+   (hvDiskType, "Disk_type"),
+   (hvInitrdPath, "Initrd_path"),
+   (hvKernelPath, "Kernel_path"),
+   (hvNicType, "NIC_type"),
+   (hvPae, "PAE"),
+   (hvPassthrough, "pci_pass"),
+   (hvVncBindAddress, "VNC_bind_address")]
+
+-- * Migration statuses
+
+hvMigrationActive :: String
+hvMigrationActive = "active"
+
+hvMigrationCancelled :: String
+hvMigrationCancelled = "cancelled"
+
+hvMigrationCompleted :: String
+hvMigrationCompleted = "completed"
+
+hvMigrationFailed :: String
+hvMigrationFailed = "failed"
+
+hvMigrationValidStatuses :: FrozenSet String
+hvMigrationValidStatuses =
+  ConstantUtils.mkSet [hvMigrationActive,
+                       hvMigrationCancelled,
+                       hvMigrationCompleted,
+                       hvMigrationFailed]
+
+hvMigrationFailedStatuses :: FrozenSet String
+hvMigrationFailedStatuses =
+  ConstantUtils.mkSet [hvMigrationFailed, hvMigrationCancelled]
+
+-- | KVM-specific statuses
+--
+-- FIXME: this constant seems unnecessary
+hvKvmMigrationValidStatuses :: FrozenSet String
+hvKvmMigrationValidStatuses = hvMigrationValidStatuses
+
+-- | Node info keys
+hvNodeinfoKeyVersion :: String
+hvNodeinfoKeyVersion = "hv_version"
+
+-- * Hypervisor state
+
+hvstCpuNode :: String
+hvstCpuNode = "cpu_node"
+
+hvstCpuTotal :: String
+hvstCpuTotal = "cpu_total"
+
+hvstMemoryHv :: String
+hvstMemoryHv = "mem_hv"
+
+hvstMemoryNode :: String
+hvstMemoryNode = "mem_node"
+
+hvstMemoryTotal :: String
+hvstMemoryTotal = "mem_total"
+
+hvstsParameters :: FrozenSet String
+hvstsParameters =
+  ConstantUtils.mkSet [hvstCpuNode,
+                       hvstCpuTotal,
+                       hvstMemoryHv,
+                       hvstMemoryNode,
+                       hvstMemoryTotal]
+
+hvstDefaults :: Map String Int
+hvstDefaults =
+  Map.fromList
+  [(hvstCpuNode, 1),
+   (hvstCpuTotal, 1),
+   (hvstMemoryHv, 0),
+   (hvstMemoryTotal, 0),
+   (hvstMemoryNode, 0)]
+
+hvstsParameterTypes :: Map String VType
+hvstsParameterTypes =
+  Map.fromList [(hvstMemoryTotal, VTypeInt),
+                (hvstMemoryNode, VTypeInt),
+                (hvstMemoryHv, VTypeInt),
+                (hvstCpuTotal, VTypeInt),
+                (hvstCpuNode, VTypeInt)]
+
+-- * Disk state
+
+dsDiskOverhead :: String
+dsDiskOverhead = "disk_overhead"
+
+dsDiskReserved :: String
+dsDiskReserved = "disk_reserved"
+
+dsDiskTotal :: String
+dsDiskTotal = "disk_total"
+
+dsDefaults :: Map String Int
+dsDefaults =
+  Map.fromList
+  [(dsDiskTotal, 0),
+   (dsDiskReserved, 0),
+   (dsDiskOverhead, 0)]
+
+dssParameterTypes :: Map String VType
+dssParameterTypes =
+  Map.fromList [(dsDiskTotal, VTypeInt),
+                (dsDiskReserved, VTypeInt),
+                (dsDiskOverhead, VTypeInt)]
+
+dssParameters :: FrozenSet String
+dssParameters =
+  ConstantUtils.mkSet [dsDiskTotal, dsDiskReserved, dsDiskOverhead]
+
+dsValidTypes :: FrozenSet String
+dsValidTypes = ConstantUtils.mkSet [Types.diskTemplateToRaw DTPlain]
+
+-- Backend parameter names
+
+beAlwaysFailover :: String
+beAlwaysFailover = "always_failover"
+
+beAutoBalance :: String
+beAutoBalance = "auto_balance"
+
+beMaxmem :: String
+beMaxmem = "maxmem"
+
+-- | Deprecated and replaced by max and min mem
+beMemory :: String
+beMemory = "memory"
+
+beMinmem :: String
+beMinmem = "minmem"
+
+beSpindleUse :: String
+beSpindleUse = "spindle_use"
+
+beVcpus :: String
+beVcpus = "vcpus"
+
+besParameterTypes :: Map String VType
+besParameterTypes =
+  Map.fromList [(beAlwaysFailover, VTypeBool),
+                (beAutoBalance, VTypeBool),
+                (beMaxmem, VTypeSize),
+                (beMinmem, VTypeSize),
+                (beSpindleUse, VTypeInt),
+                (beVcpus, VTypeInt)]
+
+besParameterTitles :: Map String String
+besParameterTitles =
+  Map.fromList [(beAutoBalance, "Auto_balance"),
+                (beMinmem, "ConfigMinMem"),
+                (beVcpus, "ConfigVCPUs"),
+                (beMaxmem, "ConfigMaxMem")]
+
+besParameterCompat :: Map String VType
+besParameterCompat = Map.insert beMemory VTypeSize besParameterTypes
+
+besParameters :: FrozenSet String
+besParameters =
+  ConstantUtils.mkSet [beAlwaysFailover,
+                       beAutoBalance,
+                       beMaxmem,
+                       beMinmem,
+                       beSpindleUse,
+                       beVcpus]
+
+-- | Instance specs
+--
+-- FIXME: these should be associated with 'Ganeti.HTools.Types.ISpec'
+
+ispecMemSize :: String
+ispecMemSize = ConstantUtils.ispecMemSize
+
+ispecCpuCount :: String
+ispecCpuCount = ConstantUtils.ispecCpuCount
+
+ispecDiskCount :: String
+ispecDiskCount = ConstantUtils.ispecDiskCount
+
+ispecDiskSize :: String
+ispecDiskSize = ConstantUtils.ispecDiskSize
+
+ispecNicCount :: String
+ispecNicCount = ConstantUtils.ispecNicCount
+
+ispecSpindleUse :: String
+ispecSpindleUse = ConstantUtils.ispecSpindleUse
+
+ispecsParameterTypes :: Map String VType
+ispecsParameterTypes =
+  Map.fromList
+  [(ConstantUtils.ispecDiskSize, VTypeInt),
+   (ConstantUtils.ispecCpuCount, VTypeInt),
+   (ConstantUtils.ispecSpindleUse, VTypeInt),
+   (ConstantUtils.ispecMemSize, VTypeInt),
+   (ConstantUtils.ispecNicCount, VTypeInt),
+   (ConstantUtils.ispecDiskCount, VTypeInt)]
+
+ispecsParameters :: FrozenSet String
+ispecsParameters =
+  ConstantUtils.mkSet [ConstantUtils.ispecCpuCount,
+                       ConstantUtils.ispecDiskCount,
+                       ConstantUtils.ispecDiskSize,
+                       ConstantUtils.ispecMemSize,
+                       ConstantUtils.ispecNicCount,
+                       ConstantUtils.ispecSpindleUse]
+
+ispecsMinmax :: String
+ispecsMinmax = ConstantUtils.ispecsMinmax
+
+ispecsMax :: String
+ispecsMax = "max"
+
+ispecsMin :: String
+ispecsMin = "min"
+
+ispecsStd :: String
+ispecsStd = ConstantUtils.ispecsStd
+
+ipolicyDts :: String
+ipolicyDts = ConstantUtils.ipolicyDts
+
+ipolicyVcpuRatio :: String
+ipolicyVcpuRatio = ConstantUtils.ipolicyVcpuRatio
+
+ipolicySpindleRatio :: String
+ipolicySpindleRatio = ConstantUtils.ipolicySpindleRatio
+
+ispecsMinmaxKeys :: FrozenSet String
+ispecsMinmaxKeys = ConstantUtils.mkSet [ispecsMax, ispecsMin]
+
+ipolicyParameters :: FrozenSet String
+ipolicyParameters =
+  ConstantUtils.mkSet [ConstantUtils.ipolicyVcpuRatio,
+                       ConstantUtils.ipolicySpindleRatio]
+
+ipolicyAllKeys :: FrozenSet String
+ipolicyAllKeys =
+  ConstantUtils.union ipolicyParameters $
+  ConstantUtils.mkSet [ConstantUtils.ipolicyDts,
+                       ConstantUtils.ispecsMinmax,
+                       ispecsStd]
+
+-- | Node parameter names
+
+ndExclusiveStorage :: String
+ndExclusiveStorage = "exclusive_storage"
+
+ndOobProgram :: String
+ndOobProgram = "oob_program"
+
+ndSpindleCount :: String
+ndSpindleCount = "spindle_count"
+
+ndOvs :: String
+ndOvs = "ovs"
+
+ndOvsLink :: String
+ndOvsLink = "ovs_link"
+
+ndOvsName :: String
+ndOvsName = "ovs_name"
+
+ndsParameterTypes :: Map String VType
+ndsParameterTypes =
+  Map.fromList
+  [(ndExclusiveStorage, VTypeBool),
+   (ndOobProgram, VTypeString),
+   (ndOvs, VTypeBool),
+   (ndOvsLink, VTypeMaybeString),
+   (ndOvsName, VTypeMaybeString),
+   (ndSpindleCount, VTypeInt)]
+
+ndsParameters :: FrozenSet String
+ndsParameters = ConstantUtils.mkSet (Map.keys ndsParameterTypes)
+
+ndsParameterTitles :: Map String String
+ndsParameterTitles =
+  Map.fromList
+  [(ndExclusiveStorage, "ExclusiveStorage"),
+   (ndOobProgram, "OutOfBandProgram"),
+   (ndOvs, "OpenvSwitch"),
+   (ndOvsLink, "OpenvSwitchLink"),
+   (ndOvsName, "OpenvSwitchName"),
+   (ndSpindleCount, "SpindleCount")]
+
+-- * Logical Disks parameters
+
+ldpAccess :: String
+ldpAccess = "access"
+
+ldpBarriers :: String
+ldpBarriers = "disabled-barriers"
+
+ldpDefaultMetavg :: String
+ldpDefaultMetavg = "default-metavg"
+
+ldpDelayTarget :: String
+ldpDelayTarget = "c-delay-target"
+
+ldpDiskCustom :: String
+ldpDiskCustom = "disk-custom"
+
+ldpDynamicResync :: String
+ldpDynamicResync = "dynamic-resync"
+
+ldpFillTarget :: String
+ldpFillTarget = "c-fill-target"
+
+ldpMaxRate :: String
+ldpMaxRate = "c-max-rate"
+
+ldpMinRate :: String
+ldpMinRate = "c-min-rate"
+
+ldpNetCustom :: String
+ldpNetCustom = "net-custom"
+
+ldpNoMetaFlush :: String
+ldpNoMetaFlush = "disable-meta-flush"
+
+ldpPlanAhead :: String
+ldpPlanAhead = "c-plan-ahead"
+
+ldpPool :: String
+ldpPool = "pool"
+
+ldpProtocol :: String
+ldpProtocol = "protocol"
+
+ldpResyncRate :: String
+ldpResyncRate = "resync-rate"
+
+ldpStripes :: String
+ldpStripes = "stripes"
+
+diskLdTypes :: Map String VType
+diskLdTypes =
+  Map.fromList
+  [(ldpAccess, VTypeString),
+   (ldpResyncRate, VTypeInt),
+   (ldpStripes, VTypeInt),
+   (ldpBarriers, VTypeString),
+   (ldpNoMetaFlush, VTypeBool),
+   (ldpDefaultMetavg, VTypeString),
+   (ldpDiskCustom, VTypeString),
+   (ldpNetCustom, VTypeString),
+   (ldpProtocol, VTypeString),
+   (ldpDynamicResync, VTypeBool),
+   (ldpPlanAhead, VTypeInt),
+   (ldpFillTarget, VTypeInt),
+   (ldpDelayTarget, VTypeInt),
+   (ldpMaxRate, VTypeInt),
+   (ldpMinRate, VTypeInt),
+   (ldpPool, VTypeString)]
+
+diskLdParameters :: FrozenSet String
+diskLdParameters = ConstantUtils.mkSet (Map.keys diskLdTypes)
+
+-- * Disk template parameters
+--
+-- Disk template parameters can be set/changed by the user via
+-- gnt-cluster and gnt-group)
+
+drbdResyncRate :: String
+drbdResyncRate = "resync-rate"
+
+drbdDataStripes :: String
+drbdDataStripes = "data-stripes"
+
+drbdMetaStripes :: String
+drbdMetaStripes = "meta-stripes"
+
+drbdDiskBarriers :: String
+drbdDiskBarriers = "disk-barriers"
+
+drbdMetaBarriers :: String
+drbdMetaBarriers = "meta-barriers"
+
+drbdDefaultMetavg :: String
+drbdDefaultMetavg = "metavg"
+
+drbdDiskCustom :: String
+drbdDiskCustom = "disk-custom"
+
+drbdNetCustom :: String
+drbdNetCustom = "net-custom"
+
+drbdProtocol :: String
+drbdProtocol = "protocol"
+
+drbdDynamicResync :: String
+drbdDynamicResync = "dynamic-resync"
+
+drbdPlanAhead :: String
+drbdPlanAhead = "c-plan-ahead"
+
+drbdFillTarget :: String
+drbdFillTarget = "c-fill-target"
+
+drbdDelayTarget :: String
+drbdDelayTarget = "c-delay-target"
+
+drbdMaxRate :: String
+drbdMaxRate = "c-max-rate"
+
+drbdMinRate :: String
+drbdMinRate = "c-min-rate"
+
+lvStripes :: String
+lvStripes = "stripes"
+
+rbdAccess :: String
+rbdAccess = "access"
+
+rbdPool :: String
+rbdPool = "pool"
+
+diskDtTypes :: Map String VType
+diskDtTypes =
+  Map.fromList [(drbdResyncRate, VTypeInt),
+                (drbdDataStripes, VTypeInt),
+                (drbdMetaStripes, VTypeInt),
+                (drbdDiskBarriers, VTypeString),
+                (drbdMetaBarriers, VTypeBool),
+                (drbdDefaultMetavg, VTypeString),
+                (drbdDiskCustom, VTypeString),
+                (drbdNetCustom, VTypeString),
+                (drbdProtocol, VTypeString),
+                (drbdDynamicResync, VTypeBool),
+                (drbdPlanAhead, VTypeInt),
+                (drbdFillTarget, VTypeInt),
+                (drbdDelayTarget, VTypeInt),
+                (drbdMaxRate, VTypeInt),
+                (drbdMinRate, VTypeInt),
+                (lvStripes, VTypeInt),
+                (rbdAccess, VTypeString),
+                (rbdPool, VTypeString)]
+
+diskDtParameters :: FrozenSet String
+diskDtParameters = ConstantUtils.mkSet (Map.keys diskDtTypes)
+
+-- * Dynamic disk parameters
+
+ddpLocalIp :: String
+ddpLocalIp = "local-ip"
+
+ddpRemoteIp :: String
+ddpRemoteIp = "remote-ip"
+
+ddpPort :: String
+ddpPort = "port"
+
+ddpLocalMinor :: String
+ddpLocalMinor = "local-minor"
+
+ddpRemoteMinor :: String
+ddpRemoteMinor = "remote-minor"
+
+-- * OOB supported commands
+
+oobPowerOn :: String
+oobPowerOn = Types.oobCommandToRaw OobPowerOn
+
+oobPowerOff :: String
+oobPowerOff = Types.oobCommandToRaw OobPowerOff
+
+oobPowerCycle :: String
+oobPowerCycle = Types.oobCommandToRaw OobPowerCycle
+
+oobPowerStatus :: String
+oobPowerStatus = Types.oobCommandToRaw OobPowerStatus
+
+oobHealth :: String
+oobHealth = Types.oobCommandToRaw OobHealth
+
+oobCommands :: FrozenSet String
+oobCommands = ConstantUtils.mkSet $ map Types.oobCommandToRaw [minBound..]
+
+oobPowerStatusPowered :: String
+oobPowerStatusPowered = "powered"
+
+-- | 60 seconds
+oobTimeout :: Int
+oobTimeout = 60
+
+-- | 2 seconds
+oobPowerDelay :: Double
+oobPowerDelay = 2.0
+
+oobStatusCritical :: String
+oobStatusCritical = Types.oobStatusToRaw OobStatusCritical
+
+oobStatusOk :: String
+oobStatusOk = Types.oobStatusToRaw OobStatusOk
+
+oobStatusUnknown :: String
+oobStatusUnknown = Types.oobStatusToRaw OobStatusUnknown
+
+oobStatusWarning :: String
+oobStatusWarning = Types.oobStatusToRaw OobStatusWarning
+
+oobStatuses :: FrozenSet String
+oobStatuses = ConstantUtils.mkSet $ map Types.oobStatusToRaw [minBound..]
+
+-- | Instance Parameters Profile
+ppDefault :: String
+ppDefault = "default"
+
+-- * nic* constants are used inside the ganeti config
+
+nicLink :: String
+nicLink = "link"
+
+nicMode :: String
+nicMode = "mode"
+
+nicVlan :: String
+nicVlan = "vlan"
+
+nicsParameterTypes :: Map String VType
+nicsParameterTypes =
+  Map.fromList [(nicMode, vtypeString),
+                (nicLink, vtypeString),
+                (nicVlan, vtypeMaybeString)]
+
+nicsParameters :: FrozenSet String
+nicsParameters = ConstantUtils.mkSet (Map.keys nicsParameterTypes)
+
+nicModeBridged :: String
+nicModeBridged = Types.nICModeToRaw NMBridged
+
+nicModeRouted :: String
+nicModeRouted = Types.nICModeToRaw NMRouted
+
+nicModeOvs :: String
+nicModeOvs = Types.nICModeToRaw NMOvs
+
+nicIpPool :: String
+nicIpPool = Types.nICModeToRaw NMPool
+
+nicValidModes :: FrozenSet String
+nicValidModes = ConstantUtils.mkSet $ map Types.nICModeToRaw [minBound..]
+
+releaseAction :: String
+releaseAction = "release"
+
+reserveAction :: String
+reserveAction = "reserve"
+
+-- * idisk* constants are used in opcodes, to create/change disks
+
+idiskAdopt :: String
+idiskAdopt = "adopt"
+
+idiskMetavg :: String
+idiskMetavg = "metavg"
+
+idiskMode :: String
+idiskMode = "mode"
+
+idiskName :: String
+idiskName = "name"
+
+idiskSize :: String
+idiskSize = "size"
+
+idiskSpindles :: String
+idiskSpindles = "spindles"
+
+idiskVg :: String
+idiskVg = "vg"
+
+idiskProvider :: String
+idiskProvider = "provider"
+
+idiskParamsTypes :: Map String VType
+idiskParamsTypes =
+  Map.fromList [(idiskSize, VTypeSize),
+                (idiskSpindles, VTypeInt),
+                (idiskMode, VTypeString),
+                (idiskAdopt, VTypeString),
+                (idiskVg, VTypeString),
+                (idiskMetavg, VTypeString),
+                (idiskProvider, VTypeString),
+                (idiskName, VTypeMaybeString)]
+
+idiskParams :: FrozenSet String
+idiskParams = ConstantUtils.mkSet (Map.keys idiskParamsTypes)
+
+-- * inic* constants are used in opcodes, to create/change nics
+
+inicBridge :: String
+inicBridge = "bridge"
+
+inicIp :: String
+inicIp = "ip"
+
+inicLink :: String
+inicLink = "link"
+
+inicMac :: String
+inicMac = "mac"
+
+inicMode :: String
+inicMode = "mode"
+
+inicName :: String
+inicName = "name"
+
+inicNetwork :: String
+inicNetwork = "network"
+
+inicVlan :: String
+inicVlan = "vlan"
+
+inicParamsTypes :: Map String VType
+inicParamsTypes =
+  Map.fromList [(inicBridge, VTypeMaybeString),
+                (inicIp, VTypeMaybeString),
+                (inicLink, VTypeString),
+                (inicMac, VTypeString),
+                (inicMode, VTypeString),
+                (inicName, VTypeMaybeString),
+                (inicNetwork, VTypeMaybeString),
+                (inicVlan, VTypeMaybeString)]
+
+inicParams :: FrozenSet String
+inicParams = ConstantUtils.mkSet (Map.keys inicParamsTypes)
+
+-- * Hypervisor constants
+
+htXenPvm :: String
+htXenPvm = Types.hypervisorToRaw XenPvm
+
+htFake :: String
+htFake = Types.hypervisorToRaw Fake
+
+htXenHvm :: String
+htXenHvm = Types.hypervisorToRaw XenHvm
+
+htKvm :: String
+htKvm = Types.hypervisorToRaw Kvm
+
+htChroot :: String
+htChroot = Types.hypervisorToRaw Chroot
+
+htLxc :: String
+htLxc = Types.hypervisorToRaw Lxc
+
+hyperTypes :: FrozenSet String
+hyperTypes = ConstantUtils.mkSet $ map Types.hypervisorToRaw [minBound..]
+
+htsReqPort :: FrozenSet String
+htsReqPort = ConstantUtils.mkSet [htXenHvm, htKvm]
+
+vncBasePort :: Int
+vncBasePort = 5900
+
+vncDefaultBindAddress :: String
+vncDefaultBindAddress = ip4AddressAny
+
+-- * NIC types
+
+htNicE1000 :: String
+htNicE1000 = "e1000"
+
+htNicI82551 :: String
+htNicI82551 = "i82551"
+
+htNicI8259er :: String
+htNicI8259er = "i82559er"
+
+htNicI85557b :: String
+htNicI85557b = "i82557b"
+
+htNicNe2kIsa :: String
+htNicNe2kIsa = "ne2k_isa"
+
+htNicNe2kPci :: String
+htNicNe2kPci = "ne2k_pci"
+
+htNicParavirtual :: String
+htNicParavirtual = "paravirtual"
+
+htNicPcnet :: String
+htNicPcnet = "pcnet"
+
+htNicRtl8139 :: String
+htNicRtl8139 = "rtl8139"
+
+htHvmValidNicTypes :: FrozenSet String
+htHvmValidNicTypes =
+  ConstantUtils.mkSet [htNicE1000,
+                       htNicNe2kIsa,
+                       htNicNe2kPci,
+                       htNicParavirtual,
+                       htNicRtl8139]
+
+htKvmValidNicTypes :: FrozenSet String
+htKvmValidNicTypes =
+  ConstantUtils.mkSet [htNicE1000,
+                       htNicI82551,
+                       htNicI8259er,
+                       htNicI85557b,
+                       htNicNe2kIsa,
+                       htNicNe2kPci,
+                       htNicParavirtual,
+                       htNicPcnet,
+                       htNicRtl8139]
+
+-- * Vif types
+
+-- | Default vif type in xen-hvm
+htHvmVifIoemu :: String
+htHvmVifIoemu = "ioemu"
+
+htHvmVifVif :: String
+htHvmVifVif = "vif"
+
+htHvmValidVifTypes :: FrozenSet String
+htHvmValidVifTypes = ConstantUtils.mkSet [htHvmVifIoemu, htHvmVifVif]
+
+-- * Disk types
+
+htDiskIde :: String
+htDiskIde = "ide"
+
+htDiskIoemu :: String
+htDiskIoemu = "ioemu"
+
+htDiskMtd :: String
+htDiskMtd = "mtd"
+
+htDiskParavirtual :: String
+htDiskParavirtual = "paravirtual"
+
+htDiskPflash :: String
+htDiskPflash = "pflash"
+
+htDiskScsi :: String
+htDiskScsi = "scsi"
+
+htDiskSd :: String
+htDiskSd = "sd"
+
+htHvmValidDiskTypes :: FrozenSet String
+htHvmValidDiskTypes = ConstantUtils.mkSet [htDiskIoemu, htDiskParavirtual]
+
+htKvmValidDiskTypes :: FrozenSet String
+htKvmValidDiskTypes =
+  ConstantUtils.mkSet [htDiskIde,
+                       htDiskMtd,
+                       htDiskParavirtual,
+                       htDiskPflash,
+                       htDiskScsi,
+                       htDiskSd]
+
+htCacheDefault :: String
+htCacheDefault = "default"
+
+htCacheNone :: String
+htCacheNone = "none"
+
+htCacheWback :: String
+htCacheWback = "writeback"
+
+htCacheWthrough :: String
+htCacheWthrough = "writethrough"
+
+htValidCacheTypes :: FrozenSet String
+htValidCacheTypes =
+  ConstantUtils.mkSet [htCacheDefault,
+                       htCacheNone,
+                       htCacheWback,
+                       htCacheWthrough]
+
+-- * Mouse types
+
+htMouseMouse :: String
+htMouseMouse = "mouse"
+
+htMouseTablet :: String
+htMouseTablet = "tablet"
+
+htKvmValidMouseTypes :: FrozenSet String
+htKvmValidMouseTypes = ConstantUtils.mkSet [htMouseMouse, htMouseTablet]
+
+-- * Boot order
+
+htBoCdrom :: String
+htBoCdrom = "cdrom"
+
+htBoDisk :: String
+htBoDisk = "disk"
+
+htBoFloppy :: String
+htBoFloppy = "floppy"
+
+htBoNetwork :: String
+htBoNetwork = "network"
+
+htKvmValidBoTypes :: FrozenSet String
+htKvmValidBoTypes =
+  ConstantUtils.mkSet [htBoCdrom, htBoDisk, htBoFloppy, htBoNetwork]
+
+-- * SPICE lossless image compression options
+
+htKvmSpiceLosslessImgComprAutoGlz :: String
+htKvmSpiceLosslessImgComprAutoGlz = "auto_glz"
+
+htKvmSpiceLosslessImgComprAutoLz :: String
+htKvmSpiceLosslessImgComprAutoLz = "auto_lz"
+
+htKvmSpiceLosslessImgComprGlz :: String
+htKvmSpiceLosslessImgComprGlz = "glz"
+
+htKvmSpiceLosslessImgComprLz :: String
+htKvmSpiceLosslessImgComprLz = "lz"
+
+htKvmSpiceLosslessImgComprOff :: String
+htKvmSpiceLosslessImgComprOff = "off"
+
+htKvmSpiceLosslessImgComprQuic :: String
+htKvmSpiceLosslessImgComprQuic = "quic"
+
+htKvmSpiceValidLosslessImgComprOptions :: FrozenSet String
+htKvmSpiceValidLosslessImgComprOptions =
+  ConstantUtils.mkSet [htKvmSpiceLosslessImgComprAutoGlz,
+                       htKvmSpiceLosslessImgComprAutoLz,
+                       htKvmSpiceLosslessImgComprGlz,
+                       htKvmSpiceLosslessImgComprLz,
+                       htKvmSpiceLosslessImgComprOff,
+                       htKvmSpiceLosslessImgComprQuic]
+
+htKvmSpiceLossyImgComprAlways :: String
+htKvmSpiceLossyImgComprAlways = "always"
+
+htKvmSpiceLossyImgComprAuto :: String
+htKvmSpiceLossyImgComprAuto = "auto"
+
+htKvmSpiceLossyImgComprNever :: String
+htKvmSpiceLossyImgComprNever = "never"
+
+htKvmSpiceValidLossyImgComprOptions :: FrozenSet String
+htKvmSpiceValidLossyImgComprOptions =
+  ConstantUtils.mkSet [htKvmSpiceLossyImgComprAlways,
+                       htKvmSpiceLossyImgComprAuto,
+                       htKvmSpiceLossyImgComprNever]
+
+-- * SPICE video stream detection
+
+htKvmSpiceVideoStreamDetectionAll :: String
+htKvmSpiceVideoStreamDetectionAll = "all"
+
+htKvmSpiceVideoStreamDetectionFilter :: String
+htKvmSpiceVideoStreamDetectionFilter = "filter"
+
+htKvmSpiceVideoStreamDetectionOff :: String
+htKvmSpiceVideoStreamDetectionOff = "off"
+
+htKvmSpiceValidVideoStreamDetectionOptions :: FrozenSet String
+htKvmSpiceValidVideoStreamDetectionOptions =
+  ConstantUtils.mkSet [htKvmSpiceVideoStreamDetectionAll,
+                       htKvmSpiceVideoStreamDetectionFilter,
+                       htKvmSpiceVideoStreamDetectionOff]
+
+-- * Security models
+
+htSmNone :: String
+htSmNone = "none"
+
+htSmPool :: String
+htSmPool = "pool"
+
+htSmUser :: String
+htSmUser = "user"
+
+htKvmValidSmTypes :: FrozenSet String
+htKvmValidSmTypes = ConstantUtils.mkSet [htSmNone, htSmPool, htSmUser]
+
+-- * Kvm flag values
+
+htKvmDisabled :: String
+htKvmDisabled = "disabled"
+
+htKvmEnabled :: String
+htKvmEnabled = "enabled"
+
+htKvmFlagValues :: FrozenSet String
+htKvmFlagValues = ConstantUtils.mkSet [htKvmDisabled, htKvmEnabled]
+
+-- * Migration type
+
+htMigrationLive :: String
+htMigrationLive = Types.migrationModeToRaw MigrationLive
+
+htMigrationNonlive :: String
+htMigrationNonlive = Types.migrationModeToRaw MigrationNonLive
+
+htMigrationModes :: FrozenSet String
+htMigrationModes =
+  ConstantUtils.mkSet $ map Types.migrationModeToRaw [minBound..]
+
+-- * Cluster verify steps
+
+verifyNplusoneMem :: String
+verifyNplusoneMem = Types.verifyOptionalChecksToRaw VerifyNPlusOneMem
+
+verifyOptionalChecks :: FrozenSet String
+verifyOptionalChecks =
+  ConstantUtils.mkSet $ map Types.verifyOptionalChecksToRaw [minBound..]
+
+-- * Cluster Verify error classes
+
+cvTcluster :: String
+cvTcluster = "cluster"
+
+cvTgroup :: String
+cvTgroup = "group"
+
+cvTnode :: String
+cvTnode = "node"
+
+cvTinstance :: String
+cvTinstance = "instance"
+
+-- * Cluster Verify error codes and documentation
+
+cvEclustercert :: (String, String, String)
+cvEclustercert =
+  ("cluster",
+   Types.cVErrorCodeToRaw CvECLUSTERCERT,
+   "Cluster certificate files verification failure")
+
+cvEclustercfg :: (String, String, String)
+cvEclustercfg =
+  ("cluster",
+   Types.cVErrorCodeToRaw CvECLUSTERCFG,
+   "Cluster configuration verification failure")
+
+cvEclusterdanglinginst :: (String, String, String)
+cvEclusterdanglinginst =
+  ("node",
+   Types.cVErrorCodeToRaw CvECLUSTERDANGLINGINST,
+   "Some instances have a non-existing primary node")
+
+cvEclusterdanglingnodes :: (String, String, String)
+cvEclusterdanglingnodes =
+  ("node",
+   Types.cVErrorCodeToRaw CvECLUSTERDANGLINGNODES,
+   "Some nodes belong to non-existing groups")
+
+cvEclusterfilecheck :: (String, String, String)
+cvEclusterfilecheck =
+  ("cluster",
+   Types.cVErrorCodeToRaw CvECLUSTERFILECHECK,
+   "Cluster configuration verification failure")
+
+cvEgroupdifferentpvsize :: (String, String, String)
+cvEgroupdifferentpvsize =
+  ("group",
+   Types.cVErrorCodeToRaw CvEGROUPDIFFERENTPVSIZE,
+   "PVs in the group have different sizes")
+
+cvEinstancebadnode :: (String, String, String)
+cvEinstancebadnode =
+  ("instance",
+   Types.cVErrorCodeToRaw CvEINSTANCEBADNODE,
+   "Instance marked as running lives on an offline node")
+
+cvEinstancedown :: (String, String, String)
+cvEinstancedown =
+  ("instance",
+   Types.cVErrorCodeToRaw CvEINSTANCEDOWN,
+   "Instance not running on its primary node")
+
+cvEinstancefaultydisk :: (String, String, String)
+cvEinstancefaultydisk =
+  ("instance",
+   Types.cVErrorCodeToRaw CvEINSTANCEFAULTYDISK,
+   "Impossible to retrieve status for a disk")
+
+cvEinstancelayout :: (String, String, String)
+cvEinstancelayout =
+  ("instance",
+   Types.cVErrorCodeToRaw CvEINSTANCELAYOUT,
+   "Instance has multiple secondary nodes")
+
+cvEinstancemissingcfgparameter :: (String, String, String)
+cvEinstancemissingcfgparameter =
+  ("instance",
+   Types.cVErrorCodeToRaw CvEINSTANCEMISSINGCFGPARAMETER,
+   "A configuration parameter for an instance is missing")
+
+cvEinstancemissingdisk :: (String, String, String)
+cvEinstancemissingdisk =
+  ("instance",
+   Types.cVErrorCodeToRaw CvEINSTANCEMISSINGDISK,
+   "Missing volume on an instance")
+
+cvEinstancepolicy :: (String, String, String)
+cvEinstancepolicy =
+  ("instance",
+   Types.cVErrorCodeToRaw CvEINSTANCEPOLICY,
+   "Instance does not meet policy")
+
+cvEinstancesplitgroups :: (String, String, String)
+cvEinstancesplitgroups =
+  ("instance",
+   Types.cVErrorCodeToRaw CvEINSTANCESPLITGROUPS,
+   "Instance with primary and secondary nodes in different groups")
+
+cvEinstanceunsuitablenode :: (String, String, String)
+cvEinstanceunsuitablenode =
+  ("instance",
+   Types.cVErrorCodeToRaw CvEINSTANCEUNSUITABLENODE,
+   "Instance running on nodes that are not suitable for it")
+
+cvEinstancewrongnode :: (String, String, String)
+cvEinstancewrongnode =
+  ("instance",
+   Types.cVErrorCodeToRaw CvEINSTANCEWRONGNODE,
+   "Instance running on the wrong node")
+
+cvEnodedrbd :: (String, String, String)
+cvEnodedrbd =
+  ("node",
+   Types.cVErrorCodeToRaw CvENODEDRBD,
+   "Error parsing the DRBD status file")
+
+cvEnodedrbdhelper :: (String, String, String)
+cvEnodedrbdhelper =
+  ("node",
+   Types.cVErrorCodeToRaw CvENODEDRBDHELPER,
+   "Error caused by the DRBD helper")
+
+cvEnodedrbdversion :: (String, String, String)
+cvEnodedrbdversion =
+  ("node",
+   Types.cVErrorCodeToRaw CvENODEDRBDVERSION,
+   "DRBD version mismatch within a node group")
+
+cvEnodefilecheck :: (String, String, String)
+cvEnodefilecheck =
+  ("node",
+   Types.cVErrorCodeToRaw CvENODEFILECHECK,
+   "Error retrieving the checksum of the node files")
+
+cvEnodefilestoragepaths :: (String, String, String)
+cvEnodefilestoragepaths =
+  ("node",
+   Types.cVErrorCodeToRaw CvENODEFILESTORAGEPATHS,
+   "Detected bad file storage paths")
+
+cvEnodefilestoragepathunusable :: (String, String, String)
+cvEnodefilestoragepathunusable =
+  ("node",
+   Types.cVErrorCodeToRaw CvENODEFILESTORAGEPATHUNUSABLE,
+   "File storage path unusable")
+
+cvEnodehooks :: (String, String, String)
+cvEnodehooks =
+  ("node",
+   Types.cVErrorCodeToRaw CvENODEHOOKS,
+   "Communication failure in hooks execution")
+
+cvEnodehv :: (String, String, String)
+cvEnodehv =
+  ("node",
+   Types.cVErrorCodeToRaw CvENODEHV,
+   "Hypervisor parameters verification failure")
+
+cvEnodelvm :: (String, String, String)
+cvEnodelvm =
+  ("node",
+   Types.cVErrorCodeToRaw CvENODELVM,
+   "LVM-related node error")
+
+cvEnoden1 :: (String, String, String)
+cvEnoden1 =
+  ("node",
+   Types.cVErrorCodeToRaw CvENODEN1,
+   "Not enough memory to accommodate instance failovers")
+
+cvEnodenet :: (String, String, String)
+cvEnodenet =
+  ("node",
+   Types.cVErrorCodeToRaw CvENODENET,
+   "Network-related node error")
+
+cvEnodeoobpath :: (String, String, String)
+cvEnodeoobpath =
+  ("node",
+   Types.cVErrorCodeToRaw CvENODEOOBPATH,
+   "Invalid Out Of Band path")
+
+cvEnodeorphaninstance :: (String, String, String)
+cvEnodeorphaninstance =
+  ("node",
+   Types.cVErrorCodeToRaw CvENODEORPHANINSTANCE,
+   "Unknown intance running on a node")
+
+cvEnodeorphanlv :: (String, String, String)
+cvEnodeorphanlv =
+  ("node",
+   Types.cVErrorCodeToRaw CvENODEORPHANLV,
+   "Unknown LVM logical volume")
+
+cvEnodeos :: (String, String, String)
+cvEnodeos =
+  ("node",
+   Types.cVErrorCodeToRaw CvENODEOS,
+   "OS-related node error")
+
+cvEnoderpc :: (String, String, String)
+cvEnoderpc =
+  ("node",
+   Types.cVErrorCodeToRaw CvENODERPC,
+   "Error during connection to the primary node of an instance")
+
+cvEnodesetup :: (String, String, String)
+cvEnodesetup =
+  ("node",
+   Types.cVErrorCodeToRaw CvENODESETUP,
+   "Node setup error")
+
+cvEnodesharedfilestoragepathunusable :: (String, String, String)
+cvEnodesharedfilestoragepathunusable =
+  ("node",
+   Types.cVErrorCodeToRaw CvENODESHAREDFILESTORAGEPATHUNUSABLE,
+   "Shared file storage path unusable")
+
+cvEnodessh :: (String, String, String)
+cvEnodessh =
+  ("node",
+   Types.cVErrorCodeToRaw CvENODESSH,
+   "SSH-related node error")
+
+cvEnodetime :: (String, String, String)
+cvEnodetime =
+  ("node",
+   Types.cVErrorCodeToRaw CvENODETIME,
+   "Node returned invalid time")
+
+cvEnodeuserscripts :: (String, String, String)
+cvEnodeuserscripts =
+  ("node",
+   Types.cVErrorCodeToRaw CvENODEUSERSCRIPTS,
+   "User scripts not present or not executable")
+
+cvEnodeversion :: (String, String, String)
+cvEnodeversion =
+  ("node",
+   Types.cVErrorCodeToRaw CvENODEVERSION,
+   "Protocol version mismatch or Ganeti version mismatch")
+
+cvAllEcodes :: FrozenSet (String, String, String)
+cvAllEcodes =
+  ConstantUtils.mkSet
+  [cvEclustercert,
+   cvEclustercfg,
+   cvEclusterdanglinginst,
+   cvEclusterdanglingnodes,
+   cvEclusterfilecheck,
+   cvEgroupdifferentpvsize,
+   cvEinstancebadnode,
+   cvEinstancedown,
+   cvEinstancefaultydisk,
+   cvEinstancelayout,
+   cvEinstancemissingcfgparameter,
+   cvEinstancemissingdisk,
+   cvEinstancepolicy,
+   cvEinstancesplitgroups,
+   cvEinstanceunsuitablenode,
+   cvEinstancewrongnode,
+   cvEnodedrbd,
+   cvEnodedrbdhelper,
+   cvEnodedrbdversion,
+   cvEnodefilecheck,
+   cvEnodefilestoragepaths,
+   cvEnodefilestoragepathunusable,
+   cvEnodehooks,
+   cvEnodehv,
+   cvEnodelvm,
+   cvEnoden1,
+   cvEnodenet,
+   cvEnodeoobpath,
+   cvEnodeorphaninstance,
+   cvEnodeorphanlv,
+   cvEnodeos,
+   cvEnoderpc,
+   cvEnodesetup,
+   cvEnodesharedfilestoragepathunusable,
+   cvEnodessh,
+   cvEnodetime,
+   cvEnodeuserscripts,
+   cvEnodeversion]
+
+cvAllEcodesStrings :: FrozenSet String
+cvAllEcodesStrings =
+  ConstantUtils.mkSet $ map Types.cVErrorCodeToRaw [minBound..]
+
+-- * Node verify constants
+
+nvBridges :: String
+nvBridges = "bridges"
+
+nvDrbdhelper :: String
+nvDrbdhelper = "drbd-helper"
+
+nvDrbdversion :: String
+nvDrbdversion = "drbd-version"
+
+nvDrbdlist :: String
+nvDrbdlist = "drbd-list"
+
+nvExclusivepvs :: String
+nvExclusivepvs = "exclusive-pvs"
+
+nvFilelist :: String
+nvFilelist = "filelist"
+
+nvAcceptedStoragePaths :: String
+nvAcceptedStoragePaths = "allowed-file-storage-paths"
+
+nvFileStoragePath :: String
+nvFileStoragePath = "file-storage-path"
+
+nvSharedFileStoragePath :: String
+nvSharedFileStoragePath = "shared-file-storage-path"
+
+nvHvinfo :: String
+nvHvinfo = "hvinfo"
+
+nvHvparams :: String
+nvHvparams = "hvparms"
+
+nvHypervisor :: String
+nvHypervisor = "hypervisor"
+
+nvInstancelist :: String
+nvInstancelist = "instancelist"
+
+nvLvlist :: String
+nvLvlist = "lvlist"
+
+nvMasterip :: String
+nvMasterip = "master-ip"
+
+nvNodelist :: String
+nvNodelist = "nodelist"
+
+nvNodenettest :: String
+nvNodenettest = "node-net-test"
+
+nvNodesetup :: String
+nvNodesetup = "nodesetup"
+
+nvOobPaths :: String
+nvOobPaths = "oob-paths"
+
+nvOslist :: String
+nvOslist = "oslist"
+
+nvPvlist :: String
+nvPvlist = "pvlist"
+
+nvTime :: String
+nvTime = "time"
+
+nvUserscripts :: String
+nvUserscripts = "user-scripts"
+
+nvVersion :: String
+nvVersion = "version"
+
+nvVglist :: String
+nvVglist = "vglist"
+
+nvVmnodes :: String
+nvVmnodes = "vmnodes"
+
+-- * Instance status
+
+inststAdmindown :: String
+inststAdmindown = Types.instanceStatusToRaw StatusDown
+
+inststAdminoffline :: String
+inststAdminoffline = Types.instanceStatusToRaw StatusOffline
+
+inststErrordown :: String
+inststErrordown = Types.instanceStatusToRaw ErrorDown
+
+inststErrorup :: String
+inststErrorup = Types.instanceStatusToRaw ErrorUp
+
+inststNodedown :: String
+inststNodedown = Types.instanceStatusToRaw NodeDown
+
+inststNodeoffline :: String
+inststNodeoffline = Types.instanceStatusToRaw NodeOffline
+
+inststRunning :: String
+inststRunning = Types.instanceStatusToRaw Running
+
+inststWrongnode :: String
+inststWrongnode = Types.instanceStatusToRaw WrongNode
+
+inststAll :: FrozenSet String
+inststAll = ConstantUtils.mkSet $ map Types.instanceStatusToRaw [minBound..]
+
+-- * Admin states
+
+adminstDown :: String
+adminstDown = Types.adminStateToRaw AdminDown
+
+adminstOffline :: String
+adminstOffline = Types.adminStateToRaw AdminOffline
+
+adminstUp :: String
+adminstUp = Types.adminStateToRaw AdminUp
+
+adminstAll :: FrozenSet String
+adminstAll = ConstantUtils.mkSet $ map Types.adminStateToRaw [minBound..]
+
+-- * Node roles
+
+nrDrained :: String
+nrDrained = Types.nodeRoleToRaw NRDrained
+
+nrMaster :: String
+nrMaster = Types.nodeRoleToRaw NRMaster
+
+nrMcandidate :: String
+nrMcandidate = Types.nodeRoleToRaw NRCandidate
+
+nrOffline :: String
+nrOffline = Types.nodeRoleToRaw NROffline
+
+nrRegular :: String
+nrRegular = Types.nodeRoleToRaw NRRegular
+
+nrAll :: FrozenSet String
+nrAll = ConstantUtils.mkSet $ map Types.nodeRoleToRaw [minBound..]
+
+-- * SSL certificate check constants (in days)
+
+sslCertExpirationError :: Int
+sslCertExpirationError = 7
+
+sslCertExpirationWarn :: Int
+sslCertExpirationWarn = 30
+
+-- * Allocator framework constants
+
+iallocatorVersion :: Int
+iallocatorVersion = 2
+
+iallocatorDirIn :: String
+iallocatorDirIn = Types.iAllocatorTestDirToRaw IAllocatorDirIn
+
+iallocatorDirOut :: String
+iallocatorDirOut = Types.iAllocatorTestDirToRaw IAllocatorDirOut
+
+validIallocatorDirections :: FrozenSet String
+validIallocatorDirections =
+  ConstantUtils.mkSet $ map Types.iAllocatorTestDirToRaw [minBound..]
+
+iallocatorModeAlloc :: String
+iallocatorModeAlloc = Types.iAllocatorModeToRaw IAllocatorAlloc
+
+iallocatorModeChgGroup :: String
+iallocatorModeChgGroup = Types.iAllocatorModeToRaw IAllocatorChangeGroup
+
+iallocatorModeMultiAlloc :: String
+iallocatorModeMultiAlloc = Types.iAllocatorModeToRaw IAllocatorMultiAlloc
+
+iallocatorModeNodeEvac :: String
+iallocatorModeNodeEvac = Types.iAllocatorModeToRaw IAllocatorNodeEvac
+
+iallocatorModeReloc :: String
+iallocatorModeReloc = Types.iAllocatorModeToRaw IAllocatorReloc
+
+validIallocatorModes :: FrozenSet String
+validIallocatorModes =
+  ConstantUtils.mkSet $ map Types.iAllocatorModeToRaw [minBound..]
+
+iallocatorSearchPath :: [String]
+iallocatorSearchPath = AutoConf.iallocatorSearchPath
+
+defaultIallocatorShortcut :: String
+defaultIallocatorShortcut = "."
+
+-- * Node evacuation
+
+nodeEvacPri :: String
+nodeEvacPri = Types.evacModeToRaw ChangePrimary
+
+nodeEvacSec :: String
+nodeEvacSec = Types.evacModeToRaw ChangeSecondary
+
+nodeEvacAll :: String
+nodeEvacAll = Types.evacModeToRaw ChangeAll
+
+nodeEvacModes :: FrozenSet String
+nodeEvacModes = ConstantUtils.mkSet $ map Types.evacModeToRaw [minBound..]
+
+-- * Job queue
+
+jobQueueVersion :: Int
+jobQueueVersion = 1
+
+jobQueueSizeHardLimit :: Int
+jobQueueSizeHardLimit = 5000
+
+jobQueueFilesPerms :: Int
+jobQueueFilesPerms = 0o640
+
+-- * Unchanged job return
+
+jobNotchanged :: String
+jobNotchanged = "nochange"
+
+-- * Job status
+
+jobStatusQueued :: String
+jobStatusQueued = Types.jobStatusToRaw JOB_STATUS_QUEUED
+
+jobStatusWaiting :: String
+jobStatusWaiting = Types.jobStatusToRaw JOB_STATUS_WAITING
+
+jobStatusCanceling :: String
+jobStatusCanceling = Types.jobStatusToRaw JOB_STATUS_CANCELING
+
+jobStatusRunning :: String
+jobStatusRunning = Types.jobStatusToRaw JOB_STATUS_RUNNING
+
+jobStatusCanceled :: String
+jobStatusCanceled = Types.jobStatusToRaw JOB_STATUS_CANCELED
+
+jobStatusSuccess :: String
+jobStatusSuccess = Types.jobStatusToRaw JOB_STATUS_SUCCESS
+
+jobStatusError :: String
+jobStatusError = Types.jobStatusToRaw JOB_STATUS_ERROR
+
+jobsPending :: FrozenSet String
+jobsPending =
+  ConstantUtils.mkSet [jobStatusQueued, jobStatusWaiting, jobStatusCanceling]
+
+jobsFinalized :: FrozenSet String
+jobsFinalized =
+  ConstantUtils.mkSet $ map Types.finalizedJobStatusToRaw [minBound..]
+
+jobStatusAll :: FrozenSet String
+jobStatusAll = ConstantUtils.mkSet $ map Types.jobStatusToRaw [minBound..]
+
+-- * OpCode status
+
+-- ** Not yet finalized opcodes
+
+opStatusCanceling :: String
+opStatusCanceling = "canceling"
+
+opStatusQueued :: String
+opStatusQueued = "queued"
+
+opStatusRunning :: String
+opStatusRunning = "running"
+
+opStatusWaiting :: String
+opStatusWaiting = "waiting"
+
+-- ** Finalized opcodes
+
+opStatusCanceled :: String
+opStatusCanceled = "canceled"
+
+opStatusError :: String
+opStatusError = "error"
+
+opStatusSuccess :: String
+opStatusSuccess = "success"
+
+opsFinalized :: FrozenSet String
+opsFinalized =
+  ConstantUtils.mkSet [opStatusCanceled, opStatusError, opStatusSuccess]
+
+-- * OpCode priority
+
+opPrioLowest :: Int
+opPrioLowest = 19
+
+opPrioHighest :: Int
+opPrioHighest = -20
+
+opPrioLow :: Int
+opPrioLow = Types.opSubmitPriorityToRaw OpPrioLow
+
+opPrioNormal :: Int
+opPrioNormal = Types.opSubmitPriorityToRaw OpPrioNormal
+
+opPrioHigh :: Int
+opPrioHigh = Types.opSubmitPriorityToRaw OpPrioHigh
+
+opPrioSubmitValid :: FrozenSet Int
+opPrioSubmitValid = ConstantUtils.mkSet [opPrioLow, opPrioNormal, opPrioHigh]
+
+opPrioDefault :: Int
+opPrioDefault = opPrioNormal
+
+-- * Lock recalculate mode
+
+locksAppend :: String
+locksAppend = "append"
+
+locksReplace :: String
+locksReplace = "replace"
+
+-- * Lock timeout
+--
+-- The lock timeout (sum) before we transition into blocking acquire
+-- (this can still be reset by priority change).  Computed as max time
+-- (10 hours) before we should actually go into blocking acquire,
+-- given that we start from the default priority level.
+
+lockAttemptsMaxwait :: Double
+lockAttemptsMaxwait = 15.0
+
+lockAttemptsMinwait :: Double
+lockAttemptsMinwait = 1.0
+
+lockAttemptsTimeout :: Int
+lockAttemptsTimeout = (10 * 3600) `div` (opPrioDefault - opPrioHighest)
+
+-- * Execution log types
+
+elogMessage :: String
+elogMessage = Types.eLogTypeToRaw ELogMessage
+
+elogRemoteImport :: String
+elogRemoteImport = Types.eLogTypeToRaw ELogRemoteImport
+
+elogJqueueTest :: String
+elogJqueueTest = Types.eLogTypeToRaw ELogJqueueTest
+
+-- * /etc/hosts modification
+
+etcHostsAdd :: String
+etcHostsAdd = "add"
+
+etcHostsRemove :: String
+etcHostsRemove = "remove"
+
+-- * Job queue test
+
+jqtMsgprefix :: String
+jqtMsgprefix = "TESTMSG="
+
+jqtExec :: String
+jqtExec = "exec"
+
+jqtExpandnames :: String
+jqtExpandnames = "expandnames"
+
+jqtLogmsg :: String
+jqtLogmsg = "logmsg"
+
+jqtStartmsg :: String
+jqtStartmsg = "startmsg"
+
+jqtAll :: FrozenSet String
+jqtAll = ConstantUtils.mkSet [jqtExec, jqtExpandnames, jqtLogmsg, jqtStartmsg]
+
+-- * Query resources
+
+qrCluster :: String
+qrCluster = "cluster"
+
+qrExport :: String
+qrExport = "export"
+
+qrExtstorage :: String
+qrExtstorage = "extstorage"
+
+qrGroup :: String
+qrGroup = "group"
+
+qrInstance :: String
+qrInstance = "instance"
+
+qrJob :: String
+qrJob = "job"
+
+qrLock :: String
+qrLock = "lock"
+
+qrNetwork :: String
+qrNetwork = "network"
+
+qrNode :: String
+qrNode = "node"
+
+qrOs :: String
+qrOs = "os"
+
+-- | List of resources which can be queried using 'Ganeti.OpCodes.OpQuery'
+qrViaOp :: FrozenSet String
+qrViaOp =
+  ConstantUtils.mkSet [qrCluster,
+                       qrInstance,
+                       qrNode,
+                       qrGroup,
+                       qrOs,
+                       qrExport,
+                       qrNetwork,
+                       qrExtstorage]
+
+-- | List of resources which can be queried using Local UniX Interface
+qrViaLuxi :: FrozenSet String
+qrViaLuxi = ConstantUtils.mkSet [qrLock, qrJob]
+
+-- | List of resources which can be queried using RAPI
+qrViaRapi :: FrozenSet String
+qrViaRapi = qrViaLuxi
+
+-- * Query field types
+
+qftBool :: String
+qftBool = "bool"
+
+qftNumber :: String
+qftNumber = "number"
+
+qftOther :: String
+qftOther = "other"
+
+qftText :: String
+qftText = "text"
+
+qftTimestamp :: String
+qftTimestamp = "timestamp"
+
+qftUnit :: String
+qftUnit = "unit"
+
+qftUnknown :: String
+qftUnknown = "unknown"
+
+qftAll :: FrozenSet String
+qftAll =
+  ConstantUtils.mkSet [qftBool,
+                       qftNumber,
+                       qftOther,
+                       qftText,
+                       qftTimestamp,
+                       qftUnit,
+                       qftUnknown]
+
+-- * Query result field status
+--
+-- Don't change or reuse values as they're used by clients.
+--
+-- FIXME: link with 'Ganeti.Query.Language.ResultStatus'
+
+-- | No data (e.g. RPC error), can be used instead of 'rsOffline'
+rsNodata :: Int
+rsNodata = 2
+
+rsNormal :: Int
+rsNormal = 0
+
+-- | Resource marked offline
+rsOffline :: Int
+rsOffline = 4
+
+-- | Value unavailable/unsupported for item; if this field is
+-- supported but we cannot get the data for the moment, 'rsNodata' or
+-- 'rsOffline' should be used
+rsUnavail :: Int
+rsUnavail = 3
+
+rsUnknown :: Int
+rsUnknown = 1
+
+rsAll :: FrozenSet Int
+rsAll =
+  ConstantUtils.mkSet [rsNodata,
+                       rsNormal,
+                       rsOffline,
+                       rsUnavail,
+                       rsUnknown]
+
+-- | Special field cases and their verbose/terse formatting
+rssDescription :: Map Int (String, String)
+rssDescription =
+  Map.fromList [(rsUnknown, ("(unknown)", "??")),
+                (rsNodata, ("(nodata)", "?")),
+                (rsOffline, ("(offline)", "*")),
+                (rsUnavail, ("(unavail)", "-"))]
+
+-- * Max dynamic devices
+
+maxDisks :: Int
+maxDisks = Types.maxDisks
+
+maxNics :: Int
+maxNics = Types.maxNics
+
+-- | SSCONF file prefix
+ssconfFileprefix :: String
+ssconfFileprefix = "ssconf_"
+
+-- * SSCONF keys
+
+ssClusterName :: String
+ssClusterName = "cluster_name"
+
+ssClusterTags :: String
+ssClusterTags = "cluster_tags"
+
+ssFileStorageDir :: String
+ssFileStorageDir = "file_storage_dir"
+
+ssSharedFileStorageDir :: String
+ssSharedFileStorageDir = "shared_file_storage_dir"
+
+ssMasterCandidates :: String
+ssMasterCandidates = "master_candidates"
+
+ssMasterCandidatesIps :: String
+ssMasterCandidatesIps = "master_candidates_ips"
+
+ssMasterIp :: String
+ssMasterIp = "master_ip"
+
+ssMasterNetdev :: String
+ssMasterNetdev = "master_netdev"
+
+ssMasterNetmask :: String
+ssMasterNetmask = "master_netmask"
+
+ssMasterNode :: String
+ssMasterNode = "master_node"
+
+ssNodeList :: String
+ssNodeList = "node_list"
+
+ssNodePrimaryIps :: String
+ssNodePrimaryIps = "node_primary_ips"
+
+ssNodeSecondaryIps :: String
+ssNodeSecondaryIps = "node_secondary_ips"
+
+ssOfflineNodes :: String
+ssOfflineNodes = "offline_nodes"
+
+ssOnlineNodes :: String
+ssOnlineNodes = "online_nodes"
+
+ssPrimaryIpFamily :: String
+ssPrimaryIpFamily = "primary_ip_family"
+
+ssInstanceList :: String
+ssInstanceList = "instance_list"
+
+ssReleaseVersion :: String
+ssReleaseVersion = "release_version"
+
+ssHypervisorList :: String
+ssHypervisorList = "hypervisor_list"
+
+ssMaintainNodeHealth :: String
+ssMaintainNodeHealth = "maintain_node_health"
+
+ssUidPool :: String
+ssUidPool = "uid_pool"
+
+ssNodegroups :: String
+ssNodegroups = "nodegroups"
+
+ssNetworks :: String
+ssNetworks = "networks"
+
+-- | This is not a complete SSCONF key, but the prefix for the
+-- hypervisor keys
+ssHvparamsPref :: String
+ssHvparamsPref = "hvparams_"
+
+-- * Hvparams keys
+
+ssHvparamsXenChroot :: String
+ssHvparamsXenChroot = ssHvparamsPref ++ htChroot
+
+ssHvparamsXenFake :: String
+ssHvparamsXenFake = ssHvparamsPref ++ htFake
+
+ssHvparamsXenHvm :: String
+ssHvparamsXenHvm = ssHvparamsPref ++ htXenHvm
+
+ssHvparamsXenKvm :: String
+ssHvparamsXenKvm = ssHvparamsPref ++ htKvm
+
+ssHvparamsXenLxc :: String
+ssHvparamsXenLxc = ssHvparamsPref ++ htLxc
+
+ssHvparamsXenPvm :: String
+ssHvparamsXenPvm = ssHvparamsPref ++ htXenPvm
+
+validSsHvparamsKeys :: FrozenSet String
+validSsHvparamsKeys =
+  ConstantUtils.mkSet [ssHvparamsXenChroot,
+                       ssHvparamsXenLxc,
+                       ssHvparamsXenFake,
+                       ssHvparamsXenHvm,
+                       ssHvparamsXenKvm,
+                       ssHvparamsXenPvm]
+
+ssFilePerms :: Int
+ssFilePerms = 0o444
+
+-- | Cluster wide default parameters
+defaultEnabledHypervisor :: String
+defaultEnabledHypervisor = htXenPvm
+
+hvcDefaults :: Map Hypervisor (Map String PyValueEx)
+hvcDefaults =
+  Map.fromList
+  [ (XenPvm, Map.fromList
+             [ (hvUseBootloader,  PyValueEx False)
+             , (hvBootloaderPath, PyValueEx xenBootloader)
+             , (hvBootloaderArgs, PyValueEx "")
+             , (hvKernelPath,     PyValueEx xenKernel)
+             , (hvInitrdPath,     PyValueEx "")
+             , (hvRootPath,       PyValueEx "/dev/xvda1")
+             , (hvKernelArgs,     PyValueEx "ro")
+             , (hvMigrationPort,  PyValueEx (8002 :: Int))
+             , (hvMigrationMode,  PyValueEx htMigrationLive)
+             , (hvBlockdevPrefix, PyValueEx "sd")
+             , (hvRebootBehavior, PyValueEx instanceRebootAllowed)
+             , (hvCpuMask,        PyValueEx cpuPinningAll)
+             , (hvCpuCap,         PyValueEx (0 :: Int))
+             , (hvCpuWeight,      PyValueEx (256 :: Int))
+             , (hvVifScript,      PyValueEx "")
+             , (hvXenCmd,         PyValueEx xenCmdXm)
+             , (hvXenCpuid,       PyValueEx "")
+             , (hvSoundhw,        PyValueEx "")
+             ])
+  , (XenHvm, Map.fromList
+             [ (hvBootOrder,      PyValueEx "cd")
+             , (hvCdromImagePath, PyValueEx "")
+             , (hvNicType,        PyValueEx htNicRtl8139)
+             , (hvDiskType,       PyValueEx htDiskParavirtual)
+             , (hvVncBindAddress, PyValueEx ip4AddressAny)
+             , (hvAcpi,           PyValueEx True)
+             , (hvPae,            PyValueEx True)
+             , (hvKernelPath,     PyValueEx "/usr/lib/xen/boot/hvmloader")
+             , (hvDeviceModel,    PyValueEx "/usr/lib/xen/bin/qemu-dm")
+             , (hvMigrationPort,  PyValueEx (8002 :: Int))
+             , (hvMigrationMode,  PyValueEx htMigrationNonlive)
+             , (hvUseLocaltime,   PyValueEx False)
+             , (hvBlockdevPrefix, PyValueEx "hd")
+             , (hvPassthrough,    PyValueEx "")
+             , (hvRebootBehavior, PyValueEx instanceRebootAllowed)
+             , (hvCpuMask,        PyValueEx cpuPinningAll)
+             , (hvCpuCap,         PyValueEx (0 :: Int))
+             , (hvCpuWeight,      PyValueEx (256 :: Int))
+             , (hvVifType,        PyValueEx htHvmVifIoemu)
+             , (hvVifScript,      PyValueEx "")
+             , (hvViridian,       PyValueEx False)
+             , (hvXenCmd,         PyValueEx xenCmdXm)
+             , (hvXenCpuid,       PyValueEx "")
+             , (hvSoundhw,        PyValueEx "")
+             ])
+  , (Kvm, Map.fromList
+          [ (hvKvmPath,                         PyValueEx kvmPath)
+          , (hvKernelPath,                      PyValueEx kvmKernel)
+          , (hvInitrdPath,                      PyValueEx "")
+          , (hvKernelArgs,                      PyValueEx "ro")
+          , (hvRootPath,                        PyValueEx "/dev/vda1")
+          , (hvAcpi,                            PyValueEx True)
+          , (hvSerialConsole,                   PyValueEx True)
+          , (hvSerialSpeed,                     PyValueEx (38400 :: Int))
+          , (hvVncBindAddress,                  PyValueEx "")
+          , (hvVncTls,                          PyValueEx False)
+          , (hvVncX509,                         PyValueEx "")
+          , (hvVncX509Verify,                   PyValueEx False)
+          , (hvVncPasswordFile,                 PyValueEx "")
+          , (hvKvmSpiceBind,                    PyValueEx "")
+          , (hvKvmSpiceIpVersion,           PyValueEx ifaceNoIpVersionSpecified)
+          , (hvKvmSpicePasswordFile,            PyValueEx "")
+          , (hvKvmSpiceLosslessImgCompr,        PyValueEx "")
+          , (hvKvmSpiceJpegImgCompr,            PyValueEx "")
+          , (hvKvmSpiceZlibGlzImgCompr,         PyValueEx "")
+          , (hvKvmSpiceStreamingVideoDetection, PyValueEx "")
+          , (hvKvmSpiceAudioCompr,              PyValueEx True)
+          , (hvKvmSpiceUseTls,                  PyValueEx False)
+          , (hvKvmSpiceTlsCiphers,              PyValueEx opensslCiphers)
+          , (hvKvmSpiceUseVdagent,              PyValueEx True)
+          , (hvKvmFloppyImagePath,              PyValueEx "")
+          , (hvCdromImagePath,                  PyValueEx "")
+          , (hvKvmCdrom2ImagePath,              PyValueEx "")
+          , (hvBootOrder,                       PyValueEx htBoDisk)
+          , (hvNicType,                         PyValueEx htNicParavirtual)
+          , (hvDiskType,                        PyValueEx htDiskParavirtual)
+          , (hvKvmCdromDiskType,                PyValueEx "")
+          , (hvUsbMouse,                        PyValueEx "")
+          , (hvKeymap,                          PyValueEx "")
+          , (hvMigrationPort,                   PyValueEx (8102 :: Int))
+          , (hvMigrationBandwidth,              PyValueEx (32 :: Int))
+          , (hvMigrationDowntime,               PyValueEx (30 :: Int))
+          , (hvMigrationMode,                   PyValueEx htMigrationLive)
+          , (hvUseLocaltime,                    PyValueEx False)
+          , (hvDiskCache,                       PyValueEx htCacheDefault)
+          , (hvSecurityModel,                   PyValueEx htSmNone)
+          , (hvSecurityDomain,                  PyValueEx "")
+          , (hvKvmFlag,                         PyValueEx "")
+          , (hvVhostNet,                        PyValueEx False)
+          , (hvKvmUseChroot,                    PyValueEx False)
+          , (hvMemPath,                         PyValueEx "")
+          , (hvRebootBehavior,                  PyValueEx instanceRebootAllowed)
+          , (hvCpuMask,                         PyValueEx cpuPinningAll)
+          , (hvCpuType,                         PyValueEx "")
+          , (hvCpuCores,                        PyValueEx (0 :: Int))
+          , (hvCpuThreads,                      PyValueEx (0 :: Int))
+          , (hvCpuSockets,                      PyValueEx (0 :: Int))
+          , (hvSoundhw,                         PyValueEx "")
+          , (hvUsbDevices,                      PyValueEx "")
+          , (hvVga,                             PyValueEx "")
+          , (hvKvmExtra,                        PyValueEx "")
+          , (hvKvmMachineVersion,               PyValueEx "")
+          , (hvVnetHdr,                         PyValueEx True)])
+  , (Fake, Map.fromList [(hvMigrationMode, PyValueEx htMigrationLive)])
+  , (Chroot, Map.fromList [(hvInitScript, PyValueEx "/ganeti-chroot")])
+  , (Lxc, Map.fromList [(hvCpuMask, PyValueEx "")])
+  ]
+
+hvcGlobals :: FrozenSet String
+hvcGlobals =
+  ConstantUtils.mkSet [hvMigrationBandwidth,
+                       hvMigrationMode,
+                       hvMigrationPort,
+                       hvXenCmd]
+
+becDefaults :: Map String PyValueEx
+becDefaults =
+  Map.fromList
+  [ (beMinmem, PyValueEx (128 :: Int))
+  , (beMaxmem, PyValueEx (128 :: Int))
+  , (beVcpus, PyValueEx (1 :: Int))
+  , (beAutoBalance, PyValueEx True)
+  , (beAlwaysFailover, PyValueEx False)
+  , (beSpindleUse, PyValueEx (1 :: Int))
+  ]
+
+ndcDefaults :: Map String PyValueEx
+ndcDefaults =
+  Map.fromList
+  [ (ndOobProgram,       PyValueEx "")
+  , (ndSpindleCount,     PyValueEx (1 :: Int))
+  , (ndExclusiveStorage, PyValueEx False)
+  , (ndOvs,              PyValueEx False)
+  , (ndOvsName,          PyValueEx defaultOvs)
+  , (ndOvsLink,          PyValueEx "")
+  ]
+
+ndcGlobals :: FrozenSet String
+ndcGlobals = ConstantUtils.mkSet [ndExclusiveStorage]
+
+-- | Default delay target measured in sectors
+defaultDelayTarget :: Int
+defaultDelayTarget = 1
+
+defaultDiskCustom :: String
+defaultDiskCustom = ""
+
+defaultDiskResync :: Bool
+defaultDiskResync = False
+
+-- | Default fill target measured in sectors
+defaultFillTarget :: Int
+defaultFillTarget = 0
+
+-- | Default mininum rate measured in KiB/s
+defaultMinRate :: Int
+defaultMinRate = 4 * 1024
+
+defaultNetCustom :: String
+defaultNetCustom = ""
+
+-- | Default plan ahead measured in sectors
+--
+-- 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.
+defaultPlanAhead :: Int
+defaultPlanAhead = 20
+
+defaultRbdPool :: String
+defaultRbdPool = "rbd"
+
+diskLdDefaults :: Map DiskTemplate (Map String PyValueEx)
+diskLdDefaults =
+  Map.fromList
+  [ (DTBlock, Map.empty)
+  , (DTDrbd8, Map.fromList
+              [ (ldpBarriers,      PyValueEx drbdBarriers)
+              , (ldpDefaultMetavg, PyValueEx defaultVg)
+              , (ldpDelayTarget,   PyValueEx defaultDelayTarget)
+              , (ldpDiskCustom,    PyValueEx defaultDiskCustom)
+              , (ldpDynamicResync, PyValueEx defaultDiskResync)
+              , (ldpFillTarget,    PyValueEx defaultFillTarget)
+              , (ldpMaxRate,       PyValueEx classicDrbdSyncSpeed)
+              , (ldpMinRate,       PyValueEx defaultMinRate)
+              , (ldpNetCustom,     PyValueEx defaultNetCustom)
+              , (ldpNoMetaFlush,   PyValueEx drbdNoMetaFlush)
+              , (ldpPlanAhead,     PyValueEx defaultPlanAhead)
+              , (ldpProtocol,      PyValueEx drbdDefaultNetProtocol)
+              , (ldpResyncRate,    PyValueEx classicDrbdSyncSpeed)
+              ])
+  , (DTExt, Map.empty)
+  , (DTFile, Map.empty)
+  , (DTPlain, Map.fromList [(ldpStripes, PyValueEx lvmStripecount)])
+  , (DTRbd, Map.fromList
+            [ (ldpPool, PyValueEx defaultRbdPool)
+            , (ldpAccess, PyValueEx diskKernelspace)
+            ])
+  , (DTSharedFile, Map.empty)
+  ]
+
+diskDtDefaults :: Map DiskTemplate (Map String PyValueEx)
+diskDtDefaults =
+  Map.fromList
+  [ (DTBlock,      Map.empty)
+  , (DTDiskless,   Map.empty)
+  , (DTDrbd8,      Map.fromList
+                   [ (drbdDataStripes,   PyValueEx lvmStripecount)
+                   , (drbdDefaultMetavg, PyValueEx defaultVg)
+                   , (drbdDelayTarget,   PyValueEx defaultDelayTarget)
+                   , (drbdDiskBarriers,  PyValueEx drbdBarriers)
+                   , (drbdDiskCustom,    PyValueEx defaultDiskCustom)
+                   , (drbdDynamicResync, PyValueEx defaultDiskResync)
+                   , (drbdFillTarget,    PyValueEx defaultFillTarget)
+                   , (drbdMaxRate,       PyValueEx classicDrbdSyncSpeed)
+                   , (drbdMetaBarriers,  PyValueEx drbdNoMetaFlush)
+                   , (drbdMetaStripes,   PyValueEx lvmStripecount)
+                   , (drbdMinRate,       PyValueEx defaultMinRate)
+                   , (drbdNetCustom,     PyValueEx defaultNetCustom)
+                   , (drbdPlanAhead,     PyValueEx defaultPlanAhead)
+                   , (drbdProtocol,      PyValueEx drbdDefaultNetProtocol)
+                   , (drbdResyncRate,    PyValueEx classicDrbdSyncSpeed)
+                   ])
+  , (DTExt,        Map.empty)
+  , (DTFile,       Map.empty)
+  , (DTPlain,      Map.fromList [(lvStripes, PyValueEx lvmStripecount)])
+  , (DTRbd,        Map.fromList
+                   [ (rbdPool, PyValueEx defaultRbdPool)
+                   , (rbdAccess, PyValueEx diskKernelspace)
+                   ])
+  , (DTSharedFile, Map.empty)
+  ]
+
+niccDefaults :: Map String PyValueEx
+niccDefaults =
+  Map.fromList
+  [ (nicMode, PyValueEx nicModeBridged)
+  , (nicLink, PyValueEx defaultBridge)
+  , (nicVlan, PyValueEx valueHsNothing)
+  ]
+
+-- | All of the following values are quite arbitrary - there are no
+-- "good" defaults, these must be customised per-site
+ispecsMinmaxDefaults :: Map String (Map String Int)
+ispecsMinmaxDefaults =
+  Map.fromList
+  [(ispecsMin,
+    Map.fromList
+    [(ConstantUtils.ispecMemSize, Types.iSpecMemorySize Types.defMinISpec),
+     (ConstantUtils.ispecCpuCount, Types.iSpecCpuCount Types.defMinISpec),
+     (ConstantUtils.ispecDiskCount, Types.iSpecDiskCount Types.defMinISpec),
+     (ConstantUtils.ispecDiskSize, Types.iSpecDiskSize Types.defMinISpec),
+     (ConstantUtils.ispecNicCount, Types.iSpecNicCount Types.defMinISpec),
+     (ConstantUtils.ispecSpindleUse, Types.iSpecSpindleUse Types.defMinISpec)]),
+   (ispecsMax,
+    Map.fromList
+    [(ConstantUtils.ispecMemSize, Types.iSpecMemorySize Types.defMaxISpec),
+     (ConstantUtils.ispecCpuCount, Types.iSpecCpuCount Types.defMaxISpec),
+     (ConstantUtils.ispecDiskCount, Types.iSpecDiskCount Types.defMaxISpec),
+     (ConstantUtils.ispecDiskSize, Types.iSpecDiskSize Types.defMaxISpec),
+     (ConstantUtils.ispecNicCount, Types.iSpecNicCount Types.defMaxISpec),
+     (ConstantUtils.ispecSpindleUse, Types.iSpecSpindleUse Types.defMaxISpec)])]
+
+ipolicyDefaults :: Map String PyValueEx
+ipolicyDefaults =
+  Map.fromList
+  [ (ispecsMinmax,        PyValueEx [ispecsMinmaxDefaults])
+  , (ispecsStd,           PyValueEx (Map.fromList
+                                     [ (ispecMemSize,    128)
+                                     , (ispecCpuCount,   1)
+                                     , (ispecDiskCount,  1)
+                                     , (ispecDiskSize,   1024)
+                                     , (ispecNicCount,   1)
+                                     , (ispecSpindleUse, 1)
+                                     ] :: Map String Int))
+  , (ipolicyDts,          PyValueEx (ConstantUtils.toList diskTemplates))
+  , (ipolicyVcpuRatio,    PyValueEx (4.0 :: Double))
+  , (ipolicySpindleRatio, PyValueEx (32.0 :: Double))
+  ]
+
+masterPoolSizeDefault :: Int
+masterPoolSizeDefault = 10
+
+-- * Exclusive storage
+
+-- | Error margin used to compare physical disks
+partMargin :: Double
+partMargin = 0.01
+
+-- | Space reserved when creating instance disks
+partReserved :: Double
+partReserved = 0.02
+
+-- * Confd
+
+confdProtocolVersion :: Int
+confdProtocolVersion = ConstantUtils.confdProtocolVersion
+
+-- Confd request type
+
+confdReqPing :: Int
+confdReqPing = Types.confdRequestTypeToRaw ReqPing
+
+confdReqNodeRoleByname :: Int
+confdReqNodeRoleByname = Types.confdRequestTypeToRaw ReqNodeRoleByName
+
+confdReqNodePipByInstanceIp :: Int
+confdReqNodePipByInstanceIp = Types.confdRequestTypeToRaw ReqNodePipByInstPip
+
+confdReqClusterMaster :: Int
+confdReqClusterMaster = Types.confdRequestTypeToRaw ReqClusterMaster
+
+confdReqNodePipList :: Int
+confdReqNodePipList = Types.confdRequestTypeToRaw ReqNodePipList
+
+confdReqMcPipList :: Int
+confdReqMcPipList = Types.confdRequestTypeToRaw ReqMcPipList
+
+confdReqInstancesIpsList :: Int
+confdReqInstancesIpsList = Types.confdRequestTypeToRaw ReqInstIpsList
+
+confdReqNodeDrbd :: Int
+confdReqNodeDrbd = Types.confdRequestTypeToRaw ReqNodeDrbd
+
+confdReqNodeInstances :: Int
+confdReqNodeInstances = Types.confdRequestTypeToRaw ReqNodeInstances
+
+confdReqs :: FrozenSet Int
+confdReqs =
+  ConstantUtils.mkSet .
+  map Types.confdRequestTypeToRaw $
+  [minBound..] \\ [ReqNodeInstances]
+
+-- * Confd request type
+
+confdReqfieldName :: Int
+confdReqfieldName = Types.confdReqFieldToRaw ReqFieldName
+
+confdReqfieldIp :: Int
+confdReqfieldIp = Types.confdReqFieldToRaw ReqFieldIp
+
+confdReqfieldMnodePip :: Int
+confdReqfieldMnodePip = Types.confdReqFieldToRaw ReqFieldMNodePip
+
+-- * Confd repl status
+
+confdReplStatusOk :: Int
+confdReplStatusOk = Types.confdReplyStatusToRaw ReplyStatusOk
+
+confdReplStatusError :: Int
+confdReplStatusError = Types.confdReplyStatusToRaw ReplyStatusError
+
+confdReplStatusNotimplemented :: Int
+confdReplStatusNotimplemented = Types.confdReplyStatusToRaw ReplyStatusNotImpl
+
+confdReplStatuses :: FrozenSet Int
+confdReplStatuses =
+  ConstantUtils.mkSet $ map Types.confdReplyStatusToRaw [minBound..]
+
+-- * Confd node role
+
+confdNodeRoleMaster :: Int
+confdNodeRoleMaster = Types.confdNodeRoleToRaw NodeRoleMaster
+
+confdNodeRoleCandidate :: Int
+confdNodeRoleCandidate = Types.confdNodeRoleToRaw NodeRoleCandidate
+
+confdNodeRoleOffline :: Int
+confdNodeRoleOffline = Types.confdNodeRoleToRaw NodeRoleOffline
+
+confdNodeRoleDrained :: Int
+confdNodeRoleDrained = Types.confdNodeRoleToRaw NodeRoleDrained
+
+confdNodeRoleRegular :: Int
+confdNodeRoleRegular = Types.confdNodeRoleToRaw NodeRoleRegular
+
+-- * A few common errors for confd
+
+confdErrorUnknownEntry :: Int
+confdErrorUnknownEntry = Types.confdErrorTypeToRaw ConfdErrorUnknownEntry
+
+confdErrorInternal :: Int
+confdErrorInternal = Types.confdErrorTypeToRaw ConfdErrorInternal
+
+confdErrorArgument :: Int
+confdErrorArgument = Types.confdErrorTypeToRaw ConfdErrorArgument
+
+-- * Confd request query fields
+
+confdReqqLink :: String
+confdReqqLink = ConstantUtils.confdReqqLink
+
+confdReqqIp :: String
+confdReqqIp = ConstantUtils.confdReqqIp
+
+confdReqqIplist :: String
+confdReqqIplist = ConstantUtils.confdReqqIplist
+
+confdReqqFields :: String
+confdReqqFields = ConstantUtils.confdReqqFields
+
+-- | Each request is "salted" by the current timestamp.
+--
+-- This constant decides how many seconds of skew to accept.
+--
+-- TODO: make this a default and allow the value to be more
+-- configurable
+confdMaxClockSkew :: Int
+confdMaxClockSkew = 2 * nodeMaxClockSkew
+
+-- | When we haven't reloaded the config for more than this amount of
+-- seconds, we force a test to see if inotify is betraying us. Using a
+-- prime number to ensure we get less chance of 'same wakeup' with
+-- other processes.
+confdConfigReloadTimeout :: Int
+confdConfigReloadTimeout = 17
+
+-- | If we receive more than one update in this amount of
+-- microseconds, we move to polling every RATELIMIT seconds, rather
+-- than relying on inotify, to be able to serve more requests.
+confdConfigReloadRatelimit :: Int
+confdConfigReloadRatelimit = 250000
+
+-- | Magic number prepended to all confd queries.
+--
+-- This allows us to distinguish different types of confd protocols
+-- and handle them. For example by changing this we can move the whole
+-- payload to be compressed, or move away from json.
+confdMagicFourcc :: String
+confdMagicFourcc = "plj0"
+
+-- | By default a confd request is sent to the minimum between this
+-- number and all MCs. 6 was chosen because even in the case of a
+-- disastrous 50% response rate, we should have enough answers to be
+-- able to compare more than one.
+confdDefaultReqCoverage :: Int
+confdDefaultReqCoverage = 6
+
+-- | Timeout in seconds to expire pending query request in the confd
+-- client library. We don't actually expect any answer more than 10
+-- seconds after we sent a request.
+confdClientExpireTimeout :: Int
+confdClientExpireTimeout = 10
+
+-- | Maximum UDP datagram size.
+--
+-- On IPv4: 64K - 20 (ip header size) - 8 (udp header size) = 65507
+-- On IPv6: 64K - 40 (ip6 header size) - 8 (udp header size) = 65487
+--   (assuming we can't use jumbo frames)
+-- We just set this to 60K, which should be enough
+maxUdpDataSize :: Int
+maxUdpDataSize = 61440
+
+-- * User-id pool minimum/maximum acceptable user-ids
+
+uidpoolUidMin :: Int
+uidpoolUidMin = 0
+
+-- | Assuming 32 bit user-ids
+uidpoolUidMax :: Integer
+uidpoolUidMax = 2 ^ 32 - 1
+
+-- | Name or path of the pgrep command
+pgrep :: String
+pgrep = "pgrep"
+
+-- | Name of the node group that gets created at cluster init or
+-- upgrade
+initialNodeGroupName :: String
+initialNodeGroupName = "default"
+
+-- * Possible values for NodeGroup.alloc_policy
+
+allocPolicyLastResort :: String
+allocPolicyLastResort = Types.allocPolicyToRaw AllocLastResort
+
+allocPolicyPreferred :: String
+allocPolicyPreferred = Types.allocPolicyToRaw AllocPreferred
+
+allocPolicyUnallocable :: String
+allocPolicyUnallocable = Types.allocPolicyToRaw AllocUnallocable
+
+validAllocPolicies :: [String]
+validAllocPolicies = map Types.allocPolicyToRaw [minBound..]
+
+-- | Temporary external/shared storage parameters
+blockdevDriverManual :: String
+blockdevDriverManual = Types.blockDriverToRaw BlockDrvManual
+
+-- | 'qemu-img' path, required for 'ovfconverter'
+qemuimgPath :: String
+qemuimgPath = AutoConf.qemuimgPath
+
+-- | Whether htools was enabled at compilation time
+--
+-- FIXME: this should be moved next to the other enable constants,
+-- such as, 'enableConfd', and renamed to 'enableHtools'.
+htools :: Bool
+htools = AutoConf.htools
+
+-- | The hail iallocator
+iallocHail :: String
+iallocHail = "hail"
+
+-- * Fake opcodes for functions that have hooks attached to them via
+-- backend.RunLocalHooks
+
+fakeOpMasterTurndown :: String
+fakeOpMasterTurndown = "OP_CLUSTER_IP_TURNDOWN"
+
+fakeOpMasterTurnup :: String
+fakeOpMasterTurnup = "OP_CLUSTER_IP_TURNUP"
+
+-- * SSH key types
+
+sshkDsa :: String
+sshkDsa = "dsa"
+
+sshkRsa :: String
+sshkRsa = "rsa"
+
+sshkAll :: FrozenSet String
+sshkAll = ConstantUtils.mkSet [sshkRsa, sshkDsa]
+
+-- * SSH authorized key types
+
+sshakDss :: String
+sshakDss = "ssh-dss"
+
+sshakRsa :: String
+sshakRsa = "ssh-rsa"
+
+sshakAll :: FrozenSet String
+sshakAll = ConstantUtils.mkSet [sshakDss, sshakRsa]
+
+-- * SSH setup
+
+sshsClusterName :: String
+sshsClusterName = "cluster_name"
+
+sshsSshHostKey :: String
+sshsSshHostKey = "ssh_host_key"
+
+sshsSshRootKey :: String
+sshsSshRootKey = "ssh_root_key"
+
+sshsNodeDaemonCertificate :: String
+sshsNodeDaemonCertificate = "node_daemon_certificate"
+
+-- * Key files for SSH daemon
+
+sshHostDsaPriv :: String
+sshHostDsaPriv = sshConfigDir ++ "/ssh_host_dsa_key"
+
+sshHostDsaPub :: String
+sshHostDsaPub = sshHostDsaPriv ++ ".pub"
+
+sshHostRsaPriv :: String
+sshHostRsaPriv = sshConfigDir ++ "/ssh_host_rsa_key"
+
+sshHostRsaPub :: String
+sshHostRsaPub = sshHostRsaPriv ++ ".pub"
+
+sshDaemonKeyfiles :: Map String (String, String)
+sshDaemonKeyfiles =
+  Map.fromList [ (sshkRsa, (sshHostRsaPriv, sshHostRsaPub))
+               , (sshkDsa, (sshHostDsaPriv, sshHostDsaPub))
+               ]
+
+-- * Node daemon setup
+
+ndsClusterName :: String
+ndsClusterName = "cluster_name"
+
+ndsNodeDaemonCertificate :: String
+ndsNodeDaemonCertificate = "node_daemon_certificate"
+
+ndsSsconf :: String
+ndsSsconf = "ssconf"
+
+ndsStartNodeDaemon :: String
+ndsStartNodeDaemon = "start_node_daemon"
+
+-- * The source reasons for the execution of an OpCode
+
+opcodeReasonSrcClient :: String
+opcodeReasonSrcClient = "gnt:client"
+
+opcodeReasonSrcNoded :: String
+opcodeReasonSrcNoded = "gnt:daemon:noded"
+
+opcodeReasonSrcOpcode :: String
+opcodeReasonSrcOpcode = "gnt:opcode"
+
+opcodeReasonSrcRlib2 :: String
+opcodeReasonSrcRlib2 = "gnt:library:rlib2"
+
+opcodeReasonSrcUser :: String
+opcodeReasonSrcUser = "gnt:user"
+
+opcodeReasonSources :: FrozenSet String
+opcodeReasonSources =
+  ConstantUtils.mkSet [opcodeReasonSrcClient,
+                       opcodeReasonSrcNoded,
+                       opcodeReasonSrcOpcode,
+                       opcodeReasonSrcRlib2,
+                       opcodeReasonSrcUser]
+
+-- | Path generating random UUID
+randomUuidFile :: String
+randomUuidFile = ConstantUtils.randomUuidFile
+
+-- * Auto-repair tag prefixes
+
+autoRepairTagPrefix :: String
+autoRepairTagPrefix = "ganeti:watcher:autorepair:"
+
+autoRepairTagEnabled :: String
+autoRepairTagEnabled = autoRepairTagPrefix
+
+autoRepairTagPending :: String
+autoRepairTagPending = autoRepairTagPrefix ++ "pending:"
+
+autoRepairTagResult :: String
+autoRepairTagResult = autoRepairTagPrefix ++ "result:"
+
+autoRepairTagSuspended :: String
+autoRepairTagSuspended = autoRepairTagPrefix ++ "suspend:"
+
+-- * Auto-repair levels
+
+autoRepairFailover :: String
+autoRepairFailover = Types.autoRepairTypeToRaw ArFailover
+
+autoRepairFixStorage :: String
+autoRepairFixStorage = Types.autoRepairTypeToRaw ArFixStorage
+
+autoRepairMigrate :: String
+autoRepairMigrate = Types.autoRepairTypeToRaw ArMigrate
+
+autoRepairReinstall :: String
+autoRepairReinstall = Types.autoRepairTypeToRaw ArReinstall
+
+autoRepairAllTypes :: FrozenSet String
+autoRepairAllTypes =
+  ConstantUtils.mkSet [autoRepairFailover,
+                       autoRepairFixStorage,
+                       autoRepairMigrate,
+                       autoRepairReinstall]
+
+-- * Auto-repair results
+
+autoRepairEnoperm :: String
+autoRepairEnoperm = Types.autoRepairResultToRaw ArEnoperm
+
+autoRepairFailure :: String
+autoRepairFailure = Types.autoRepairResultToRaw ArFailure
+
+autoRepairSuccess :: String
+autoRepairSuccess = Types.autoRepairResultToRaw ArSuccess
+
+autoRepairAllResults :: FrozenSet String
+autoRepairAllResults =
+  ConstantUtils.mkSet [autoRepairEnoperm, autoRepairFailure, autoRepairSuccess]
+
+-- | The version identifier for builtin data collectors
+builtinDataCollectorVersion :: String
+builtinDataCollectorVersion = "B"
+
+-- | The reason trail opcode parameter name
+opcodeReason :: String
+opcodeReason = "reason"
+
+diskstatsFile :: String
+diskstatsFile = "/proc/diskstats"
+
+-- *  CPU load collector
+
+statFile :: String
+statFile = "/proc/stat"
+
+cpuavgloadBufferSize :: Int
+cpuavgloadBufferSize = 150
+
+cpuavgloadWindowSize :: Int
+cpuavgloadWindowSize = 600
+
+-- * Monitoring daemon
+
+-- | Mond's variable for periodical data collection
+mondTimeInterval :: Int
+mondTimeInterval = 5
+
+-- | Mond's latest API version
+mondLatestApiVersion :: Int
+mondLatestApiVersion = 1
+
+-- * Disk access modes
+
+diskUserspace :: String
+diskUserspace = Types.diskAccessModeToRaw DiskUserspace
+
+diskKernelspace :: String
+diskKernelspace = Types.diskAccessModeToRaw DiskKernelspace
+
+diskValidAccessModes :: FrozenSet String
+diskValidAccessModes =
+  ConstantUtils.mkSet $ map Types.diskAccessModeToRaw [minBound..]
+
+-- | Timeout for queue draining in upgrades
+upgradeQueueDrainTimeout :: Int
+upgradeQueueDrainTimeout = 36 * 60 * 60 -- 1.5 days
+
+-- | Intervall at which the queue is polled during upgrades
+upgradeQueuePollInterval :: Int
+upgradeQueuePollInterval  = 10
+
+-- * Hotplug Actions
+
+hotplugActionAdd :: String
+hotplugActionAdd = Types.hotplugActionToRaw HAAdd
+
+hotplugActionRemove :: String
+hotplugActionRemove = Types.hotplugActionToRaw HARemove
+
+hotplugActionModify :: String
+hotplugActionModify = Types.hotplugActionToRaw HAMod
+
+hotplugAllActions :: FrozenSet String
+hotplugAllActions =
+  ConstantUtils.mkSet $ map Types.hotplugActionToRaw [minBound..]
+
+-- * Hotplug Device Targets
+
+hotplugTargetNic :: String
+hotplugTargetNic = Types.hotplugTargetToRaw HTNic
+
+hotplugTargetDisk :: String
+hotplugTargetDisk = Types.hotplugTargetToRaw HTDisk
+
+hotplugAllTargets :: FrozenSet String
+hotplugAllTargets =
+  ConstantUtils.mkSet $ map Types.hotplugTargetToRaw [minBound..]
+
+-- | Timeout for disk removal
+diskRemoveRetryTimeout :: Int
+diskRemoveRetryTimeout = 30
+
+-- | Intervall between disk removal retries
+diskRemoveRetryInterval :: Int
+diskRemoveRetryInterval  = 3
index 9437c75..53c45be 100644 (file)
@@ -138,7 +138,7 @@ maybeFromObj o k =
 
 -- | Reads the value of a key in a JSON object with a default if
 -- missing. Note that both missing keys and keys with value \'null\'
--- will case the default value to be returned.
+-- will cause the default value to be returned.
 fromObjWithDefault :: (J.JSON a, Monad m) =>
                       JSRecord -> String -> a -> m a
 fromObjWithDefault o k d = liftM (fromMaybe d) $ maybeFromObj o k
index f1776df..015b63e 100644 (file)
@@ -55,13 +55,13 @@ import System.Log.Formatter
 import System.IO
 
 import Ganeti.THH
-import qualified Ganeti.Constants as C
+import qualified Ganeti.ConstantUtils as ConstantUtils
 
 -- | Syslog usage type.
-$(declareSADT "SyslogUsage"
-  [ ("SyslogNo",   'C.syslogNo)
-  , ("SyslogYes",  'C.syslogYes)
-  , ("SyslogOnly", 'C.syslogOnly)
+$(declareLADT ''String "SyslogUsage"
+  [ ("SyslogNo",   "no")
+  , ("SyslogYes",  "yes")
+  , ("SyslogOnly", "only")
   ])
 
 -- | Builds the log formatter.
@@ -98,7 +98,7 @@ setupLogging :: Maybe String    -- ^ Log file
              -> 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
+      destf = if console then Just ConstantUtils.devConsole else logf
       fmt = logFormatter program False False
       file_logging = syslog /= SyslogOnly
 
index ecad598..570992f 100644 (file)
@@ -76,8 +76,8 @@ import Ganeti.Errors
 import Ganeti.JSON
 import Ganeti.OpParams (pTagsObject)
 import Ganeti.OpCodes
-import Ganeti.Runtime
 import qualified Ganeti.Query.Language as Qlang
+import Ganeti.Runtime (GanetiDaemon(..), MiscGroup(..), GanetiGroup(..))
 import Ganeti.THH
 import Ganeti.Types
 import Ganeti.Utils
@@ -144,10 +144,15 @@ $(genLuxiOp "LuxiOp"
     )
   , (luxiReqQueryClusterInfo, [])
   , (luxiReqQueryTags,
-     [ pTagsObject ])
+     [ pTagsObject 
+     , simpleField "name" [t| String |]
+     ])
   , (luxiReqSubmitJob,
      [ simpleField "job" [t| [MetaOpCode] |] ]
     )
+  , (luxiReqSubmitJobToDrainedQueue,
+     [ simpleField "job" [t| [MetaOpCode] |] ]
+    )
   , (luxiReqSubmitManyJobs,
      [ simpleField "ops" [t| [[MetaOpCode]] |] ]
     )
@@ -366,6 +371,10 @@ decodeCall (LuxiCall call args) =
               [ops1] <- fromJVal args
               ops2 <- mapM (fromJResult (luxiReqToRaw call) . J.readJSON) ops1
               return $ SubmitJob ops2
+    ReqSubmitJobToDrainedQueue -> do
+              [ops1] <- fromJVal args
+              ops2 <- mapM (fromJResult (luxiReqToRaw call) . J.readJSON) ops1
+              return $ SubmitJobToDrainedQueue ops2
     ReqSubmitManyJobs -> do
               [ops1] <- fromJVal args
               ops2 <- mapM (fromJResult (luxiReqToRaw call) . J.readJSON) ops1
@@ -399,8 +408,7 @@ decodeCall (LuxiCall call args) =
               return $ QueryConfigValues fields
     ReqQueryTags -> do
               (kind, name) <- fromJVal args
-              item <- tagObjectFrom kind name
-              return $ QueryTags item
+              return $ QueryTags kind name
     ReqCancelJob -> do
               [jid] <- fromJVal args
               return $ CancelJob jid
index dc010b4..59968d2 100644 (file)
@@ -36,18 +36,22 @@ import Control.Monad
 import Control.Monad.IO.Class
 import Data.ByteString.Char8 hiding (map, filter, find)
 import Data.List
+import qualified Data.Map as Map
 import Snap.Core
 import Snap.Http.Server
 import qualified Text.JSON as J
+import Control.Concurrent
 
 import qualified Ganeti.BasicTypes as BT
 import Ganeti.Daemon
+import qualified Ganeti.DataCollectors.CPUload as CPUload
 import qualified Ganeti.DataCollectors.Diskstats as Diskstats
 import qualified Ganeti.DataCollectors.Drbd as Drbd
 import qualified Ganeti.DataCollectors.InstStatus as InstStatus
 import qualified Ganeti.DataCollectors.Lv as Lv
 import Ganeti.DataCollectors.Types
 import qualified Ganeti.Constants as C
+import Ganeti.Runtime
 
 -- * Types and constants definitions
 
@@ -59,7 +63,11 @@ type PrepResult = Config Snap ()
 
 -- | Version of the latest supported http API.
 latestAPIVersion :: Int
-latestAPIVersion = 1
+latestAPIVersion = C.mondLatestApiVersion
+
+-- | A report of a data collector might be stateful or stateless.
+data Report = StatelessR (IO DCReport)
+            | StatefulR (Maybe CollectorData -> IO DCReport)
 
 -- | Type describing a data collector basic information
 data DataCollector = DataCollector
@@ -68,28 +76,35 @@ data DataCollector = DataCollector
                                   --   of the collector
   , dKind     :: DCKind           -- ^ Kind (performance or status reporting) of
                                   --   the data collector
-  , dReport   :: IO DCReport      -- ^ Report produced by the collector
+  , dReport   :: Report           -- ^ Report produced by the collector
+  , dUpdate   :: Maybe (Maybe CollectorData -> IO CollectorData)
+                                  -- ^ Update operation for stateful collectors.
   }
 
+
 -- | The list of available builtin data collectors.
 collectors :: [DataCollector]
 collectors =
   [ DataCollector Diskstats.dcName Diskstats.dcCategory Diskstats.dcKind
-      Diskstats.dcReport
-  , DataCollector Drbd.dcName Drbd.dcCategory Drbd.dcKind Drbd.dcReport
+      (StatelessR Diskstats.dcReport) Nothing
+  , DataCollector Drbd.dcName Drbd.dcCategory Drbd.dcKind
+      (StatelessR Drbd.dcReport) Nothing
   , DataCollector InstStatus.dcName InstStatus.dcCategory InstStatus.dcKind
-      InstStatus.dcReport
-  , DataCollector Lv.dcName Lv.dcCategory Lv.dcKind Lv.dcReport
+      (StatelessR InstStatus.dcReport) Nothing
+  , DataCollector Lv.dcName Lv.dcCategory Lv.dcKind
+      (StatelessR Lv.dcReport) Nothing
+  , DataCollector CPUload.dcName CPUload.dcCategory CPUload.dcKind
+      (StatefulR CPUload.dcReport) (Just CPUload.dcUpdate)
   ]
 
 -- * Configuration handling
 
 -- | The default configuration for the HTTP server.
-defaultHttpConf :: Config Snap ()
-defaultHttpConf =
-  setAccessLog (ConfigFileLog C.daemonsExtraLogfilesGanetiMondAccess) .
+defaultHttpConf :: FilePath -> FilePath -> Config Snap ()
+defaultHttpConf accessLog errorLog =
+  setAccessLog (ConfigFileLog accessLog) .
   setCompression False .
-  setErrorLog (ConfigFileLog C.daemonsExtraLogfilesGanetiMondError) $
+  setErrorLog (ConfigFileLog errorLog) $
   setVerbose False
   emptyConfig
 
@@ -101,10 +116,13 @@ checkMain _ = return $ Right ()
 
 -- | Prepare function for monitoring agent.
 prepMain :: PrepFn CheckResult PrepResult
-prepMain opts _ =
+prepMain opts _ = do
+  accessLog <- daemonsExtraLogFile GanetiMond AccessLog
+  errorLog <- daemonsExtraLogFile GanetiMond ErrorLog
   return $
-    setPort (maybe C.defaultMondPort fromIntegral (optPort opts))
-      defaultHttpConf
+    setPort
+      (maybe C.defaultMondPort fromIntegral (optPort opts))
+      (defaultHttpConf accessLog errorLog)
 
 -- * Query answers
 
@@ -113,13 +131,13 @@ versionQ :: Snap ()
 versionQ = writeBS . pack $ J.encode [latestAPIVersion]
 
 -- | Version 1 of the monitoring HTTP API.
-version1Api :: Snap ()
-version1Api =
+version1Api :: MVar CollectorMap -> Snap ()
+version1Api mvar =
   let returnNull = writeBS . pack $ J.encode J.JSNull :: Snap ()
   in ifTop returnNull <|>
      route
        [ ("list", listHandler)
-       , ("report", reportHandler)
+       , ("report", reportHandler mvar)
        ]
 
 -- | Get the JSON representation of a data collector to be used in the collector
@@ -138,20 +156,36 @@ listHandler =
   dir "collectors" . writeBS . pack . J.encode $ map dcListItem collectors
 
 -- | Handler for returning data collector reports.
-reportHandler :: Snap ()
-reportHandler =
+reportHandler :: MVar CollectorMap -> Snap ()
+reportHandler mvar =
   route
-    [ ("all", allReports)
-    , (":category/:collector", oneReport)
+    [ ("all", allReports mvar)
+    , (":category/:collector", oneReport mvar)
     ] <|>
   errorReport
 
 -- | Return the report of all the available collectors.
-allReports :: Snap ()
-allReports = do
-  reports <- mapM (liftIO . dReport) collectors
+allReports :: MVar CollectorMap -> Snap ()
+allReports mvar = do
+  reports <- mapM (liftIO . getReport mvar) collectors
   writeBS . pack . J.encode $ reports
 
+-- | Takes the CollectorMap and a DataCollector and returns the report for this
+-- collector.
+getReport :: MVar CollectorMap -> DataCollector -> IO DCReport
+getReport mvar collector =
+  case dReport collector of
+    StatelessR r -> r
+    StatefulR r -> do
+      colData <- getColData (dName collector) mvar
+      r colData
+
+-- | Returns the data for the corresponding collector.
+getColData :: String -> MVar CollectorMap -> IO (Maybe CollectorData)
+getColData name mvar = do
+  m <- readMVar mvar
+  return $ Map.lookup name m
+
 -- | Returns a category given its name.
 -- If "collector" is given as the name, the collector has no category, and
 -- Nothing will be returned.
@@ -173,9 +207,9 @@ error404 = do
   modifyResponse $ setResponseStatus 404 "Not found"
   writeBS "Resource not found"
 
--- | Return the report of one collector
-oneReport :: Snap ()
-oneReport = do
+-- | Return the report of one collector.
+oneReport :: MVar CollectorMap -> Snap ()
+oneReport mvar = do
   categoryName <- fmap (maybe mzero unpack) $ getParam "category"
   collectorName <- fmap (maybe mzero unpack) $ getParam "collector"
   category <-
@@ -188,17 +222,45 @@ oneReport = do
         filter (\c -> category == dCategory c) collectors of
       Just col -> return col
       Nothing -> fail "Unable to find the requested collector"
-  report <- liftIO $ dReport collector
-  writeBS . pack . J.encode $ report
+  dcr <- liftIO $ getReport mvar collector
+  writeBS . pack . J.encode $ dcr
 
 -- | The function implementing the HTTP API of the monitoring agent.
-monitoringApi :: Snap ()
-monitoringApi =
+monitoringApi :: MVar CollectorMap -> Snap ()
+monitoringApi mvar =
   ifTop versionQ <|>
-  dir "1" version1Api <|>
+  dir "1" (version1Api mvar) <|>
   error404
 
+-- | The function collecting data for each data collector providing a dcUpdate
+-- function.
+collect :: CollectorMap -> DataCollector -> IO CollectorMap
+collect m collector =
+  case dUpdate collector of
+    Nothing -> return m
+    Just update -> do
+      let name = dName collector
+          existing = Map.lookup name m
+      new_data <- update existing
+      return $ Map.insert name new_data m
+
+-- | Invokes collect for each data collector.
+collection :: CollectorMap -> IO CollectorMap
+collection m = foldM collect m collectors
+
+-- | The thread responsible for the periodical collection of data for each data
+-- data collector.
+collectord :: MVar CollectorMap -> IO ()
+collectord mvar = do
+  m <- takeMVar mvar
+  m' <- collection m
+  putMVar mvar m'
+  threadDelay $ 10^(6 :: Int) * C.mondTimeInterval
+  collectord mvar
+
 -- | Main function.
 main :: MainFn CheckResult PrepResult
-main _ _ httpConf =
-  httpServe httpConf $ method GET monitoringApi
+main _ _ httpConf = do
+  mvar <- newMVar Map.empty
+  _ <- forkIO $ collectord mvar
+  httpServe httpConf . method GET $ monitoringApi mvar
index c7e1cf1..86161dd 100644 (file)
@@ -29,9 +29,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 -}
 
 module Ganeti.Objects
-  ( VType(..)
-  , vTypeFromRaw
-  , HvParams
+  ( HvParams
   , OsParams
   , PartialNicParams(..)
   , FilledNicParams(..)
@@ -39,8 +37,6 @@ module Ganeti.Objects
   , allNicParamFields
   , PartialNic(..)
   , FileDriver(..)
-  , BlockDriver(..)
-  , DiskMode(..)
   , DiskLogicalId(..)
   , Disk(..)
   , includesLogicalId
@@ -49,8 +45,6 @@ module Ganeti.Objects
   , FilledBeParams(..)
   , fillBeParams
   , allBeParamFields
-  , AdminState(..)
-  , adminStateFromRaw
   , Instance(..)
   , toDictInstance
   , PartialNDParams(..)
@@ -58,9 +52,6 @@ module Ganeti.Objects
   , fillNDParams
   , allNDParamFields
   , Node(..)
-  , NodeRole(..)
-  , nodeRoleToRaw
-  , roleDescription
   , AllocPolicy(..)
   , FilledISpecParams(..)
   , PartialISpecParams(..)
@@ -105,6 +96,7 @@ import Text.JSON (showJSON, readJSON, JSON, JSValue(..), fromJSString)
 import qualified Text.JSON as J
 
 import qualified Ganeti.Constants as C
+import qualified Ganeti.ConstantUtils as ConstantUtils
 import Ganeti.JSON
 import Ganeti.Types
 import Ganeti.THH
@@ -119,16 +111,6 @@ fillDict defaults custom skip_keys =
   let updated = Map.union custom defaults
   in foldl' (flip Map.delete) updated skip_keys
 
--- | The VTYPES, a mini-type system in Python.
-$(declareSADT "VType"
-  [ ("VTypeString",      'C.vtypeString)
-  , ("VTypeMaybeString", 'C.vtypeMaybeString)
-  , ("VTypeBool",        'C.vtypeBool)
-  , ("VTypeSize",        'C.vtypeSize)
-  , ("VTypeInt",         'C.vtypeInt)
-  ])
-$(makeJSONInstance ''VType)
-
 -- | The hypervisor parameter type. This is currently a simple map,
 -- without type checking on key/value pairs.
 type HvParams = Container JSValue
@@ -155,25 +137,6 @@ class SerialNoObject a where
 class TagsObject a where
   tagsOf :: a -> Set.Set String
 
--- * Node role object
-
-$(declareSADT "NodeRole"
-  [ ("NROffline",   'C.nrOffline)
-  , ("NRDrained",   'C.nrDrained)
-  , ("NRRegular",   'C.nrRegular)
-  , ("NRCandidate", 'C.nrMcandidate)
-  , ("NRMaster",    'C.nrMaster)
-  ])
-$(makeJSONInstance ''NodeRole)
-
--- | The description of the node role.
-roleDescription :: NodeRole -> String
-roleDescription NROffline   = "offline"
-roleDescription NRDrained   = "drained"
-roleDescription NRRegular   = "regular"
-roleDescription NRCandidate = "master candidate"
-roleDescription NRMaster    = "master"
-
 -- * Network definitions
 
 -- ** Ipv4 types
@@ -281,6 +244,7 @@ instance TimeStampObject Network where
 $(buildParam "Nic" "nicp"
   [ simpleField "mode" [t| NICMode |]
   , simpleField "link" [t| String  |]
+  , simpleField "vlan" [t| Maybe String |]
   ])
 
 $(buildObject "PartialNic" "nic" $
@@ -296,18 +260,6 @@ instance UuidObject PartialNic where
 
 -- * Disk definitions
 
-$(declareSADT "DiskMode"
-  [ ("DiskRdOnly", 'C.diskRdonly)
-  , ("DiskRdWr",   'C.diskRdwr)
-  ])
-$(makeJSONInstance ''DiskMode)
-
--- | 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"
@@ -428,7 +380,6 @@ decodeDLId obj lid = do
 -- code currently can't build it.
 data Disk = Disk
   { diskLogicalId  :: DiskLogicalId
---  , diskPhysicalId :: String
   , diskChildren   :: [Disk]
   , diskIvName     :: String
   , diskSize       :: Int
@@ -441,7 +392,6 @@ data Disk = Disk
 $(buildObjectSerialisation "Disk" $
   [ customField 'decodeDLId 'encodeFullDLId ["dev_type"] $
       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 |]
@@ -465,16 +415,8 @@ includesLogicalId vg_name lv_name disk =
       any (includesLogicalId vg_name lv_name) $ diskChildren disk
     _ -> False
 
-
 -- * Instance definitions
 
-$(declareSADT "AdminState"
-  [ ("AdminOffline", 'C.adminstOffline)
-  , ("AdminDown",    'C.adminstDown)
-  , ("AdminUp",      'C.adminstUp)
-  ])
-$(makeJSONInstance ''AdminState)
-
 $(buildParam "Be" "bep"
   [ simpleField "minmem"       [t| Int  |]
   , simpleField "maxmem"       [t| Int  |]
@@ -518,12 +460,12 @@ instance TagsObject Instance where
 -- * IPolicy definitions
 
 $(buildParam "ISpec" "ispec"
-  [ simpleField C.ispecMemSize     [t| Int |]
-  , simpleField C.ispecDiskSize    [t| Int |]
-  , simpleField C.ispecDiskCount   [t| Int |]
-  , simpleField C.ispecCpuCount    [t| Int |]
-  , simpleField C.ispecNicCount    [t| Int |]
-  , simpleField C.ispecSpindleUse  [t| Int |]
+  [ simpleField ConstantUtils.ispecMemSize     [t| Int |]
+  , simpleField ConstantUtils.ispecDiskSize    [t| Int |]
+  , simpleField ConstantUtils.ispecDiskCount   [t| Int |]
+  , simpleField ConstantUtils.ispecCpuCount    [t| Int |]
+  , simpleField ConstantUtils.ispecNicCount    [t| Int |]
+  , simpleField ConstantUtils.ispecSpindleUse  [t| Int |]
   ])
 
 $(buildObject "MinMaxISpecs" "mmis"
@@ -534,23 +476,23 @@ $(buildObject "MinMaxISpecs" "mmis"
 -- | Custom partial ipolicy. This is not built via buildParam since it
 -- has a special 2-level inheritance mode.
 $(buildObject "PartialIPolicy" "ipolicy"
-  [ optionalField . renameField "MinMaxISpecsP"
-                    $ simpleField C.ispecsMinmax   [t| [MinMaxISpecs] |]
-  , optionalField . renameField "StdSpecP"
-                    $ simpleField "std"            [t| PartialISpecParams |]
-  , optionalField . renameField "SpindleRatioP"
-                    $ simpleField "spindle-ratio"  [t| Double |]
-  , optionalField . renameField "VcpuRatioP"
-                    $ simpleField "vcpu-ratio"     [t| Double |]
-  , optionalField . renameField "DiskTemplatesP"
-                    $ simpleField "disk-templates" [t| [DiskTemplate] |]
+  [ optionalField . renameField "MinMaxISpecsP" $
+    simpleField ConstantUtils.ispecsMinmax [t| [MinMaxISpecs] |]
+  , optionalField . renameField "StdSpecP" $
+    simpleField "std" [t| PartialISpecParams |]
+  , optionalField . renameField "SpindleRatioP" $
+    simpleField "spindle-ratio" [t| Double |]
+  , optionalField . renameField "VcpuRatioP" $
+    simpleField "vcpu-ratio" [t| Double |]
+  , optionalField . renameField "DiskTemplatesP" $
+    simpleField "disk-templates" [t| [DiskTemplate] |]
   ])
 
 -- | Custom filled ipolicy. This is not built via buildParam since it
 -- has a special 2-level inheritance mode.
 $(buildObject "FilledIPolicy" "ipolicy"
-  [ renameField "MinMaxISpecs"
-    $ simpleField C.ispecsMinmax [t| [MinMaxISpecs] |]
+  [ renameField "MinMaxISpecs" $
+    simpleField ConstantUtils.ispecsMinmax [t| [MinMaxISpecs] |]
   , renameField "StdSpec" $ simpleField "std" [t| FilledISpecParams |]
   , simpleField "spindle-ratio"  [t| Double |]
   , simpleField "vcpu-ratio"     [t| Double |]
@@ -584,6 +526,9 @@ $(buildParam "ND" "ndp"
   [ simpleField "oob_program"   [t| String |]
   , simpleField "spindle_count" [t| Int    |]
   , simpleField "exclusive_storage" [t| Bool |]
+  , simpleField "ovs"           [t| Bool |]
+  , simpleField "ovs_name"       [t| String |]
+  , simpleField "ovs_link"       [t| String |]
   ])
 
 $(buildObject "Node" "node" $
index d7f75ef..421f0c5 100644 (file)
@@ -1,4 +1,5 @@
-{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE ExistentialQuantification, TemplateHaskell #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
 
 {-| Implementation of the opcodes.
 
@@ -26,11 +27,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 -}
 
 module Ganeti.OpCodes
-  ( OpCode(..)
-  , TagObject(..)
-  , tagObjectFrom
-  , encodeTagObject
-  , decodeTagObject
+  ( pyClasses
+  , OpCode(..)
   , ReplaceDisksMode(..)
   , DiskIndex
   , mkDiskIndex
@@ -47,116 +45,164 @@ module Ganeti.OpCodes
   , setOpPriority
   ) where
 
-import Data.Maybe (fromMaybe)
-import Text.JSON (readJSON, JSON, JSValue, makeObj)
+import Text.JSON (readJSON, JSObject, JSON, JSValue(..), makeObj, fromJSObject)
 import qualified Text.JSON
 
 import Ganeti.THH
 
+import qualified Ganeti.Hs2Py.OpDoc as OpDoc
 import Ganeti.OpParams
-import Ganeti.Types (OpSubmitPriority(..), fromNonEmpty)
+import Ganeti.PyValueInstances ()
+import Ganeti.Types
 import Ganeti.Query.Language (queryTypeOpToRaw)
 
+import Data.List (intercalate)
+import Data.Map (Map)
+
+import qualified Ganeti.Constants as C
+
+instance PyValue DiskIndex where
+  showValue = showValue . unDiskIndex
+
+instance PyValue IDiskParams where
+  showValue _ = error "OpCodes.showValue(IDiskParams): unhandled case"
+
+instance PyValue RecreateDisksInfo where
+  showValue RecreateDisksAll = "[]"
+  showValue (RecreateDisksIndices is) = showValue is
+  showValue (RecreateDisksParams is) = showValue is
+
+instance PyValue a => PyValue (SetParamsMods a) where
+  showValue SetParamsEmpty = "[]"
+  showValue _ = error "OpCodes.showValue(SetParamsMods): unhandled case"
+
+instance PyValue a => PyValue (NonNegative a) where
+  showValue = showValue . fromNonNegative
+  
+instance PyValue a => PyValue (NonEmpty a) where
+  showValue = showValue . fromNonEmpty
+  
+-- FIXME: should use the 'toRaw' function instead of being harcoded or
+-- perhaps use something similar to the NonNegative type instead of
+-- using the declareSADT
+instance PyValue ExportMode where
+  showValue ExportModeLocal = show C.exportModeLocal
+  showValue ExportModeRemote = show C.exportModeLocal
+
+instance PyValue CVErrorCode where
+  showValue = cVErrorCodeToRaw
+
+instance PyValue VerifyOptionalChecks where
+  showValue = verifyOptionalChecksToRaw
+
+instance PyValue INicParams where
+  showValue = error "instance PyValue INicParams: not implemented"
+
+instance PyValue a => PyValue (JSObject a) where
+  showValue obj =
+    "{" ++ intercalate ", " (map showPair (fromJSObject obj)) ++ "}"
+    where showPair (k, v) = show k ++ ":" ++ showValue v
+
+instance PyValue JSValue where
+  showValue (JSObject obj) = showValue obj
+  showValue x = show x
+
+type JobIdListOnly = [(Bool, Either String JobId)]
+
+type InstanceMultiAllocResponse =
+  ([(Bool, Either String JobId)], NonEmptyString)
+
+type QueryFieldDef =
+  (NonEmptyString, NonEmptyString, TagKind, NonEmptyString)
+
+type QueryResponse =
+  ([QueryFieldDef], [[(QueryResultCode, JSValue)]])
+
+type QueryFieldsResponse = [QueryFieldDef]
+
 -- | OpCode representation.
 --
 -- We only implement a subset of Ganeti opcodes: those which are actually used
 -- in the htools codebase.
 $(genOpCode "OpCode"
-  [ ("OpTestDelay",
-     [ pDelayDuration
-     , pDelayOnMaster
-     , pDelayOnNodes
-     , pDelayOnNodeUuids
-     , pDelayRepeat
-     ])
-  , ("OpInstanceReplaceDisks",
-     [ pInstanceName
-     , pInstanceUuid
-     , pEarlyRelease
-     , pIgnoreIpolicy
-     , pReplaceDisksMode
-     , pReplaceDisksList
-     , pRemoteNode
-     , pRemoteNodeUuid
-     , pIallocator
-     ])
-  , ("OpInstanceFailover",
-     [ pInstanceName
-     , pInstanceUuid
-     , pShutdownTimeout
-     , pIgnoreConsistency
-     , pMigrationTargetNode
-     , pMigrationTargetNodeUuid
-     , pIgnoreIpolicy
-     , pIallocator
-     , pMigrationCleanup
-     ])
-  , ("OpInstanceMigrate",
-     [ pInstanceName
-     , pInstanceUuid
-     , pMigrationMode
-     , pMigrationLive
-     , pMigrationTargetNode
-     , pMigrationTargetNodeUuid
-     , pAllowRuntimeChgs
-     , pIgnoreIpolicy
-     , pMigrationCleanup
-     , pIallocator
-     , pAllowFailover
-     ])
-  , ("OpTagsGet",
-     [ pTagsObject
-     , pUseLocking
-     ])
-  , ("OpTagsSearch",
-     [ pTagSearchPattern ])
-  , ("OpTagsSet",
-     [ pTagsObject
-     , pTagsList
-     ])
-  , ("OpTagsDel",
-     [ pTagsObject
-     , pTagsList
-     ])
-  , ("OpClusterPostInit", [])
-  , ("OpClusterDestroy", [])
-  , ("OpClusterQuery", [])
+  [ ("OpClusterPostInit",
+     [t| Bool |],
+     OpDoc.opClusterPostInit,
+     [],
+     [])
+  , ("OpClusterDestroy",
+     [t| NonEmptyString |],
+     OpDoc.opClusterDestroy,
+     [],
+     [])
+  , ("OpClusterQuery",
+     [t| JSObject JSValue |],
+     OpDoc.opClusterQuery,
+     [],
+     [])
   , ("OpClusterVerify",
+     [t| JobIdListOnly |],
+     OpDoc.opClusterVerify,
      [ pDebugSimulateErrors
      , pErrorCodes
      , pSkipChecks
      , pIgnoreErrors
      , pVerbose
      , pOptGroupName
-     ])
+     ],
+     [])
   , ("OpClusterVerifyConfig",
+     [t| Bool |],
+     OpDoc.opClusterVerifyConfig,
      [ pDebugSimulateErrors
      , pErrorCodes
      , pIgnoreErrors
      , pVerbose
-     ])
+     ],
+     [])
   , ("OpClusterVerifyGroup",
+     [t| Bool |],
+     OpDoc.opClusterVerifyGroup,
      [ pGroupName
      , pDebugSimulateErrors
      , pErrorCodes
      , pSkipChecks
      , pIgnoreErrors
      , pVerbose
-     ])
-  , ("OpClusterVerifyDisks", [])
+     ],
+     "group_name")
+  , ("OpClusterVerifyDisks",
+     [t| JobIdListOnly |],
+     OpDoc.opClusterVerifyDisks,
+     [],
+     [])
   , ("OpGroupVerifyDisks",
+     [t| (Map String String, [String], Map String [[String]]) |],
+     OpDoc.opGroupVerifyDisks,
      [ pGroupName
-     ])
+     ],
+     "group_name")
   , ("OpClusterRepairDiskSizes",
+     [t| [(NonEmptyString, NonNegative Int, NonEmptyString, NonNegative Int)]|],
+     OpDoc.opClusterRepairDiskSizes,
      [ pInstances
-     ])
+     ],
+     [])
   , ("OpClusterConfigQuery",
+     [t| [JSValue] |],
+     OpDoc.opClusterConfigQuery,
      [ pOutputFields
-     ])
+     ],
+     [])
   , ("OpClusterRename",
+      [t| NonEmptyString |],
+      OpDoc.opClusterRename,
      [ pName
-     ])
+     ],
+     "name")
   , ("OpClusterSetParams",
+     [t| () |],
+     OpDoc.opClusterSetParams,
      [ pForce
      , pHvState
      , pDiskState
@@ -174,8 +220,8 @@ $(genOpCode "OpCode"
      , pMaintainNodeHealth
      , pPreallocWipeDisks
      , pNicParams
-     , pNdParams
-     , pIpolicy
+     , withDoc "Cluster-wide node parameter defaults" pNdParams
+     , withDoc "Cluster-wide ipolicy specs" pIpolicy
      , pDrbdHelper
      , pDefaultIAllocator
      , pMasterNetdev
@@ -186,35 +232,75 @@ $(genOpCode "OpCode"
      , pUseExternalMipScript
      , pEnabledDiskTemplates
      , pModifyEtcHosts
-     , pGlobalFileStorageDir
-     , pGlobalSharedFileStorageDir
-     ])
-  , ("OpClusterRedistConf", [])
-  , ("OpClusterActivateMasterIp", [])
-  , ("OpClusterDeactivateMasterIp", [])
+     , pClusterFileStorageDir
+     , pClusterSharedFileStorageDir
+     ],
+     [])
+  , ("OpClusterRedistConf",
+     [t| () |],
+     OpDoc.opClusterRedistConf,
+     [],
+     [])
+  , ("OpClusterActivateMasterIp",
+     [t| () |],
+     OpDoc.opClusterActivateMasterIp,
+     [],
+     [])
+  , ("OpClusterDeactivateMasterIp",
+     [t| () |],
+     OpDoc.opClusterDeactivateMasterIp,
+     [],
+     [])
   , ("OpQuery",
+     [t| QueryResponse |],
+     OpDoc.opQuery,
      [ pQueryWhat
      , pUseLocking
      , pQueryFields
      , pQueryFilter
-     ])
+     ],
+     "what")
   , ("OpQueryFields",
+     [t| QueryFieldsResponse |],
+     OpDoc.opQueryFields,
      [ pQueryWhat
-     , pQueryFields
-     ])
+     , pQueryFieldsFields
+     ],
+     "what")
   , ("OpOobCommand",
+     [t| [[(QueryResultCode, JSValue)]] |],
+     OpDoc.opOobCommand,
      [ pNodeNames
-     , pNodeUuids
+     , withDoc "List of node UUIDs to run the OOB command against" pNodeUuids
      , pOobCommand
      , pOobTimeout
      , pIgnoreStatus
      , pPowerDelay
-     ])
+     ],
+     [])
+  , ("OpRestrictedCommand",
+     [t| [(Bool, String)] |],
+     OpDoc.opRestrictedCommand,
+     [ pUseLocking
+     , withDoc
+       "Nodes on which the command should be run (at least one)"
+       pRequiredNodes
+     , withDoc
+       "Node UUIDs on which the command should be run (at least one)"
+       pRequiredNodeUuids
+     , pRestrictedCommand
+     ],
+     [])
   , ("OpNodeRemove",
+     [t| () |],
+      OpDoc.opNodeRemove,
      [ pNodeName
      , pNodeUuid
-     ])
+     ],
+     "node_name")
   , ("OpNodeAdd",
+     [t| () |],
+      OpDoc.opNodeAdd,
      [ pNodeName
      , pHvState
      , pDiskState
@@ -225,40 +311,64 @@ $(genOpCode "OpCode"
      , pMasterCapable
      , pVmCapable
      , pNdParams
-    ])
-  , ("OpNodeQuery", dOldQuery)
+     ],
+     "node_name")
+  , ("OpNodeQuery",
+     [t| [[JSValue]] |],
+     OpDoc.opNodeQuery,
+     [ pOutputFields
+     , withDoc "Empty list to query all nodes, node names otherwise" pNames
+     , pUseLocking
+     ],
+     [])
   , ("OpNodeQueryvols",
+     [t| [JSValue] |],
+     OpDoc.opNodeQueryvols,
      [ pOutputFields
-     , pNodes
-     ])
+     , withDoc "Empty list to query all nodes, node names otherwise" pNodes
+     ],
+     [])
   , ("OpNodeQueryStorage",
+     [t| [[JSValue]] |],
+     OpDoc.opNodeQueryStorage,
      [ pOutputFields
-     , pStorageType
-     , pNodes
+     , pStorageTypeOptional
+     , withDoc
+       "Empty list to query all, list of names to query otherwise"
+       pNodes
      , pStorageName
-     ])
+     ],
+     [])
   , ("OpNodeModifyStorage",
+     [t| () |],
+     OpDoc.opNodeModifyStorage,
      [ pNodeName
      , pNodeUuid
      , pStorageType
      , pStorageName
      , pStorageChanges
-     ])
+     ],
+     "node_name")
   , ("OpRepairNodeStorage",
+      [t| () |],
+      OpDoc.opRepairNodeStorage,
      [ pNodeName
      , pNodeUuid
      , pStorageType
      , pStorageName
      , pIgnoreConsistency
-     ])
+     ],
+     "node_name")
   , ("OpNodeSetParams",
+     [t| [(NonEmptyString, JSValue)] |],
+     OpDoc.opNodeSetParams,
      [ pNodeName
      , pNodeUuid
      , pForce
      , pHvState
      , pDiskState
      , pMasterCandidate
-     , pOffline
+     , withDoc "Whether to mark the node offline" pOffline
      , pDrained
      , pAutoPromote
      , pMasterCapable
@@ -266,13 +376,19 @@ $(genOpCode "OpCode"
      , pSecondaryIp
      , pNdParams
      , pPowered
-     ])
+     ],
+     "node_name")
   , ("OpNodePowercycle",
+     [t| Maybe NonEmptyString |],
+     OpDoc.opNodePowercycle,
      [ pNodeName
      , pNodeUuid
      , pForce
-     ])
+     ],
+     "node_name")
   , ("OpNodeMigrate",
+     [t| JobIdListOnly |],
+     OpDoc.opNodeMigrate,
      [ pNodeName
      , pNodeUuid
      , pMigrationMode
@@ -282,8 +398,11 @@ $(genOpCode "OpCode"
      , pAllowRuntimeChgs
      , pIgnoreIpolicy
      , pIallocator
-     ])
+     ],
+     "node_name")
   , ("OpNodeEvacuate",
+     [t| JobIdListOnly |],
+     OpDoc.opNodeEvacuate,
      [ pEarlyRelease
      , pNodeName
      , pNodeUuid
@@ -291,16 +410,20 @@ $(genOpCode "OpCode"
      , pRemoteNodeUuid
      , pIallocator
      , pEvacMode
-     ])
+     ],
+     "node_name")
   , ("OpInstanceCreate",
+     [t| [NonEmptyString] |],
+     OpDoc.opInstanceCreate,
      [ pInstanceName
      , pForceVariant
      , pWaitForSync
      , pNameCheck
      , pIgnoreIpolicy
+     , pOpportunisticLocking
      , pInstBeParams
      , pInstDisks
-     , pDiskTemplate
+     , pOptDiskTemplate
      , pFileDriver
      , pFileStorageDir
      , pInstHvParams
@@ -326,35 +449,49 @@ $(genOpCode "OpCode"
      , pSrcNodeUuid
      , pSrcPath
      , pStartInstance
-     , pOpportunisticLocking
      , pInstTags
-     ])
+     ],
+     "instance_name")
   , ("OpInstanceMultiAlloc",
-     [ pIallocator
+     [t| InstanceMultiAllocResponse |],
+     OpDoc.opInstanceMultiAlloc,
+     [ pOpportunisticLocking
+     , pIallocator
      , pMultiAllocInstances
-     , pOpportunisticLocking
-     ])
+     ],
+     [])
   , ("OpInstanceReinstall",
+     [t| () |],
+     OpDoc.opInstanceReinstall,
      [ pInstanceName
      , pInstanceUuid
      , pForceVariant
      , pInstOs
      , pTempOsParams
-     ])
+     ],
+     "instance_name")
   , ("OpInstanceRemove",
+     [t| () |],
+     OpDoc.opInstanceRemove,
      [ pInstanceName
      , pInstanceUuid
      , pShutdownTimeout
      , pIgnoreFailures
-     ])
+     ],
+     "instance_name")
   , ("OpInstanceRename",
+     [t| NonEmptyString |],
+     OpDoc.opInstanceRename,
      [ pInstanceName
      , pInstanceUuid
-     , pNewName
+     , withDoc "New instance name" pNewName
      , pNameCheck
      , pIpCheck
-     ])
+     ],
+     [])
   , ("OpInstanceStartup",
+     [t| () |],
+     OpDoc.opInstanceStartup,
      [ pInstanceName
      , pInstanceUuid
      , pForce
@@ -363,23 +500,76 @@ $(genOpCode "OpCode"
      , pTempBeParams
      , pNoRemember
      , pStartupPaused
-     ])
+     ],
+     "instance_name")
   , ("OpInstanceShutdown",
+     [t| () |],
+     OpDoc.opInstanceShutdown,
      [ pInstanceName
      , pInstanceUuid
      , pForce
      , pIgnoreOfflineNodes
      , pShutdownTimeout'
      , pNoRemember
-     ])
+     ],
+     "instance_name")
   , ("OpInstanceReboot",
+     [t| () |],
+     OpDoc.opInstanceReboot,
      [ pInstanceName
      , pInstanceUuid
      , pShutdownTimeout
      , pIgnoreSecondaries
      , pRebootType
-     ])
+     ],
+     "instance_name")
+  , ("OpInstanceReplaceDisks",
+     [t| () |],
+     OpDoc.opInstanceReplaceDisks,
+     [ pInstanceName
+     , pInstanceUuid
+     , pEarlyRelease
+     , pIgnoreIpolicy
+     , pReplaceDisksMode
+     , pReplaceDisksList
+     , pRemoteNode
+     , pRemoteNodeUuid
+     , pIallocator
+     ],
+     "instance_name")
+  , ("OpInstanceFailover",
+     [t| () |],
+     OpDoc.opInstanceFailover,
+     [ pInstanceName
+     , pInstanceUuid
+     , pShutdownTimeout
+     , pIgnoreConsistency
+     , pMigrationTargetNode
+     , pMigrationTargetNodeUuid
+     , pIgnoreIpolicy
+     , pMigrationCleanup
+     , pIallocator
+     ],
+     "instance_name")
+  , ("OpInstanceMigrate",
+     [t| () |],
+     OpDoc.opInstanceMigrate,
+     [ pInstanceName
+     , pInstanceUuid
+     , pMigrationMode
+     , pMigrationLive
+     , pMigrationTargetNode
+     , pMigrationTargetNodeUuid
+     , pAllowRuntimeChgs
+     , pIgnoreIpolicy
+     , pMigrationCleanup
+     , pIallocator
+     , pAllowFailover
+     ],
+     "instance_name")
   , ("OpInstanceMove",
+     [t| () |],
+     OpDoc.opInstanceMove,
      [ pInstanceName
      , pInstanceUuid
      , pShutdownTimeout
@@ -387,37 +577,64 @@ $(genOpCode "OpCode"
      , pMoveTargetNode
      , pMoveTargetNodeUuid
      , pIgnoreConsistency
-     ])
+     ],
+     "instance_name")
   , ("OpInstanceConsole",
+     [t| JSObject JSValue |],
+     OpDoc.opInstanceConsole,
      [ pInstanceName
      , pInstanceUuid
-     ])
+     ],
+     "instance_name")
   , ("OpInstanceActivateDisks",
+     [t| [(NonEmptyString, NonEmptyString, NonEmptyString)] |],
+     OpDoc.opInstanceActivateDisks,
      [ pInstanceName
      , pInstanceUuid
      , pIgnoreDiskSize
      , pWaitForSyncFalse
-     ])
+     ],
+     "instance_name")
   , ("OpInstanceDeactivateDisks",
+     [t| () |],
+     OpDoc.opInstanceDeactivateDisks,
      [ pInstanceName
      , pInstanceUuid
      , pForce
-     ])
+     ],
+     "instance_name")
   , ("OpInstanceRecreateDisks",
+     [t| () |],
+     OpDoc.opInstanceRecreateDisks,
      [ pInstanceName
      , pInstanceUuid
      , pRecreateDisksInfo
-     , pNodes
-     , pNodeUuids
+     , withDoc "New instance nodes, if relocation is desired" pNodes
+     , withDoc "New instance node UUIDs, if relocation is desired" pNodeUuids
      , pIallocator
-     ])
-  , ("OpInstanceQuery", dOldQuery)
+     ],
+     "instance_name")
+  , ("OpInstanceQuery",
+     [t| [[JSValue]] |],
+     OpDoc.opInstanceQuery,
+     [ pOutputFields
+     , pUseLocking
+     , withDoc
+       "Empty list to query all instances, instance names otherwise"
+       pNames
+     ],
+     [])
   , ("OpInstanceQueryData",
+     [t| JSObject (JSObject JSValue) |],
+     OpDoc.opInstanceQueryData,
      [ pUseLocking
      , pInstances
      , pStatic
-     ])
+     ],
+     [])
   , ("OpInstanceSetParams",
+      [t| [(NonEmptyString, JSValue)] |],
+      OpDoc.opInstanceSetParams,
      [ pInstanceName
      , pInstanceUuid
      , pForce
@@ -431,82 +648,133 @@ $(genOpCode "OpCode"
      , pOptDiskTemplate
      , pPrimaryNode
      , pPrimaryNodeUuid
-     , pRemoteNode
-     , pRemoteNodeUuid
+     , withDoc "Secondary node (used when changing disk template)" pRemoteNode
+     , withDoc
+       "Secondary node UUID (used when changing disk template)"
+       pRemoteNodeUuid
      , pOsNameChange
      , pInstOsParams
      , pWaitForSync
-     , pOffline
+     , withDoc "Whether to mark the instance as offline" pOffline
      , pIpConflictsCheck
-     ])
+     , pHotplug
+     ],
+     "instance_name")
   , ("OpInstanceGrowDisk",
+     [t| () |],
+     OpDoc.opInstanceGrowDisk,
      [ pInstanceName
      , pInstanceUuid
      , pWaitForSync
      , pDiskIndex
      , pDiskChgAmount
      , pDiskChgAbsolute
-     ])
+     ],
+     "instance_name")
   , ("OpInstanceChangeGroup",
+     [t| JobIdListOnly |],
+     OpDoc.opInstanceChangeGroup,
      [ pInstanceName
      , pInstanceUuid
      , pEarlyRelease
      , pIallocator
      , pTargetGroups
-     ])
+     ],
+     "instance_name")
   , ("OpGroupAdd",
+     [t| () |],
+     OpDoc.opGroupAdd,
      [ pGroupName
      , pNodeGroupAllocPolicy
      , pGroupNodeParams
      , pDiskParams
      , pHvState
      , pDiskState
-     , pIpolicy
-     ])
+     , withDoc "Group-wide ipolicy specs" pIpolicy
+     ],
+     "group_name")
   , ("OpGroupAssignNodes",
+     [t| () |],
+     OpDoc.opGroupAssignNodes,
      [ pGroupName
      , pForce
-     , pRequiredNodes
-     , pRequiredNodeUuids
-     ])
-  , ("OpGroupQuery", dOldQueryNoLocking)
+     , withDoc "List of nodes to assign" pRequiredNodes
+     , withDoc "List of node UUIDs to assign" pRequiredNodeUuids
+     ],
+     "group_name")
+  , ("OpGroupQuery",
+     [t| [[JSValue]] |],
+     OpDoc.opGroupQuery,
+     [ pOutputFields
+     , withDoc "Empty list to query all groups, group names otherwise" pNames
+     ],
+     [])
   , ("OpGroupSetParams",
+     [t| [(NonEmptyString, JSValue)] |],
+     OpDoc.opGroupSetParams,
      [ pGroupName
      , pNodeGroupAllocPolicy
      , pGroupNodeParams
      , pDiskParams
      , pHvState
      , pDiskState
-     , pIpolicy
-     ])
+     , withDoc "Group-wide ipolicy specs" pIpolicy
+     ],
+     "group_name")
   , ("OpGroupRemove",
-     [ pGroupName ])
+     [t| () |],
+     OpDoc.opGroupRemove,
+     [ pGroupName
+     ],
+     "group_name")
   , ("OpGroupRename",
+     [t| NonEmptyString |],
+     OpDoc.opGroupRename,
      [ pGroupName
-     , pNewName
-     ])
+     , withDoc "New group name" pNewName
+     ],
+     [])
   , ("OpGroupEvacuate",
+     [t| JobIdListOnly |],
+     OpDoc.opGroupEvacuate,
      [ pGroupName
      , pEarlyRelease
      , pIallocator
      , pTargetGroups
-     ])
+     ],
+     "group_name")
   , ("OpOsDiagnose",
+     [t| [[JSValue]] |],
+     OpDoc.opOsDiagnose,
      [ pOutputFields
-     , pNames ])
+     , withDoc "Which operating systems to diagnose" pNames
+     ],
+     [])
   , ("OpExtStorageDiagnose",
+     [t| [[JSValue]] |],
+     OpDoc.opExtStorageDiagnose,
      [ pOutputFields
-     , pNames ])
+     , withDoc "Which ExtStorage Provider to diagnose" pNames
+     ],
+     [])
   , ("OpBackupQuery",
+     [t| JSObject (Either Bool [NonEmptyString]) |],
+     OpDoc.opBackupQuery,
      [ pUseLocking
-     , pNodes
-     ])
+     , withDoc "Empty list to query all nodes, node names otherwise" pNodes
+     ],
+     [])
   , ("OpBackupPrepare",
+     [t| Maybe (JSObject JSValue) |],
+     OpDoc.opBackupPrepare,
      [ pInstanceName
      , pInstanceUuid
      , pExportMode
-     ])
+     ],
+     "instance_name")
   , ("OpBackupExport",
+     [t| (Bool, [Bool]) |],
+     OpDoc.opBackupExport,
      [ pInstanceName
      , pInstanceUuid
      , pShutdownTimeout
@@ -515,15 +783,61 @@ $(genOpCode "OpCode"
      , pShutdownInstance
      , pRemoveInstance
      , pIgnoreRemoveFailures
-     , pExportMode
+     , defaultField [| ExportModeLocal |] pExportMode
      , pX509KeyName
      , pX509DestCA
-     ])
+     ],
+     "instance_name")
   , ("OpBackupRemove",
+     [t| () |],
+     OpDoc.opBackupRemove,
      [ pInstanceName
      , pInstanceUuid
-     ])
+     ],
+     "instance_name")
+  , ("OpTagsGet",
+     [t| [NonEmptyString] |],
+     OpDoc.opTagsGet,
+     [ pTagsObject
+     , pUseLocking
+     , withDoc "Name of object to retrieve tags from" pTagsName
+     ],
+     "name")
+  , ("OpTagsSearch",
+     [t| [(NonEmptyString, NonEmptyString)] |],
+     OpDoc.opTagsSearch,
+     [ pTagSearchPattern
+     ],
+     "pattern")
+  , ("OpTagsSet",
+     [t| () |],
+     OpDoc.opTagsSet,
+     [ pTagsObject
+     , pTagsList
+     , withDoc "Name of object where tag(s) should be added" pTagsName
+     ],
+     [])
+  , ("OpTagsDel",
+     [t| () |],
+     OpDoc.opTagsDel,
+     [ pTagsObject
+     , pTagsList
+     , withDoc "Name of object where tag(s) should be deleted" pTagsName
+     ],
+     [])
+  , ("OpTestDelay",
+     [t| () |],
+     OpDoc.opTestDelay,
+     [ pDelayDuration
+     , pDelayOnMaster
+     , pDelayOnNodes
+     , pDelayOnNodeUuids
+     , pDelayRepeat
+     ],
+     "duration")
   , ("OpTestAllocator",
+     [t| String |],
+     OpDoc.opTestAllocator,
      [ pIAllocatorDirection
      , pIAllocatorMode
      , pIAllocatorReqName
@@ -541,20 +855,29 @@ $(genOpCode "OpCode"
      , pTargetGroups
      , pIAllocatorSpindleUse
      , pIAllocatorCount
-     ])
+     ],
+     "iallocator")
   , ("OpTestJqueue",
+     [t| Bool |],
+     OpDoc.opTestJqueue,
      [ pJQueueNotifyWaitLock
      , pJQueueNotifyExec
      , pJQueueLogMessages
      , pJQueueFail
-     ])
+     ],
+     [])
   , ("OpTestDummy",
+     [t| () |],
+     OpDoc.opTestDummy,
      [ pTestDummyResult
      , pTestDummyMessages
      , pTestDummyFail
      , pTestDummySubmitJobs
-     ])
+     ],
+     [])
   , ("OpNetworkAdd",
+     [t| () |],
+     OpDoc.opNetworkAdd,
      [ pNetworkName
      , pNetworkAddress4
      , pNetworkGateway4
@@ -563,39 +886,53 @@ $(genOpCode "OpCode"
      , pNetworkMacPrefix
      , pNetworkAddRsvdIps
      , pIpConflictsCheck
-     , pInstTags
-     ])
+     , withDoc "Network tags" pInstTags
+     ],
+     "network_name")
   , ("OpNetworkRemove",
+     [t| () |],
+     OpDoc.opNetworkRemove,
      [ pNetworkName
      , pForce
-     ])
+     ],
+     "network_name")
   , ("OpNetworkSetParams",
+     [t| () |],
+     OpDoc.opNetworkSetParams,
      [ pNetworkName
      , pNetworkGateway4
      , pNetworkAddress6
      , pNetworkGateway6
      , pNetworkMacPrefix
-     , pNetworkAddRsvdIps
+     , withDoc "Which external IP addresses to reserve" pNetworkAddRsvdIps
      , pNetworkRemoveRsvdIps
-     ])
+     ],
+     "network_name")
   , ("OpNetworkConnect",
+     [t| () |],
+     OpDoc.opNetworkConnect,
      [ pGroupName
      , pNetworkName
      , pNetworkMode
      , pNetworkLink
      , pIpConflictsCheck
-     ])
+     ],
+     "network_name")
   , ("OpNetworkDisconnect",
+     [t| () |],
+     OpDoc.opNetworkDisconnect,
      [ pGroupName
      , pNetworkName
-     ])
-  , ("OpNetworkQuery", dOldQuery)
-  , ("OpRestrictedCommand",
-     [ pUseLocking
-     , pRequiredNodes
-     , pRequiredNodeUuids
-     , pRestrictedCommand
-     ])
+     ],
+     "network_name")
+  , ("OpNetworkQuery",
+     [t| [[JSValue]] |],
+     OpDoc.opNetworkQuery,
+     [ pOutputFields
+     , pUseLocking
+     , withDoc "Empty list to query all groups, group names otherwise" pNames
+     ],
+     [])
   ])
 
 -- | Returns the OP_ID for a given opcode value.
@@ -650,8 +987,7 @@ opSummaryVal OpGroupEvacuate { opGroupName = s } = Just (fromNonEmpty s)
 opSummaryVal OpBackupPrepare { opInstanceName = s } = Just s
 opSummaryVal OpBackupExport { opInstanceName = s } = Just s
 opSummaryVal OpBackupRemove { opInstanceName = s } = Just s
-opSummaryVal OpTagsGet { opKind = k } =
-  Just . fromMaybe "None" $ tagNameOf k
+opSummaryVal OpTagsGet { opKind = s } = Just (show s)
 opSummaryVal OpTagsSearch { opTagSearchPattern = s } = Just (fromNonEmpty s)
 opSummaryVal OpTestDelay { opDelayDuration = d } = Just (show d)
 opSummaryVal OpTestAllocator { opIallocator = s } =
index c4ef7f4..beef2d8 100644 (file)
@@ -32,13 +32,7 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 -}
 
 module Ganeti.OpParams
-  ( TagType(..)
-  , TagObject(..)
-  , tagObjectFrom
-  , tagNameOf
-  , decodeTagObject
-  , encodeTagObject
-  , ReplaceDisksMode(..)
+  ( ReplaceDisksMode(..)
   , DiskIndex
   , mkDiskIndex
   , unDiskIndex
@@ -55,6 +49,7 @@ module Ganeti.OpParams
   , pName
   , pTagsList
   , pTagsObject
+  , pTagsName
   , pOutputFields
   , pShutdownTimeout
   , pShutdownTimeout'
@@ -99,14 +94,15 @@ module Ganeti.OpParams
   , pHvState
   , pDiskState
   , pIgnoreIpolicy
+  , pHotplug
   , pAllowRuntimeChgs
   , pInstDisks
   , pDiskTemplate
   , pOptDiskTemplate
   , pFileDriver
   , pFileStorageDir
-  , pGlobalFileStorageDir
-  , pGlobalSharedFileStorageDir
+  , pClusterFileStorageDir
+  , pClusterSharedFileStorageDir
   , pVgName
   , pEnabledHypervisors
   , pHypervisor
@@ -139,6 +135,7 @@ module Ganeti.OpParams
   , pUseExternalMipScript
   , pQueryFields
   , pQueryFilter
+  , pQueryFieldsFields
   , pOobCommand
   , pOobTimeout
   , pIgnoreStatus
@@ -154,6 +151,7 @@ module Ganeti.OpParams
   , pRequiredNodes
   , pRequiredNodeUuids
   , pStorageType
+  , pStorageTypeOptional
   , pStorageChanges
   , pMasterCandidate
   , pOffline
@@ -253,14 +251,11 @@ module Ganeti.OpParams
   , pComment
   , pReason
   , pEnabledDiskTemplates
-  , dOldQuery
-  , dOldQueryNoLocking
   ) where
 
 import Control.Monad (liftM)
-import qualified Data.Set as Set
-import Text.JSON (readJSON, showJSON, JSON, JSValue(..), fromJSString,
-                  JSObject, toJSObject)
+import Text.JSON (JSON, JSValue(..), JSObject (..), readJSON, showJSON,
+                  fromJSString, toJSObject)
 import qualified Text.JSON
 import Text.JSON.Pretty (pp_value)
 
@@ -273,8 +268,6 @@ import qualified Ganeti.Query.Language as Qlang
 
 -- * Helper functions and types
 
--- * Type aliases
-
 -- | Build a boolean field.
 booleanField :: String -> Field
 booleanField = flip simpleField [t| Bool |]
@@ -299,15 +292,6 @@ optionalStringField = optionalField . stringField
 optionalNEStringField :: String -> Field
 optionalNEStringField = optionalField . flip simpleField [t| NonEmptyString |]
 
--- | Unchecked value, should be replaced by a better definition.
-type UncheckedValue = JSValue
-
--- | Unchecked dict, should be replaced by a better definition.
-type UncheckedDict = JSObject JSValue
-
--- | Unchecked list, shoild be replaced by a better definition.
-type UncheckedList = [JSValue]
-
 -- | Function to force a non-negative value, without returning via a
 -- monad. This is needed for, and should be used /only/ in the case of
 -- forcing constants. In case the constant is wrong (< 0), this will
@@ -317,76 +301,8 @@ forceNonNeg i = case mkNonNegative i of
                   Ok n -> n
                   Bad msg -> error msg
 
--- ** Tags
-
--- | Data type representing what items do the tag operations apply to.
-$(declareSADT "TagType"
-  [ ("TagTypeInstance", 'C.tagInstance)
-  , ("TagTypeNode",     'C.tagNode)
-  , ("TagTypeGroup",    'C.tagNodegroup)
-  , ("TagTypeCluster",  'C.tagCluster)
-  ])
-$(makeJSONInstance ''TagType)
-
--- | Data type holding a tag object (type and object name).
-data TagObject = TagInstance String
-               | TagNode     String
-               | TagGroup    String
-               | TagCluster
-               deriving (Show, Eq)
-
--- | Tag type for a given tag object.
-tagTypeOf :: TagObject -> TagType
-tagTypeOf (TagInstance {}) = TagTypeInstance
-tagTypeOf (TagNode     {}) = TagTypeNode
-tagTypeOf (TagGroup    {}) = TagTypeGroup
-tagTypeOf (TagCluster  {}) = TagTypeCluster
-
--- | Gets the potential tag object name.
-tagNameOf :: TagObject -> Maybe String
-tagNameOf (TagInstance s) = Just s
-tagNameOf (TagNode     s) = Just s
-tagNameOf (TagGroup    s) = Just s
-tagNameOf  TagCluster     = Nothing
-
--- | Builds a 'TagObject' from a tag type and name.
-tagObjectFrom :: (Monad m) => TagType -> JSValue -> m TagObject
-tagObjectFrom TagTypeInstance (JSString s) =
-  return . TagInstance $ fromJSString s
-tagObjectFrom TagTypeNode     (JSString s) = return . TagNode $ fromJSString s
-tagObjectFrom TagTypeGroup    (JSString s) = return . TagGroup $ fromJSString s
-tagObjectFrom TagTypeCluster   JSNull      = return TagCluster
-tagObjectFrom t v =
-  fail $ "Invalid tag type/name combination: " ++ show t ++ "/" ++
-         show (pp_value v)
-
--- | Name of the tag \"name\" field.
-tagNameField :: String
-tagNameField = "name"
-
--- | Custom encoder for 'TagObject' as represented in an opcode.
-encodeTagObject :: TagObject -> (JSValue, [(String, JSValue)])
-encodeTagObject t = ( showJSON (tagTypeOf t)
-                    , [(tagNameField, maybe JSNull showJSON (tagNameOf t))] )
-
--- | Custom decoder for 'TagObject' as represented in an opcode.
-decodeTagObject :: (Monad m) => [(String, JSValue)] -> JSValue -> m TagObject
-decodeTagObject obj kind = do
-  ttype <- fromJVal kind
-  tname <- fromObj obj tagNameField
-  tagObjectFrom ttype tname
-
 -- ** Disks
 
--- | Replace disks type.
-$(declareSADT "ReplaceDisksMode"
-  [ ("ReplaceOnPrimary",    'C.replaceDiskPri)
-  , ("ReplaceOnSecondary",  'C.replaceDiskSec)
-  , ("ReplaceNewSecondary", 'C.replaceDiskChg)
-  , ("ReplaceAuto",         'C.replaceDiskAuto)
-  ])
-$(makeJSONInstance ''ReplaceDisksMode)
-
 -- | Disk index type (embedding constraints on the index value via a
 -- smart constructor).
 newtype DiskIndex = DiskIndex { unDiskIndex :: Int }
@@ -414,11 +330,13 @@ $(makeJSONInstance ''DiskAccess)
 
 -- | NIC modification definition.
 $(buildObject "INicParams" "inic"
-  [ optionalField $ simpleField C.inicMac  [t| NonEmptyString |]
-  , optionalField $ simpleField C.inicIp   [t| String         |]
-  , optionalField $ simpleField C.inicMode [t| NonEmptyString |]
-  , optionalField $ simpleField C.inicLink [t| NonEmptyString |]
-  , optionalField $ simpleField C.inicName [t| NonEmptyString |]
+  [ optionalField $ simpleField C.inicMac    [t| NonEmptyString |]
+  , optionalField $ simpleField C.inicIp     [t| String         |]
+  , optionalField $ simpleField C.inicMode   [t| NonEmptyString |]
+  , optionalField $ simpleField C.inicLink   [t| NonEmptyString |]
+  , optionalField $ simpleField C.inicName   [t| NonEmptyString |]
+  , optionalField $ simpleField C.inicVlan   [t| NonEmptyString |]
+  , optionalField $ simpleField C.inicBridge [t| NonEmptyString |]
   ])
 
 -- | Disk modification definition. FIXME: disksize should be VTYPE_UNIT.
@@ -500,11 +418,11 @@ instance (JSON a) => JSON (SetParamsMods a) where
   readJSON = readSetParams
 
 -- | Custom type for target_node parameter of OpBackupExport, which
--- varies depending on mode. FIXME: this uses an UncheckedList since
+-- varies depending on mode. FIXME: this uses an [JSValue] since
 -- we don't care about individual rows (just like the Python code
 -- tests). But the proper type could be parsed if we wanted.
 data ExportTarget = ExportTargetLocal NonEmptyString
-                  | ExportTargetRemote UncheckedList
+                  | ExportTargetRemote [JSValue]
                     deriving (Eq, Show)
 
 -- | Custom reader for 'ExportTarget'.
@@ -520,396 +438,199 @@ instance JSON ExportTarget where
   showJSON (ExportTargetRemote l) = showJSON l
   readJSON = readExportTarget
 
--- * Parameters
-
--- | A required instance name (for single-instance LUs).
-pInstanceName :: Field
-pInstanceName = simpleField "instance_name" [t| String |]
-
--- | An instance UUID (for single-instance LUs).
-pInstanceUuid :: Field
-pInstanceUuid = optionalField $ simpleField "instance_uuid" [t| String |]
-
--- | A list of instances.
-pInstances :: Field
-pInstances = defaultField [| [] |] $
-             simpleField "instances" [t| [NonEmptyString] |]
-
--- | A generic name.
-pName :: Field
-pName = simpleField "name" [t| NonEmptyString |]
-
--- | Tags list.
-pTagsList :: Field
-pTagsList = simpleField "tags" [t| [String] |]
-
--- | Tags object.
-pTagsObject :: Field
-pTagsObject =
-  customField 'decodeTagObject 'encodeTagObject [tagNameField] $
-  simpleField "kind" [t| TagObject |]
-
--- | Selected output fields.
-pOutputFields :: Field
-pOutputFields = simpleField "output_fields" [t| [NonEmptyString] |]
-
--- | How long to wait for instance to shut down.
-pShutdownTimeout :: Field
-pShutdownTimeout = defaultField [| forceNonNeg C.defaultShutdownTimeout |] $
-                   simpleField "shutdown_timeout" [t| NonNegative Int |]
-
--- | Another name for the shutdown timeout, because we like to be
--- inconsistent.
-pShutdownTimeout' :: Field
-pShutdownTimeout' =
-  renameField "InstShutdownTimeout" .
-  defaultField [| forceNonNeg C.defaultShutdownTimeout |] $
-  simpleField "timeout" [t| NonNegative Int |]
-
--- | Whether to shutdown the instance in backup-export.
-pShutdownInstance :: Field
-pShutdownInstance = defaultTrue "shutdown"
-
--- | Whether to force the operation.
-pForce :: Field
-pForce = defaultFalse "force"
-
--- | Whether to ignore offline nodes.
-pIgnoreOfflineNodes :: Field
-pIgnoreOfflineNodes = defaultFalse "ignore_offline_nodes"
-
--- | A required node name (for single-node LUs).
-pNodeName :: Field
-pNodeName = simpleField "node_name" [t| NonEmptyString |]
-
--- | A node UUID (for single-node LUs).
-pNodeUuid :: Field
-pNodeUuid = optionalField $ simpleField "node_uuid" [t| NonEmptyString |]
-
--- | List of nodes.
-pNodeNames :: Field
-pNodeNames =
-  defaultField [| [] |] $ simpleField "node_names" [t| [NonEmptyString] |]
-
--- | List of node UUIDs.
-pNodeUuids :: Field
-pNodeUuids =
-  optionalField $ simpleField "node_uuids" [t| [NonEmptyString] |]
-
--- | A required node group name (for single-group LUs).
-pGroupName :: Field
-pGroupName = simpleField "group_name" [t| NonEmptyString |]
-
--- | Migration type (live\/non-live).
-pMigrationMode :: Field
-pMigrationMode =
-  renameField "MigrationMode" .
-  optionalField $
-  simpleField "mode" [t| MigrationMode |]
-
--- | Obsolete \'live\' migration mode (boolean).
-pMigrationLive :: Field
-pMigrationLive =
-  renameField "OldLiveMode" . optionalField $ booleanField "live"
-
--- | Migration cleanup parameter.
-pMigrationCleanup :: Field
-pMigrationCleanup = renameField "MigrationCleanup" $ defaultFalse "cleanup"
-
--- | Whether to force an unknown OS variant.
-pForceVariant :: Field
-pForceVariant = defaultFalse "force_variant"
-
--- | Whether to wait for the disk to synchronize.
-pWaitForSync :: Field
-pWaitForSync = defaultTrue "wait_for_sync"
-
--- | Whether to wait for the disk to synchronize (defaults to false).
-pWaitForSyncFalse :: Field
-pWaitForSyncFalse = defaultField [| False |] pWaitForSync
-
--- | Whether to ignore disk consistency
-pIgnoreConsistency :: Field
-pIgnoreConsistency = defaultFalse "ignore_consistency"
-
--- | Storage name.
-pStorageName :: Field
-pStorageName =
-  renameField "StorageName" $ simpleField "name" [t| NonEmptyString |]
-
--- | Whether to use synchronization.
-pUseLocking :: Field
-pUseLocking = defaultFalse "use_locking"
-
--- | Whether to employ opportunistic locking for nodes, meaning nodes already
--- locked by another opcode won't be considered for instance allocation (only
--- when an iallocator is used).
-pOpportunisticLocking :: Field
-pOpportunisticLocking = defaultFalse "opportunistic_locking"
-
--- | Whether to check name.
-pNameCheck :: Field
-pNameCheck = defaultTrue "name_check"
-
--- | Instance allocation policy.
-pNodeGroupAllocPolicy :: Field
-pNodeGroupAllocPolicy = optionalField $
-                        simpleField "alloc_policy" [t| AllocPolicy |]
-
--- | Default node parameters for group.
-pGroupNodeParams :: Field
-pGroupNodeParams = optionalField $ simpleField "ndparams" [t| UncheckedDict |]
-
--- | Resource(s) to query for.
-pQueryWhat :: Field
-pQueryWhat = simpleField "what" [t| Qlang.QueryTypeOp |]
-
--- | Whether to release locks as soon as possible.
-pEarlyRelease :: Field
-pEarlyRelease = defaultFalse "early_release"
-
--- | Whether to ensure instance's IP address is inactive.
-pIpCheck :: Field
-pIpCheck = defaultTrue "ip_check"
-
--- | Check for conflicting IPs.
-pIpConflictsCheck :: Field
-pIpConflictsCheck = defaultTrue "conflicts_check"
-
--- | Do not remember instance state changes.
-pNoRemember :: Field
-pNoRemember = defaultFalse "no_remember"
+-- * Common opcode parameters
 
--- | Target node for instance migration/failover.
-pMigrationTargetNode :: Field
-pMigrationTargetNode = optionalNEStringField "target_node"
+pDryRun :: Field
+pDryRun =
+  withDoc "Run checks only, don't execute" .
+  optionalField $ booleanField "dry_run"
 
--- | Target node UUID for instance migration/failover.
-pMigrationTargetNodeUuid :: Field
-pMigrationTargetNodeUuid = optionalNEStringField "target_node_uuid"
+pDebugLevel :: Field
+pDebugLevel =
+  withDoc "Debug level" .
+  optionalField $ simpleField "debug_level" [t| NonNegative Int |]
 
--- | Target node for instance move (required).
-pMoveTargetNode :: Field
-pMoveTargetNode =
-  renameField "MoveTargetNode" $
-  simpleField "target_node" [t| NonEmptyString |]
+pOpPriority :: Field
+pOpPriority =
+  withDoc "Opcode priority. Note: python uses a separate constant,\
+          \ we're using the actual value we know it's the default" .
+  defaultField [| OpPrioNormal |] $
+  simpleField "priority" [t| OpSubmitPriority |]
 
--- | Target node UUID for instance move.
-pMoveTargetNodeUuid :: Field
-pMoveTargetNodeUuid =
-  renameField "MoveTargetNodeUuid" . optionalField $
-  simpleField "target_node_uuid" [t| NonEmptyString |]
+pDependencies :: Field
+pDependencies =
+  withDoc "Job dependencies" .
+  optionalNullSerField $ simpleField "depends" [t| [JobDependency] |]
 
--- | Pause instance at startup.
-pStartupPaused :: Field
-pStartupPaused = defaultFalse "startup_paused"
+pComment :: Field
+pComment =
+  withDoc "Comment field" .
+  optionalNullSerField $ stringField "comment"
 
--- | Verbose mode.
-pVerbose :: Field
-pVerbose = defaultFalse "verbose"
+pReason :: Field
+pReason =
+  withDoc "Reason trail field" $
+  simpleField C.opcodeReason [t| ReasonTrail |]
 
--- ** Parameters for cluster verification
+-- * Parameters
 
--- | Whether to simulate errors (useful for debugging).
 pDebugSimulateErrors :: Field
-pDebugSimulateErrors = defaultFalse "debug_simulate_errors"
+pDebugSimulateErrors =
+  withDoc "Whether to simulate errors (useful for debugging)" $
+  defaultFalse "debug_simulate_errors"
 
--- | Error codes.
 pErrorCodes :: Field
-pErrorCodes = defaultFalse "error_codes"
+pErrorCodes = 
+  withDoc "Error codes" $
+  defaultFalse "error_codes"
 
--- | Which checks to skip.
 pSkipChecks :: Field
-pSkipChecks = defaultField [| Set.empty |] $
-              simpleField "skip_checks" [t| Set.Set VerifyOptionalChecks |]
+pSkipChecks = 
+  withDoc "Which checks to skip" .
+  defaultField [| emptyListSet |] $
+  simpleField "skip_checks" [t| ListSet VerifyOptionalChecks |]
 
--- | List of error codes that should be treated as warnings.
 pIgnoreErrors :: Field
-pIgnoreErrors = defaultField [| Set.empty |] $
-                simpleField "ignore_errors" [t| Set.Set CVErrorCode |]
-
--- | Optional group name.
-pOptGroupName :: Field
-pOptGroupName = renameField "OptGroupName" .
-                optionalField $ simpleField "group_name" [t| NonEmptyString |]
-
--- | Disk templates' parameter defaults.
-pDiskParams :: Field
-pDiskParams = optionalField $
-              simpleField "diskparams" [t| GenericContainer DiskTemplate
-                                           UncheckedDict |]
-
--- * Parameters for node resource model
-
--- | Set hypervisor states.
-pHvState :: Field
-pHvState = optionalField $ simpleField "hv_state" [t| UncheckedDict |]
-
--- | Set disk states.
-pDiskState :: Field
-pDiskState = optionalField $ simpleField "disk_state" [t| UncheckedDict |]
-
--- | Whether to ignore ipolicy violations.
-pIgnoreIpolicy :: Field
-pIgnoreIpolicy = defaultFalse "ignore_ipolicy"
-
--- | Allow runtime changes while migrating.
-pAllowRuntimeChgs :: Field
-pAllowRuntimeChgs = defaultTrue "allow_runtime_changes"
+pIgnoreErrors =
+  withDoc "List of error codes that should be treated as warnings" .
+  defaultField [| emptyListSet |] $
+  simpleField "ignore_errors" [t| ListSet CVErrorCode |]
 
--- | Utility type for OpClusterSetParams.
-type TestClusterOsListItem = (DdmSimple, NonEmptyString)
+pVerbose :: Field
+pVerbose =
+  withDoc "Verbose mode" $
+  defaultFalse "verbose"
 
--- | Utility type of OsList.
-type TestClusterOsList = [TestClusterOsListItem]
+pOptGroupName :: Field
+pOptGroupName =
+  withDoc "Optional group name" .
+  renameField "OptGroupName" .
+  optionalField $ simpleField "group_name" [t| NonEmptyString |]
 
--- Utility type for NIC definitions.
---type TestNicDef = INicParams
+pGroupName :: Field
+pGroupName =
+  withDoc "Group name" $
+  simpleField "group_name" [t| NonEmptyString |]
 
--- | List of instance disks.
-pInstDisks :: Field
-pInstDisks = renameField "instDisks" $ simpleField "disks" [t| [IDiskParams] |]
+-- | Whether to hotplug device.
+pHotplug :: Field
+pHotplug = defaultFalse "hotplug"
 
--- | Instance disk template.
-pDiskTemplate :: Field
-pDiskTemplate = simpleField "disk_template" [t| DiskTemplate |]
+pInstances :: Field
+pInstances =
+  withDoc "List of instances" .
+  defaultField [| [] |] $
+  simpleField "instances" [t| [NonEmptyString] |]
 
--- | Instance disk template.
-pOptDiskTemplate :: Field
-pOptDiskTemplate =
-  optionalField .
-  renameField "OptDiskTemplate" $
-  simpleField "disk_template" [t| DiskTemplate |]
+pOutputFields :: Field
+pOutputFields =
+  withDoc "Selected output fields" $
+  simpleField "output_fields" [t| [NonEmptyString] |]
 
--- | File driver.
-pFileDriver :: Field
-pFileDriver = optionalField $ simpleField "file_driver" [t| FileDriver |]
+pName :: Field
+pName =
+  withDoc "A generic name" $
+  simpleField "name" [t| NonEmptyString |]
 
--- | Directory for storing file-backed disks.
-pFileStorageDir :: Field
-pFileStorageDir = optionalNEStringField "file_storage_dir"
+pForce :: Field
+pForce =
+  withDoc "Whether to force the operation" $
+  defaultFalse "force"
 
--- | Global directory for storing file-backed disks.
-pGlobalFileStorageDir :: Field
-pGlobalFileStorageDir = optionalNEStringField "file_storage_dir"
+pHvState :: Field
+pHvState =
+  withDoc "Set hypervisor states" .
+  optionalField $ simpleField "hv_state" [t| JSObject JSValue |]
 
--- | Global directory for storing shared-file-backed disks.
-pGlobalSharedFileStorageDir :: Field
-pGlobalSharedFileStorageDir = optionalNEStringField "shared_file_storage_dir"
+pDiskState :: Field
+pDiskState =
+  withDoc "Set disk states" .
+  optionalField $ simpleField "disk_state" [t| JSObject JSValue |]
+
+-- | Cluster-wide default directory for storing file-backed disks.
+pClusterFileStorageDir :: Field
+pClusterFileStorageDir =
+  renameField "ClusterFileStorageDir" $
+  optionalStringField "file_storage_dir"
+
+-- | Cluster-wide default directory for storing shared-file-backed disks.
+pClusterSharedFileStorageDir :: Field
+pClusterSharedFileStorageDir =
+  renameField "ClusterSharedFileStorageDir" $
+  optionalStringField "shared_file_storage_dir"
 
 -- | Volume group name.
 pVgName :: Field
-pVgName = optionalStringField "vg_name"
+pVgName =
+  withDoc "Volume group name" $
+  optionalStringField "vg_name"
 
--- | List of enabled hypervisors.
 pEnabledHypervisors :: Field
 pEnabledHypervisors =
+  withDoc "List of enabled hypervisors" .
   optionalField $
-  simpleField "enabled_hypervisors" [t| NonEmpty Hypervisor |]
+  simpleField "enabled_hypervisors" [t| [Hypervisor] |]
 
--- | List of enabled disk templates.
-pEnabledDiskTemplates :: Field
-pEnabledDiskTemplates =
-  optionalField $
-  simpleField "enabled_disk_templates" [t| NonEmpty DiskTemplate |]
-
--- | Selected hypervisor for an instance.
-pHypervisor :: Field
-pHypervisor =
-  optionalField $
-  simpleField "hypervisor" [t| Hypervisor |]
-
--- | Cluster-wide hypervisor parameters, hypervisor-dependent.
 pClusterHvParams :: Field
 pClusterHvParams =
+  withDoc "Cluster-wide hypervisor parameters, hypervisor-dependent" .
   renameField "ClusterHvParams" .
   optionalField $
-  simpleField "hvparams" [t| Container UncheckedDict |]
-
--- | Instance hypervisor parameters.
-pInstHvParams :: Field
-pInstHvParams =
-  renameField "InstHvParams" .
-  defaultField [| toJSObject [] |] $
-  simpleField "hvparams" [t| UncheckedDict |]
+  simpleField "hvparams" [t| GenericContainer String (JSObject JSValue) |]
 
--- | Cluster-wide beparams.
 pClusterBeParams :: Field
 pClusterBeParams =
+  withDoc "Cluster-wide backend parameter defaults" .
   renameField "ClusterBeParams" .
-  optionalField $ simpleField "beparams" [t| UncheckedDict |]
+  optionalField $ simpleField "beparams" [t| JSObject JSValue |]
 
--- | Instance beparams.
-pInstBeParams :: Field
-pInstBeParams =
-  renameField "InstBeParams" .
-  defaultField [| toJSObject [] |] $
-  simpleField "beparams" [t| UncheckedDict |]
-
--- | Reset instance parameters to default if equal.
-pResetDefaults :: Field
-pResetDefaults = defaultFalse "identify_defaults"
-
--- | Cluster-wide per-OS hypervisor parameter defaults.
 pOsHvp :: Field
-pOsHvp = optionalField $ simpleField "os_hvp" [t| Container UncheckedDict |]
+pOsHvp =
+  withDoc "Cluster-wide per-OS hypervisor parameter defaults" .
+  optionalField $
+  simpleField "os_hvp" [t| GenericContainer String (JSObject JSValue) |]
 
--- | Cluster-wide OS parameter defaults.
 pClusterOsParams :: Field
 pClusterOsParams =
+  withDoc "Cluster-wide OS parameter defaults" .
   renameField "ClusterOsParams" .
-  optionalField $ simpleField "osparams" [t| Container UncheckedDict |]
-
--- | Instance OS parameters.
-pInstOsParams :: Field
-pInstOsParams =
-  renameField "InstOsParams" . defaultField [| toJSObject [] |] $
-  simpleField "osparams" [t| UncheckedDict |]
-
--- | Temporary OS parameters (currently only in reinstall, might be
--- added to install as well).
-pTempOsParams :: Field
-pTempOsParams =
-  renameField "TempOsParams" .
-  optionalField $ simpleField "osparams" [t| UncheckedDict |]
-
--- | Temporary hypervisor parameters, hypervisor-dependent.
-pTempHvParams :: Field
-pTempHvParams =
-  renameField "TempHvParams" .
-  defaultField [| toJSObject [] |] $
-  simpleField "hvparams" [t| UncheckedDict |]
+  optionalField $
+  simpleField "osparams" [t| GenericContainer String (JSObject JSValue) |]
 
--- | Temporary backend parameters.
-pTempBeParams :: Field
-pTempBeParams =
-  renameField "TempBeParams" .
-  defaultField [| toJSObject [] |] $
-  simpleField "beparams" [t| UncheckedDict |]
+pDiskParams :: Field
+pDiskParams =
+  withDoc "Disk templates' parameter defaults" .
+  optionalField $
+  simpleField "diskparams"
+              [t| GenericContainer DiskTemplate (JSObject JSValue) |]
 
--- | Candidate pool size.
 pCandidatePoolSize :: Field
 pCandidatePoolSize =
+  withDoc "Master candidate pool size" .
   optionalField $ simpleField "candidate_pool_size" [t| Positive Int |]
 
--- | Set UID pool, must be list of lists describing UID ranges (two
--- items, start and end inclusive.
 pUidPool :: Field
-pUidPool = optionalField $ simpleField "uid_pool" [t| [[(Int, Int)]] |]
+pUidPool =
+  withDoc "Set UID pool, must be list of lists describing UID ranges\
+          \ (two items, start and end inclusive)" .
+  optionalField $ simpleField "uid_pool" [t| [(Int, Int)] |]
 
--- | Extend UID pool, must be list of lists describing UID ranges (two
--- items, start and end inclusive.
 pAddUids :: Field
-pAddUids = optionalField $ simpleField "add_uids" [t| [[(Int, Int)]] |]
+pAddUids =
+  withDoc "Extend UID pool, must be list of lists describing UID\
+          \ ranges (two items, start and end inclusive)" .
+  optionalField $ simpleField "add_uids" [t| [(Int, Int)] |]
 
--- | Shrink UID pool, must be list of lists describing UID ranges (two
--- items, start and end inclusive) to be removed.
 pRemoveUids :: Field
-pRemoveUids = optionalField $ simpleField "remove_uids" [t| [[(Int, Int)]] |]
+pRemoveUids =
+  withDoc "Shrink UID pool, must be list of lists describing UID\
+          \ ranges (two items, start and end inclusive) to be removed" .
+  optionalField $ simpleField "remove_uids" [t| [(Int, Int)] |]
 
--- | Whether to automatically maintain node health.
 pMaintainNodeHealth :: Field
-pMaintainNodeHealth = optionalField $ booleanField "maintain_node_health"
+pMaintainNodeHealth =
+  withDoc "Whether to automatically maintain node health" .
+  optionalField $ booleanField "maintain_node_health"
 
 -- | Whether to modify and keep in sync the @/etc/hosts@ files of nodes.
 pModifyEtcHosts :: Field
@@ -917,635 +638,972 @@ pModifyEtcHosts = optionalField $ booleanField "modify_etc_hosts"
 
 -- | Whether to wipe disks before allocating them to instances.
 pPreallocWipeDisks :: Field
-pPreallocWipeDisks = optionalField $ booleanField "prealloc_wipe_disks"
+pPreallocWipeDisks =
+  withDoc "Whether to wipe disks before allocating them to instances" .
+  optionalField $ booleanField "prealloc_wipe_disks"
 
--- | Cluster-wide NIC parameter defaults.
 pNicParams :: Field
-pNicParams = optionalField $ simpleField "nicparams" [t| INicParams |]
-
--- | Instance NIC definitions.
-pInstNics :: Field
-pInstNics = simpleField "nics" [t| [INicParams] |]
+pNicParams =
+  withDoc "Cluster-wide NIC parameter defaults" .
+  optionalField $ simpleField "nicparams" [t| INicParams |]
 
--- | Cluster-wide node parameter defaults.
-pNdParams :: Field
-pNdParams = optionalField $ simpleField "ndparams" [t| UncheckedDict |]
-
--- | Cluster-wide ipolicy specs.
 pIpolicy :: Field
-pIpolicy = optionalField $ simpleField "ipolicy" [t| UncheckedDict |]
+pIpolicy =
+  withDoc "Ipolicy specs" .
+  optionalField $ simpleField "ipolicy" [t| JSObject JSValue |]
 
--- | DRBD helper program.
 pDrbdHelper :: Field
-pDrbdHelper = optionalStringField "drbd_helper"
+pDrbdHelper =
+  withDoc "DRBD helper program" $
+  optionalStringField "drbd_helper"
 
--- | Default iallocator for cluster.
 pDefaultIAllocator :: Field
-pDefaultIAllocator = optionalStringField "default_iallocator"
+pDefaultIAllocator =
+  withDoc "Default iallocator for cluster" $
+  optionalStringField "default_iallocator"
 
--- | Master network device.
 pMasterNetdev :: Field
-pMasterNetdev = optionalStringField "master_netdev"
+pMasterNetdev =
+  withDoc "Master network device" $
+  optionalStringField "master_netdev"
 
--- | Netmask of the master IP.
 pMasterNetmask :: Field
 pMasterNetmask =
+  withDoc "Netmask of the master IP" .
   optionalField $ simpleField "master_netmask" [t| NonNegative Int |]
 
--- | List of reserved LVs.
 pReservedLvs :: Field
 pReservedLvs =
+  withDoc "List of reserved LVs" .
   optionalField $ simpleField "reserved_lvs" [t| [NonEmptyString] |]
 
--- | Modify list of hidden operating systems: each modification must
--- have two items, the operation and the OS name; the operation can be
--- add or remove.
 pHiddenOs :: Field
-pHiddenOs = optionalField $ simpleField "hidden_os" [t| TestClusterOsList |]
+pHiddenOs =
+  withDoc "Modify list of hidden operating systems: each modification\
+          \ must have two items, the operation and the OS name; the operation\
+          \ can be add or remove" .
+  optionalField $ simpleField "hidden_os" [t| [(DdmSimple, NonEmptyString)] |]
 
--- | Modify list of blacklisted operating systems: each modification
--- must have two items, the operation and the OS name; the operation
--- can be add or remove.
 pBlacklistedOs :: Field
 pBlacklistedOs =
-  optionalField $ simpleField "blacklisted_os" [t| TestClusterOsList |]
+  withDoc "Modify list of blacklisted operating systems: each\
+          \ modification must have two items, the operation and the OS name;\
+          \ the operation can be add or remove" .
+  optionalField $
+  simpleField "blacklisted_os" [t| [(DdmSimple, NonEmptyString)] |]
 
--- | Whether to use an external master IP address setup script.
 pUseExternalMipScript :: Field
-pUseExternalMipScript = optionalField $ booleanField "use_external_mip_script"
+pUseExternalMipScript =
+  withDoc "Whether to use an external master IP address setup script" .
+  optionalField $ booleanField "use_external_mip_script"
+
+pEnabledDiskTemplates :: Field
+pEnabledDiskTemplates =
+  withDoc "List of enabled disk templates" .
+  optionalField $
+  simpleField "enabled_disk_templates" [t| [DiskTemplate] |]
+
+pQueryWhat :: Field
+pQueryWhat =
+  withDoc "Resource(s) to query for" $
+  simpleField "what" [t| Qlang.QueryTypeOp |]
+
+pUseLocking :: Field
+pUseLocking =
+  withDoc "Whether to use synchronization" $
+  defaultFalse "use_locking"
 
--- | Requested fields.
 pQueryFields :: Field
-pQueryFields = simpleField "fields" [t| [NonEmptyString] |]
+pQueryFields =
+  withDoc "Requested fields" $
+  simpleField "fields" [t| [NonEmptyString] |]
 
--- | Query filter.
 pQueryFilter :: Field
-pQueryFilter = simpleField "qfilter" [t| Qlang.Filter String |]
+pQueryFilter =
+  withDoc "Query filter" .
+  optionalField $ simpleField "qfilter" [t| [JSValue] |]
+
+pQueryFieldsFields :: Field
+pQueryFieldsFields =
+  withDoc "Requested fields; if not given, all are returned" .
+  renameField "QueryFieldsFields" $
+  optionalField pQueryFields
+
+pNodeNames :: Field
+pNodeNames =
+  withDoc "List of node names to run the OOB command against" .
+  defaultField [| [] |] $ simpleField "node_names" [t| [NonEmptyString] |]
+
+pNodeUuids :: Field
+pNodeUuids =
+  withDoc "List of node UUIDs" .
+  optionalField $ simpleField "node_uuids" [t| [NonEmptyString] |]
 
--- | OOB command to run.
 pOobCommand :: Field
-pOobCommand = simpleField "command" [t| OobCommand |]
+pOobCommand =
+  withDoc "OOB command to run" $
+  simpleField "command" [t| OobCommand |]
 
--- | Timeout before the OOB helper will be terminated.
 pOobTimeout :: Field
 pOobTimeout =
-  defaultField [| C.oobTimeout |] $ simpleField "timeout" [t| Int |]
+  withDoc "Timeout before the OOB helper will be terminated" .
+  defaultField [| C.oobTimeout |] $
+  simpleField "timeout" [t| Int |]
 
--- | Ignores the node offline status for power off.
 pIgnoreStatus :: Field
-pIgnoreStatus = defaultFalse "ignore_status"
+pIgnoreStatus =
+  withDoc "Ignores the node offline status for power off" $
+  defaultFalse "ignore_status"
 
--- | Time in seconds to wait between powering on nodes.
 pPowerDelay :: Field
 pPowerDelay =
   -- FIXME: we can't use the proper type "NonNegative Double", since
   -- the default constant is a plain Double, not a non-negative one.
+  -- And trying to fix the constant introduces a cyclic import.
+  withDoc "Time in seconds to wait between powering on nodes" .
   defaultField [| C.oobPowerDelay |] $
   simpleField "power_delay" [t| Double |]
 
--- | Primary IP address.
+pRequiredNodes :: Field
+pRequiredNodes =
+  withDoc "Required list of node names" .
+  renameField "ReqNodes " $ simpleField "nodes" [t| [NonEmptyString] |]
+
+pRequiredNodeUuids :: Field
+pRequiredNodeUuids =
+  withDoc "Required list of node UUIDs" .
+  renameField "ReqNodeUuids " . optionalField $
+  simpleField "node_uuids" [t| [NonEmptyString] |]
+
+pRestrictedCommand :: Field
+pRestrictedCommand =
+  withDoc "Restricted command name" .
+  renameField "RestrictedCommand" $
+  simpleField "command" [t| NonEmptyString |]
+
+pNodeName :: Field
+pNodeName =
+  withDoc "A required node name (for single-node LUs)" $
+  simpleField "node_name" [t| NonEmptyString |]
+
+pNodeUuid :: Field
+pNodeUuid =
+  withDoc "A node UUID (for single-node LUs)" .
+  optionalField $ simpleField "node_uuid" [t| NonEmptyString |]
+
 pPrimaryIp :: Field
-pPrimaryIp = optionalStringField "primary_ip"
+pPrimaryIp =
+  withDoc "Primary IP address" .
+  optionalField $
+  simpleField "primary_ip" [t| NonEmptyString |]
 
--- | Secondary IP address.
 pSecondaryIp :: Field
-pSecondaryIp = optionalNEStringField "secondary_ip"
+pSecondaryIp =
+  withDoc "Secondary IP address" $
+  optionalNEStringField "secondary_ip"
 
--- | Whether node is re-added to cluster.
 pReadd :: Field
-pReadd = defaultFalse "readd"
+pReadd =
+  withDoc "Whether node is re-added to cluster" $
+  defaultFalse "readd"
 
--- | Initial node group.
 pNodeGroup :: Field
-pNodeGroup = optionalNEStringField "group"
+pNodeGroup =
+  withDoc "Initial node group" $
+  optionalNEStringField "group"
 
--- | Whether node can become master or master candidate.
 pMasterCapable :: Field
-pMasterCapable = optionalField $ booleanField "master_capable"
+pMasterCapable =
+  withDoc "Whether node can become master or master candidate" .
+  optionalField $ booleanField "master_capable"
 
--- | Whether node can host instances.
 pVmCapable :: Field
-pVmCapable = optionalField $ booleanField "vm_capable"
+pVmCapable =
+  withDoc "Whether node can host instances" .
+  optionalField $ booleanField "vm_capable"
 
--- | List of names.
+pNdParams :: Field
+pNdParams =
+  withDoc "Node parameters" .
+  renameField "genericNdParams" .
+  optionalField $ simpleField "ndparams" [t| JSObject JSValue |]
+  
 pNames :: Field
-pNames = defaultField [| [] |] $ simpleField "names" [t| [NonEmptyString] |]
+pNames =
+  withDoc "List of names" .
+  defaultField [| [] |] $ simpleField "names" [t| [NonEmptyString] |]
 
--- | List of node names.
 pNodes :: Field
-pNodes = defaultField [| [] |] $ simpleField "nodes" [t| [NonEmptyString] |]
+pNodes =
+  withDoc "List of nodes" .
+  defaultField [| [] |] $ simpleField "nodes" [t| [NonEmptyString] |]
 
--- | Required list of node names.
-pRequiredNodes :: Field
-pRequiredNodes =
-  renameField "ReqNodes " $ simpleField "nodes" [t| [NonEmptyString] |]
+pStorageType :: Field
+pStorageType =
+  withDoc "Storage type" $ simpleField "storage_type" [t| StorageType |]
 
--- | Required list of node names.
-pRequiredNodeUuids :: Field
-pRequiredNodeUuids =
-  renameField "ReqNodeUuids " . optionalField $
-    simpleField "node_uuids" [t| [NonEmptyString] |]
+pStorageTypeOptional :: Field
+pStorageTypeOptional =
+  withDoc "Storage type" .
+  renameField "StorageTypeOptional" .
+  optionalField $ simpleField "storage_type" [t| StorageType |]
 
--- | Storage type.
-pStorageType :: Field
-pStorageType = simpleField "storage_type" [t| StorageType |]
+pStorageName :: Field
+pStorageName =
+  withDoc "Storage name" .
+  renameField "StorageName" .
+  optionalField $ simpleField "name" [t| NonEmptyString |]
 
--- | Storage changes (unchecked).
 pStorageChanges :: Field
-pStorageChanges = simpleField "changes" [t| UncheckedDict |]
+pStorageChanges =
+  withDoc "Requested storage changes" $
+  simpleField "changes" [t| JSObject JSValue |]
+
+pIgnoreConsistency :: Field
+pIgnoreConsistency =
+  withDoc "Whether to ignore disk consistency" $
+  defaultFalse "ignore_consistency"
 
--- | Whether the node should become a master candidate.
 pMasterCandidate :: Field
-pMasterCandidate = optionalField $ booleanField "master_candidate"
+pMasterCandidate =
+  withDoc "Whether the node should become a master candidate" .
+  optionalField $ booleanField "master_candidate"
 
--- | Whether the node should be marked as offline.
 pOffline :: Field
-pOffline = optionalField $ booleanField "offline"
+pOffline =
+  withDoc "Whether to mark the node or instance offline" .
+  optionalField $ booleanField "offline"
 
--- | Whether the node should be marked as drained.
 pDrained ::Field
-pDrained = optionalField $ booleanField "drained"
+pDrained =
+  withDoc "Whether to mark the node as drained" .
+  optionalField $ booleanField "drained"
 
--- | Whether node(s) should be promoted to master candidate if necessary.
 pAutoPromote :: Field
-pAutoPromote = defaultFalse "auto_promote"
+pAutoPromote =
+  withDoc "Whether node(s) should be promoted to master candidate if\
+          \ necessary" $
+  defaultFalse "auto_promote"
 
--- | Whether the node should be marked as powered
 pPowered :: Field
-pPowered = optionalField $ booleanField "powered"
+pPowered =
+  withDoc "Whether the node should be marked as powered" .
+  optionalField $ booleanField "powered"
+
+pMigrationMode :: Field
+pMigrationMode =
+  withDoc "Migration type (live/non-live)" .
+  renameField "MigrationMode" .
+  optionalField $
+  simpleField "mode" [t| MigrationMode |]
 
--- | Iallocator for deciding the target node for shared-storage
--- instances during migrate and failover.
+pMigrationLive :: Field
+pMigrationLive =
+  withDoc "Obsolete \'live\' migration mode (do not use)" .
+  renameField "OldLiveMode" . optionalField $ booleanField "live"
+
+pMigrationTargetNode :: Field
+pMigrationTargetNode =
+  withDoc "Target node for instance migration/failover" $
+  optionalNEStringField "target_node"
+
+pMigrationTargetNodeUuid :: Field
+pMigrationTargetNodeUuid =
+  withDoc "Target node UUID for instance migration/failover" $
+  optionalNEStringField "target_node_uuid"
+
+pAllowRuntimeChgs :: Field
+pAllowRuntimeChgs =
+  withDoc "Whether to allow runtime changes while migrating" $
+  defaultTrue "allow_runtime_changes"
+
+pIgnoreIpolicy :: Field
+pIgnoreIpolicy =
+  withDoc "Whether to ignore ipolicy violations" $
+  defaultFalse "ignore_ipolicy"
+  
 pIallocator :: Field
-pIallocator = optionalNEStringField "iallocator"
+pIallocator =
+  withDoc "Iallocator for deciding the target node for shared-storage\
+          \ instances" $
+  optionalNEStringField "iallocator"
+
+pEarlyRelease :: Field
+pEarlyRelease =
+  withDoc "Whether to release locks as soon as possible" $
+  defaultFalse "early_release"
 
--- | New secondary node.
 pRemoteNode :: Field
-pRemoteNode = optionalNEStringField "remote_node"
+pRemoteNode =
+  withDoc "New secondary node" $
+  optionalNEStringField "remote_node"
 
--- | New secondary node UUID.
 pRemoteNodeUuid :: Field
-pRemoteNodeUuid = optionalNEStringField "remote_node_uuid"
+pRemoteNodeUuid =
+  withDoc "New secondary node UUID" $
+  optionalNEStringField "remote_node_uuid"
 
--- | Node evacuation mode.
 pEvacMode :: Field
-pEvacMode = renameField "EvacMode" $ simpleField "mode" [t| NodeEvacMode |]
+pEvacMode =
+  withDoc "Node evacuation mode" .
+  renameField "EvacMode" $ simpleField "mode" [t| EvacMode |]
+
+pInstanceName :: Field
+pInstanceName =
+  withDoc "A required instance name (for single-instance LUs)" $
+  simpleField "instance_name" [t| String |]
+
+pForceVariant :: Field
+pForceVariant =
+  withDoc "Whether to force an unknown OS variant" $
+  defaultFalse "force_variant"
+
+pWaitForSync :: Field
+pWaitForSync =
+  withDoc "Whether to wait for the disk to synchronize" $
+  defaultTrue "wait_for_sync"
+
+pNameCheck :: Field
+pNameCheck =
+  withDoc "Whether to check name" $
+  defaultTrue "name_check"
+
+pInstBeParams :: Field
+pInstBeParams =
+  withDoc "Backend parameters for instance" .
+  renameField "InstBeParams" .
+  defaultField [| toJSObject [] |] $
+  simpleField "beparams" [t| JSObject JSValue |]
+
+pInstDisks :: Field
+pInstDisks =
+  withDoc "List of instance disks" .
+  renameField "instDisks" $ simpleField "disks" [t| [IDiskParams] |]
+
+pDiskTemplate :: Field
+pDiskTemplate =
+  withDoc "Disk template" $
+  simpleField "disk_template" [t| DiskTemplate |]
+
+pFileDriver :: Field
+pFileDriver =
+  withDoc "Driver for file-backed disks" .
+  optionalField $ simpleField "file_driver" [t| FileDriver |]
+
+pFileStorageDir :: Field
+pFileStorageDir =
+  withDoc "Directory for storing file-backed disks" $
+  optionalNEStringField "file_storage_dir"
+
+pInstHvParams :: Field
+pInstHvParams =
+  withDoc "Hypervisor parameters for instance, hypervisor-dependent" .
+  renameField "InstHvParams" .
+  defaultField [| toJSObject [] |] $
+  simpleField "hvparams" [t| JSObject JSValue |]
+
+pHypervisor :: Field
+pHypervisor =
+  withDoc "Selected hypervisor for an instance" .
+  optionalField $
+  simpleField "hypervisor" [t| Hypervisor |]
+
+pResetDefaults :: Field
+pResetDefaults =
+  withDoc "Reset instance parameters to default if equal" $
+  defaultFalse "identify_defaults"
+
+pIpCheck :: Field
+pIpCheck =
+  withDoc "Whether to ensure instance's IP address is inactive" $
+  defaultTrue "ip_check"
+
+pIpConflictsCheck :: Field
+pIpConflictsCheck =
+  withDoc "Whether to check for conflicting IP addresses" $
+  defaultTrue "conflicts_check"
 
--- | Instance creation mode.
 pInstCreateMode :: Field
 pInstCreateMode =
+  withDoc "Instance creation mode" .
   renameField "InstCreateMode" $ simpleField "mode" [t| InstCreateMode |]
 
--- | Do not install the OS (will disable automatic start).
+pInstNics :: Field
+pInstNics =
+  withDoc "List of NIC (network interface) definitions" $
+  simpleField "nics" [t| [INicParams] |]
+
 pNoInstall :: Field
-pNoInstall = optionalField $ booleanField "no_install"
+pNoInstall =
+  withDoc "Do not install the OS (will disable automatic start)" .
+  optionalField $ booleanField "no_install"
 
--- | OS type for instance installation.
 pInstOs :: Field
-pInstOs = optionalNEStringField "os_type"
+pInstOs =
+  withDoc "OS type for instance installation" $
+  optionalNEStringField "os_type"
+
+pInstOsParams :: Field
+pInstOsParams =
+  withDoc "OS parameters for instance" .
+  renameField "InstOsParams" .
+  defaultField [| toJSObject [] |] $
+  simpleField "osparams" [t| JSObject JSValue |]
 
--- | Primary node for an instance.
 pPrimaryNode :: Field
-pPrimaryNode = optionalNEStringField "pnode"
+pPrimaryNode =
+  withDoc "Primary node for an instance" $
+  optionalNEStringField "pnode"
 
--- | Primary node UUID for an instance.
 pPrimaryNodeUuid :: Field
-pPrimaryNodeUuid = optionalNEStringField "pnode_uuid"
+pPrimaryNodeUuid =
+  withDoc "Primary node UUID for an instance" $
+  optionalNEStringField "pnode_uuid"
 
--- | Secondary node for an instance.
 pSecondaryNode :: Field
-pSecondaryNode = optionalNEStringField "snode"
+pSecondaryNode =
+  withDoc "Secondary node for an instance" $
+  optionalNEStringField "snode"
 
--- | Secondary node UUID for an instance.
 pSecondaryNodeUuid :: Field
-pSecondaryNodeUuid = optionalNEStringField "snode_uuid"
+pSecondaryNodeUuid =
+  withDoc "Secondary node UUID for an instance" $
+  optionalNEStringField "snode_uuid"
 
--- | Signed handshake from source (remote import only).
 pSourceHandshake :: Field
 pSourceHandshake =
-  optionalField $ simpleField "source_handshake" [t| UncheckedList |]
+  withDoc "Signed handshake from source (remote import only)" .
+  optionalField $ simpleField "source_handshake" [t| [JSValue] |]
 
--- | Source instance name (remote import only).
 pSourceInstance :: Field
-pSourceInstance = optionalNEStringField "source_instance_name"
+pSourceInstance =
+  withDoc "Source instance name (remote import only)" $
+  optionalNEStringField "source_instance_name"
 
--- | How long source instance was given to shut down (remote import only).
 -- FIXME: non-negative int, whereas the constant is a plain int.
 pSourceShutdownTimeout :: Field
 pSourceShutdownTimeout =
+  withDoc "How long source instance was given to shut down (remote import\
+          \ only)" .
   defaultField [| forceNonNeg C.defaultShutdownTimeout |] $
   simpleField "source_shutdown_timeout" [t| NonNegative Int |]
 
--- | Source X509 CA in PEM format (remote import only).
 pSourceX509Ca :: Field
-pSourceX509Ca = optionalNEStringField "source_x509_ca"
+pSourceX509Ca =
+  withDoc "Source X509 CA in PEM format (remote import only)" $
+  optionalNEStringField "source_x509_ca"
 
--- | Source node for import.
 pSrcNode :: Field
-pSrcNode = optionalNEStringField "src_node"
+pSrcNode =
+  withDoc "Source node for import" $
+  optionalNEStringField "src_node"
 
--- | Source node for import.
 pSrcNodeUuid :: Field
-pSrcNodeUuid = optionalNEStringField "src_node_uuid"
+pSrcNodeUuid =
+  withDoc "Source node UUID for import" $
+  optionalNEStringField "src_node_uuid"
 
--- | Source directory for import.
 pSrcPath :: Field
-pSrcPath = optionalNEStringField "src_path"
+pSrcPath =
+  withDoc "Source directory for import" $
+  optionalNEStringField "src_path"
 
--- | Whether to start instance after creation.
 pStartInstance :: Field
-pStartInstance = defaultTrue "start"
+pStartInstance =
+  withDoc "Whether to start instance after creation" $
+  defaultTrue "start"
 
--- | Instance tags. FIXME: unify/simplify with pTags, once that
--- migrates to NonEmpty String.
+-- FIXME: unify/simplify with pTags, once that migrates to NonEmpty String"
 pInstTags :: Field
 pInstTags =
+  withDoc "Instance tags" .
   renameField "InstTags" .
   defaultField [| [] |] $
   simpleField "tags" [t| [NonEmptyString] |]
 
--- | Unchecked list of OpInstanceCreate, used in OpInstanceMultiAlloc.
 pMultiAllocInstances :: Field
 pMultiAllocInstances =
+  withDoc "List of instance create opcodes describing the instances to\
+          \ allocate" .
   renameField "InstMultiAlloc" .
   defaultField [| [] |] $
-  simpleField "instances"[t| UncheckedList |]
+  simpleField "instances"[t| [JSValue] |]
+
+pOpportunisticLocking :: Field
+pOpportunisticLocking =
+  withDoc "Whether to employ opportunistic locking for nodes, meaning\
+          \ nodes already locked by another opcode won't be considered for\
+          \ instance allocation (only when an iallocator is used)" $
+  defaultFalse "opportunistic_locking"
+
+pInstanceUuid :: Field
+pInstanceUuid =
+  withDoc "An instance UUID (for single-instance LUs)" .
+  optionalField $ simpleField "instance_uuid" [t| NonEmptyString |]
+
+pTempOsParams :: Field
+pTempOsParams =
+  withDoc "Temporary OS parameters (currently only in reinstall, might be\
+          \ added to install as well)" .
+  renameField "TempOsParams" .
+  optionalField $ simpleField "osparams" [t| JSObject JSValue |]
+
+pShutdownTimeout :: Field
+pShutdownTimeout =
+  withDoc "How long to wait for instance to shut down" .
+  defaultField [| forceNonNeg C.defaultShutdownTimeout |] $
+  simpleField "shutdown_timeout" [t| NonNegative Int |]
+
+-- | Another name for the shutdown timeout, because we like to be
+-- inconsistent.
+pShutdownTimeout' :: Field
+pShutdownTimeout' =
+  withDoc "How long to wait for instance to shut down" .
+  renameField "InstShutdownTimeout" .
+  defaultField [| forceNonNeg C.defaultShutdownTimeout |] $
+  simpleField "timeout" [t| NonNegative Int |]
 
--- | Ignore failures parameter.
 pIgnoreFailures :: Field
-pIgnoreFailures = defaultFalse "ignore_failures"
+pIgnoreFailures =
+  withDoc "Whether to ignore failures during removal" $
+  defaultFalse "ignore_failures"
 
--- | New instance or cluster name.
 pNewName :: Field
-pNewName = simpleField "new_name" [t| NonEmptyString |]
+pNewName =
+  withDoc "New group or instance name" $
+  simpleField "new_name" [t| NonEmptyString |]
+  
+pIgnoreOfflineNodes :: Field
+pIgnoreOfflineNodes =
+  withDoc "Whether to ignore offline nodes" $
+  defaultFalse "ignore_offline_nodes"
+
+pTempHvParams :: Field
+pTempHvParams =
+  withDoc "Temporary hypervisor parameters, hypervisor-dependent" .
+  renameField "TempHvParams" .
+  defaultField [| toJSObject [] |] $
+  simpleField "hvparams" [t| JSObject JSValue |]
+
+pTempBeParams :: Field
+pTempBeParams =
+  withDoc "Temporary backend parameters" .
+  renameField "TempBeParams" .
+  defaultField [| toJSObject [] |] $
+  simpleField "beparams" [t| JSObject JSValue |]
+
+pNoRemember :: Field
+pNoRemember =
+  withDoc "Do not remember instance state changes" $
+  defaultFalse "no_remember"
+
+pStartupPaused :: Field
+pStartupPaused =
+  withDoc "Pause instance at startup" $
+  defaultFalse "startup_paused"
 
--- | Whether to start the instance even if secondary disks are failing.
 pIgnoreSecondaries :: Field
-pIgnoreSecondaries = defaultFalse "ignore_secondaries"
+pIgnoreSecondaries =
+  withDoc "Whether to start the instance even if secondary disks are failing" $
+  defaultFalse "ignore_secondaries"
 
--- | How to reboot the instance.
 pRebootType :: Field
-pRebootType = simpleField "reboot_type" [t| RebootType |]
+pRebootType =
+  withDoc "How to reboot the instance" $
+  simpleField "reboot_type" [t| RebootType |]
 
--- | Whether to ignore recorded disk size.
-pIgnoreDiskSize :: Field
-pIgnoreDiskSize = defaultFalse "ignore_size"
+pReplaceDisksMode :: Field
+pReplaceDisksMode =
+  withDoc "Replacement mode" .
+  renameField "ReplaceDisksMode" $ simpleField "mode" [t| ReplaceDisksMode |]
+
+pReplaceDisksList :: Field
+pReplaceDisksList =
+  withDoc "List of disk indices" .
+  renameField "ReplaceDisksList" .
+  defaultField [| [] |] $
+  simpleField "disks" [t| [DiskIndex] |]
+
+pMigrationCleanup :: Field
+pMigrationCleanup =
+  withDoc "Whether a previously failed migration should be cleaned up" .
+  renameField "MigrationCleanup" $ defaultFalse "cleanup"
+
+pAllowFailover :: Field
+pAllowFailover =
+  withDoc "Whether we can fallback to failover if migration is not possible" $
+  defaultFalse "allow_failover"
 
--- | Disk list for recreate disks.
+pMoveTargetNode :: Field
+pMoveTargetNode =
+  withDoc "Target node for instance move" .
+  renameField "MoveTargetNode" $
+  simpleField "target_node" [t| NonEmptyString |]
+
+pMoveTargetNodeUuid :: Field
+pMoveTargetNodeUuid =
+  withDoc "Target node UUID for instance move" .
+  renameField "MoveTargetNodeUuid" . optionalField $
+  simpleField "target_node_uuid" [t| NonEmptyString |]
+
+pIgnoreDiskSize :: Field
+pIgnoreDiskSize =
+  withDoc "Whether to ignore recorded disk size" $
+  defaultFalse "ignore_size"
+  
+pWaitForSyncFalse :: Field
+pWaitForSyncFalse =
+  withDoc "Whether to wait for the disk to synchronize (defaults to false)" $
+  defaultField [| False |] pWaitForSync
+  
 pRecreateDisksInfo :: Field
 pRecreateDisksInfo =
+  withDoc "Disk list for recreate disks" .
   renameField "RecreateDisksInfo" .
   defaultField [| RecreateDisksAll |] $
   simpleField "disks" [t| RecreateDisksInfo |]
 
--- | Whether to only return configuration data without querying nodes.
 pStatic :: Field
-pStatic = defaultFalse "static"
+pStatic =
+  withDoc "Whether to only return configuration data without querying nodes" $
+  defaultFalse "static"
 
--- | InstanceSetParams NIC changes.
 pInstParamsNicChanges :: Field
 pInstParamsNicChanges =
+  withDoc "List of NIC changes" .
   renameField "InstNicChanges" .
   defaultField [| SetParamsEmpty |] $
   simpleField "nics" [t| SetParamsMods INicParams |]
 
--- | InstanceSetParams Disk changes.
 pInstParamsDiskChanges :: Field
 pInstParamsDiskChanges =
+  withDoc "List of disk changes" .
   renameField "InstDiskChanges" .
   defaultField [| SetParamsEmpty |] $
   simpleField "disks" [t| SetParamsMods IDiskParams |]
 
--- | New runtime memory.
 pRuntimeMem :: Field
-pRuntimeMem = optionalField $ simpleField "runtime_mem" [t| Positive Int |]
+pRuntimeMem =
+  withDoc "New runtime memory" .
+  optionalField $ simpleField "runtime_mem" [t| Positive Int |]
+
+pOptDiskTemplate :: Field
+pOptDiskTemplate =
+  withDoc "Instance disk template" .
+  optionalField .
+  renameField "OptDiskTemplate" $
+  simpleField "disk_template" [t| DiskTemplate |]
 
--- | Change the instance's OS without reinstalling the instance
 pOsNameChange :: Field
-pOsNameChange = optionalNEStringField "os_name"
+pOsNameChange =
+  withDoc "Change the instance's OS without reinstalling the instance" $
+  optionalNEStringField "os_name"
 
--- | Disk index for e.g. grow disk.
 pDiskIndex :: Field
-pDiskIndex = renameField "DiskIndex " $ simpleField "disk" [t| DiskIndex |]
+pDiskIndex =
+  withDoc "Disk index for e.g. grow disk" .
+  renameField "DiskIndex " $ simpleField "disk" [t| DiskIndex |]
 
--- | Disk amount to add or grow to.
 pDiskChgAmount :: Field
 pDiskChgAmount =
+  withDoc "Disk amount to add or grow to" .
   renameField "DiskChgAmount" $ simpleField "amount" [t| NonNegative Int |]
 
--- | Whether the amount parameter is an absolute target or a relative one.
 pDiskChgAbsolute :: Field
-pDiskChgAbsolute = renameField "DiskChkAbsolute" $ defaultFalse "absolute"
+pDiskChgAbsolute =
+  withDoc
+    "Whether the amount parameter is an absolute target or a relative one" .
+  renameField "DiskChkAbsolute" $ defaultFalse "absolute"
 
--- | Destination group names or UUIDs (defaults to \"all but current group\".
 pTargetGroups :: Field
 pTargetGroups =
+  withDoc
+    "Destination group names or UUIDs (defaults to \"all but current group\")" .
   optionalField $ simpleField "target_groups" [t| [NonEmptyString] |]
 
--- | Export mode field.
+pNodeGroupAllocPolicy :: Field
+pNodeGroupAllocPolicy =
+  withDoc "Instance allocation policy" .
+  optionalField $
+  simpleField "alloc_policy" [t| AllocPolicy |]
+
+pGroupNodeParams :: Field
+pGroupNodeParams =
+  withDoc "Default node parameters for group" .
+  optionalField $ simpleField "ndparams" [t| JSObject JSValue |]
+
 pExportMode :: Field
 pExportMode =
+  withDoc "Export mode" .
   renameField "ExportMode" $ simpleField "mode" [t| ExportMode |]
 
--- | Export target_node field, depends on mode.
+-- FIXME: Rename target_node as it changes meaning for different
+-- export modes (e.g. "destination")
 pExportTargetNode :: Field
 pExportTargetNode =
+  withDoc "Target node (depends on export mode)" .
   renameField "ExportTarget" $
   simpleField "target_node" [t| ExportTarget |]
 
--- | Export target node UUID field.
 pExportTargetNodeUuid :: Field
 pExportTargetNodeUuid =
+  withDoc "Target node UUID (if local export)" .
   renameField "ExportTargetNodeUuid" . optionalField $
   simpleField "target_node_uuid" [t| NonEmptyString |]
 
--- | Whether to remove instance after export.
+pShutdownInstance :: Field
+pShutdownInstance =
+  withDoc "Whether to shutdown the instance before export" $
+  defaultTrue "shutdown"
+
 pRemoveInstance :: Field
-pRemoveInstance = defaultFalse "remove_instance"
+pRemoveInstance =
+  withDoc "Whether to remove instance after export" $
+  defaultFalse "remove_instance"
 
--- | Whether to ignore failures while removing instances.
 pIgnoreRemoveFailures :: Field
-pIgnoreRemoveFailures = defaultFalse "ignore_remove_failures"
+pIgnoreRemoveFailures =
+  withDoc "Whether to ignore failures while removing instances" $
+  defaultFalse "ignore_remove_failures"
 
--- | Name of X509 key (remote export only).
 pX509KeyName :: Field
-pX509KeyName = optionalField $ simpleField "x509_key_name" [t| UncheckedList |]
+pX509KeyName =
+  withDoc "Name of X509 key (remote export only)" .
+  optionalField $ simpleField "x509_key_name" [t| [JSValue] |]
 
--- | Destination X509 CA (remote export only).
 pX509DestCA :: Field
-pX509DestCA = optionalNEStringField "destination_x509_ca"
-
--- | Search pattern (regular expression). FIXME: this should be
--- compiled at load time?
-pTagSearchPattern :: Field
-pTagSearchPattern =
-  renameField "TagSearchPattern" $ simpleField "pattern" [t| NonEmptyString |]
+pX509DestCA =
+  withDoc "Destination X509 CA (remote export only)" $
+  optionalNEStringField "destination_x509_ca"
 
--- | Restricted command name.
-pRestrictedCommand :: Field
-pRestrictedCommand =
-  renameField "RestrictedCommand" $
-  simpleField "command" [t| NonEmptyString |]
+pTagsObject :: Field
+pTagsObject =
+  withDoc "Tag kind" $
+  simpleField "kind" [t| TagKind |]
 
--- | Replace disks mode.
-pReplaceDisksMode :: Field
-pReplaceDisksMode =
-  renameField "ReplaceDisksMode" $ simpleField "mode" [t| ReplaceDisksMode |]
+pTagsName :: Field
+pTagsName =
+  withDoc "Name of object" .
+  renameField "TagsGetName" .
+  optionalField $ simpleField "name" [t| String |]
 
--- | List of disk indices.
-pReplaceDisksList :: Field
-pReplaceDisksList =
-  renameField "ReplaceDisksList" $ simpleField "disks" [t| [DiskIndex] |]
-
--- | Whether do allow failover in migrations.
-pAllowFailover :: Field
-pAllowFailover = defaultFalse "allow_failover"
+pTagsList :: Field
+pTagsList =
+  withDoc "List of tag names" $
+  simpleField "tags" [t| [String] |]
 
--- * Test opcode parameters
+-- FIXME: this should be compiled at load time?
+pTagSearchPattern :: Field
+pTagSearchPattern =
+  withDoc "Search pattern (regular expression)" .
+  renameField "TagSearchPattern" $
+  simpleField "pattern" [t| NonEmptyString |]
 
--- | Duration parameter for 'OpTestDelay'.
 pDelayDuration :: Field
 pDelayDuration =
-  renameField "DelayDuration" $ simpleField "duration" [t| Double |]
+  withDoc "Duration parameter for 'OpTestDelay'" .
+  renameField "DelayDuration" $
+  simpleField "duration" [t| Double |]
 
--- | on_master field for 'OpTestDelay'.
 pDelayOnMaster :: Field
-pDelayOnMaster = renameField "DelayOnMaster" $ defaultTrue "on_master"
+pDelayOnMaster =
+  withDoc "on_master field for 'OpTestDelay'" .
+  renameField "DelayOnMaster" $
+  defaultTrue "on_master"
 
--- | on_nodes field for 'OpTestDelay'.
 pDelayOnNodes :: Field
 pDelayOnNodes =
+  withDoc "on_nodes field for 'OpTestDelay'" .
   renameField "DelayOnNodes" .
   defaultField [| [] |] $
   simpleField "on_nodes" [t| [NonEmptyString] |]
 
--- | on_node_uuids field for 'OpTestDelay'.
 pDelayOnNodeUuids :: Field
 pDelayOnNodeUuids =
+  withDoc "on_node_uuids field for 'OpTestDelay'" .
   renameField "DelayOnNodeUuids" . optionalField $
   simpleField "on_node_uuids" [t| [NonEmptyString] |]
 
--- | Repeat parameter for OpTestDelay.
 pDelayRepeat :: Field
 pDelayRepeat =
+  withDoc "Repeat parameter for OpTestDelay" .
   renameField "DelayRepeat" .
   defaultField [| forceNonNeg (0::Int) |] $
   simpleField "repeat" [t| NonNegative Int |]
 
--- | IAllocator test direction.
 pIAllocatorDirection :: Field
 pIAllocatorDirection =
+  withDoc "IAllocator test direction" .
   renameField "IAllocatorDirection" $
   simpleField "direction" [t| IAllocatorTestDir |]
 
--- | IAllocator test mode.
 pIAllocatorMode :: Field
 pIAllocatorMode =
+  withDoc "IAllocator test mode" .
   renameField "IAllocatorMode" $
   simpleField "mode" [t| IAllocatorMode |]
 
--- | IAllocator target name (new instance, node to evac, etc.).
 pIAllocatorReqName :: Field
 pIAllocatorReqName =
+  withDoc "IAllocator target name (new instance, node to evac, etc.)" .
   renameField "IAllocatorReqName" $ simpleField "name" [t| NonEmptyString |]
 
--- | Custom OpTestIAllocator nics.
 pIAllocatorNics :: Field
 pIAllocatorNics =
-  renameField "IAllocatorNics" $ simpleField "nics" [t| [UncheckedDict] |]
+  withDoc "Custom OpTestIAllocator nics" .
+  renameField "IAllocatorNics" .
+  optionalField $ simpleField "nics" [t| [INicParams] |]
 
--- | Custom OpTestAllocator disks.
 pIAllocatorDisks :: Field
 pIAllocatorDisks =
-  renameField "IAllocatorDisks" $ simpleField "disks" [t| UncheckedList |]
+  withDoc "Custom OpTestAllocator disks" .
+  renameField "IAllocatorDisks" .
+  optionalField $ simpleField "disks" [t| [JSValue] |]
 
--- | IAllocator memory field.
 pIAllocatorMemory :: Field
 pIAllocatorMemory =
+  withDoc "IAllocator memory field" .
   renameField "IAllocatorMem" .
   optionalField $
   simpleField "memory" [t| NonNegative Int |]
 
--- | IAllocator vcpus field.
 pIAllocatorVCpus :: Field
 pIAllocatorVCpus =
+  withDoc "IAllocator vcpus field" .
   renameField "IAllocatorVCpus" .
   optionalField $
   simpleField "vcpus" [t| NonNegative Int |]
 
--- | IAllocator os field.
 pIAllocatorOs :: Field
-pIAllocatorOs = renameField "IAllocatorOs" $ optionalNEStringField "os"
+pIAllocatorOs =
+  withDoc "IAllocator os field" .
+  renameField "IAllocatorOs" $ optionalNEStringField "os"
 
--- | IAllocator instances field.
 pIAllocatorInstances :: Field
 pIAllocatorInstances =
+  withDoc "IAllocator instances field" .
   renameField "IAllocatorInstances " .
   optionalField $
   simpleField "instances" [t| [NonEmptyString] |]
 
--- | IAllocator evac mode.
 pIAllocatorEvacMode :: Field
 pIAllocatorEvacMode =
+  withDoc "IAllocator evac mode" .
   renameField "IAllocatorEvacMode" .
   optionalField $
-  simpleField "evac_mode" [t| NodeEvacMode |]
+  simpleField "evac_mode" [t| EvacMode |]
 
--- | IAllocator spindle use.
 pIAllocatorSpindleUse :: Field
 pIAllocatorSpindleUse =
+  withDoc "IAllocator spindle use" .
   renameField "IAllocatorSpindleUse" .
   defaultField [| forceNonNeg (1::Int) |] $
   simpleField "spindle_use" [t| NonNegative Int |]
 
--- | IAllocator count field.
 pIAllocatorCount :: Field
 pIAllocatorCount =
+  withDoc "IAllocator count field" .
   renameField "IAllocatorCount" .
   defaultField [| forceNonNeg (1::Int) |] $
   simpleField "count" [t| NonNegative Int |]
 
--- | 'OpTestJqueue' notify_waitlock.
 pJQueueNotifyWaitLock :: Field
-pJQueueNotifyWaitLock = defaultFalse "notify_waitlock"
+pJQueueNotifyWaitLock =
+  withDoc "'OpTestJqueue' notify_waitlock" $
+  defaultFalse "notify_waitlock"
 
--- | 'OpTestJQueue' notify_exec.
 pJQueueNotifyExec :: Field
-pJQueueNotifyExec = defaultFalse "notify_exec"
+pJQueueNotifyExec =
+  withDoc "'OpTestJQueue' notify_exec" $
+  defaultFalse "notify_exec"
 
--- | 'OpTestJQueue' log_messages.
 pJQueueLogMessages :: Field
 pJQueueLogMessages =
+  withDoc "'OpTestJQueue' log_messages" .
   defaultField [| [] |] $ simpleField "log_messages" [t| [String] |]
 
--- | 'OpTestJQueue' fail attribute.
 pJQueueFail :: Field
 pJQueueFail =
+  withDoc "'OpTestJQueue' fail attribute" .
   renameField "JQueueFail" $ defaultFalse "fail"
 
--- | 'OpTestDummy' result field.
 pTestDummyResult :: Field
 pTestDummyResult =
-  renameField "TestDummyResult" $ simpleField "result" [t| UncheckedValue |]
+  withDoc "'OpTestDummy' result field" .
+  renameField "TestDummyResult" $ simpleField "result" [t| JSValue |]
 
--- | 'OpTestDummy' messages field.
 pTestDummyMessages :: Field
 pTestDummyMessages =
+  withDoc "'OpTestDummy' messages field" .
   renameField "TestDummyMessages" $
-  simpleField "messages" [t| UncheckedValue |]
+  simpleField "messages" [t| JSValue |]
 
--- | 'OpTestDummy' fail field.
 pTestDummyFail :: Field
 pTestDummyFail =
-  renameField "TestDummyFail" $ simpleField "fail" [t| UncheckedValue |]
+  withDoc "'OpTestDummy' fail field" .
+  renameField "TestDummyFail" $ simpleField "fail" [t| JSValue |]
 
--- | 'OpTestDummy' submit_jobs field.
 pTestDummySubmitJobs :: Field
 pTestDummySubmitJobs =
+  withDoc "'OpTestDummy' submit_jobs field" .
   renameField "TestDummySubmitJobs" $
-  simpleField "submit_jobs" [t| UncheckedValue |]
+  simpleField "submit_jobs" [t| JSValue |]
 
--- * Network parameters
-
--- | Network name.
 pNetworkName :: Field
-pNetworkName = simpleField "network_name" [t| NonEmptyString |]
+pNetworkName =
+  withDoc "Network name" $
+  simpleField "network_name" [t| NonEmptyString |]
 
--- | Network address (IPv4 subnet). FIXME: no real type for this.
 pNetworkAddress4 :: Field
 pNetworkAddress4 =
+  withDoc "Network address (IPv4 subnet)" .
   renameField "NetworkAddress4" $
-  simpleField "network" [t| NonEmptyString |]
+  simpleField "network" [t| IPv4Network |]
 
--- | Network gateway (IPv4 address). FIXME: no real type for this.
 pNetworkGateway4 :: Field
 pNetworkGateway4 =
-  renameField "NetworkGateway4" $
-  optionalNEStringField "gateway"
+  withDoc "Network gateway (IPv4 address)" .
+  renameField "NetworkGateway4" .
+  optionalField $ simpleField "gateway" [t| IPv4Address |]
 
--- | Network address (IPv6 subnet). FIXME: no real type for this.
 pNetworkAddress6 :: Field
 pNetworkAddress6 =
-  renameField "NetworkAddress6" $
-  optionalNEStringField "network6"
+  withDoc "Network address (IPv6 subnet)" .
+  renameField "NetworkAddress6" .
+  optionalField $ simpleField "network6" [t| IPv6Network |]
 
--- | Network gateway (IPv6 address). FIXME: no real type for this.
 pNetworkGateway6 :: Field
 pNetworkGateway6 =
-  renameField "NetworkGateway6" $
-  optionalNEStringField "gateway6"
+  withDoc "Network gateway (IPv6 address)" .
+  renameField "NetworkGateway6" .
+  optionalField $ simpleField "gateway6" [t| IPv6Address |]
 
--- | Network specific mac prefix (that overrides the cluster one).
 pNetworkMacPrefix :: Field
 pNetworkMacPrefix =
+  withDoc "Network specific mac prefix (that overrides the cluster one)" .
   renameField "NetMacPrefix" $
   optionalNEStringField "mac_prefix"
 
--- | Network add reserved IPs.
 pNetworkAddRsvdIps :: Field
 pNetworkAddRsvdIps =
+  withDoc "Which IP addresses to reserve" .
   renameField "NetworkAddRsvdIps" .
   optionalField $
-  simpleField "add_reserved_ips" [t| [NonEmptyString] |]
+  simpleField "add_reserved_ips" [t| [IPv4Address] |]
 
--- | Network remove reserved IPs.
 pNetworkRemoveRsvdIps :: Field
 pNetworkRemoveRsvdIps =
+  withDoc "Which external IP addresses to release" .
   renameField "NetworkRemoveRsvdIps" .
   optionalField $
-  simpleField "remove_reserved_ips" [t| [NonEmptyString] |]
+  simpleField "remove_reserved_ips" [t| [IPv4Address] |]
 
--- | Network mode when connecting to a group.
 pNetworkMode :: Field
-pNetworkMode = simpleField "network_mode" [t| NICMode |]
+pNetworkMode =
+  withDoc "Network mode when connecting to a group" $
+  simpleField "network_mode" [t| NICMode |]
 
--- | Network link when connecting to a group.
 pNetworkLink :: Field
-pNetworkLink = simpleField "network_link" [t| NonEmptyString |]
-
--- * Common opcode parameters
-
--- | Run checks only, don't execute.
-pDryRun :: Field
-pDryRun = optionalField $ booleanField "dry_run"
-
--- | Debug level.
-pDebugLevel :: Field
-pDebugLevel = optionalField $ simpleField "debug_level" [t| NonNegative Int |]
-
--- | Opcode priority. Note: python uses a separate constant, we're
--- using the actual value we know it's the default.
-pOpPriority :: Field
-pOpPriority =
-  defaultField [| OpPrioNormal |] $
-  simpleField "priority" [t| OpSubmitPriority |]
-
--- | Job dependencies.
-pDependencies :: Field
-pDependencies =
-  optionalNullSerField $ simpleField "depends" [t| [JobDependency] |]
-
--- | Comment field.
-pComment :: Field
-pComment = optionalNullSerField $ stringField "comment"
-
--- | Reason trail field.
-pReason :: Field
-pReason = simpleField C.opcodeReason [t| ReasonTrail |]
-
--- * Entire opcode parameter list
-
--- | Old-style query opcode, with locking.
-dOldQuery :: [Field]
-dOldQuery =
-  [ pOutputFields
-  , pNames
-  , pUseLocking
-  ]
-
--- | Old-style query opcode, without locking.
-dOldQueryNoLocking :: [Field]
-dOldQueryNoLocking =
-  [ pOutputFields
-  , pNames
-  ]
+pNetworkLink =
+  withDoc "Network link when connecting to a group" $
+  simpleField "network_link" [t| NonEmptyString |]
diff --git a/src/Ganeti/Parsers.hs b/src/Ganeti/Parsers.hs
new file mode 100644 (file)
index 0000000..5d14189
--- /dev/null
@@ -0,0 +1,50 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-| Utility functions for several parsers
+
+This module holds the definition for some utility functions for two
+parsers.  The parser for the @/proc/stat@ file and the parser for the
+@/proc/diskstats@ file.
+
+-}
+{-
+
+Copyright (C) 2013 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.Parsers where
+
+import Control.Applicative ((*>))
+import qualified Data.Attoparsec.Text as A
+import Data.Attoparsec.Text (Parser)
+import Data.Text (unpack)
+
+-- * Utility functions
+
+-- | Our own space-skipping function, because A.skipSpace also skips
+-- newline characters. It skips ZERO or more spaces, so it does not
+-- fail if there are no spaces.
+skipSpaces :: Parser ()
+skipSpaces = A.skipWhile A.isHorizontalSpace
+
+-- | A parser recognizing a number preceeded by spaces.
+numberP :: Parser Int
+numberP = skipSpaces *> A.decimal
+
+-- | A parser recognizing a word preceded by spaces, and closed by a space.
+stringP :: Parser String
+stringP = skipSpaces *> fmap unpack (A.takeWhile $ not . A.isHorizontalSpace)
index 0592cda..2c45afd 100644 (file)
@@ -43,7 +43,7 @@ module Ganeti.Path
 import System.FilePath
 import System.Posix.Env (getEnvDefault)
 
-import qualified Ganeti.Constants as C
+import AutoConf
 
 -- | Simple helper to concat two paths.
 pjoin :: IO String -> String -> IO String
@@ -64,7 +64,7 @@ addNodePrefix path = do
 
 -- | Directory for data.
 dataDir :: IO FilePath
-dataDir = addNodePrefix $ C.autoconfLocalstatedir </> "lib" </> "ganeti"
+dataDir = addNodePrefix $ AutoConf.localstatedir </> "lib" </> "ganeti"
 
 -- | Helper for building on top of dataDir (internal).
 dataDirP :: FilePath -> IO FilePath
@@ -72,11 +72,11 @@ dataDirP = (dataDir `pjoin`)
 
 -- | Directory for runtime files.
 runDir :: IO FilePath
-runDir = addNodePrefix $ C.autoconfLocalstatedir </> "run" </> "ganeti"
+runDir = addNodePrefix $ AutoConf.localstatedir </> "run" </> "ganeti"
 
 -- | Directory for log files.
 logDir :: IO FilePath
-logDir = addNodePrefix $ C.autoconfLocalstatedir </> "log" </> "ganeti"
+logDir = addNodePrefix $ AutoConf.localstatedir </> "log" </> "ganeti"
 
 -- | Directory for Unix sockets.
 socketDir :: IO FilePath
similarity index 88%
rename from src/Ganeti/Constants.hs.in
rename to src/Ganeti/PyConstants.hs.in
index 7a52a9d..92082f6 100644 (file)
@@ -2,13 +2,13 @@
 
 These are duplicated from the Python code. Note that this file is
 autogenerated using @autotools/convert_constants@ script with a header
-from @Constants.hs.in@.
+from @PyConstants.hs.in@.
 
 -}
 
 {-
 
-Copyright (C) 2009, 2010, 2011, 2012, 2013 Google Inc.
+Copyright (C) 2013 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,4 +27,4 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 
 -}
 
-module Ganeti.Constants where
+module Ganeti.PyConstants where
diff --git a/src/Ganeti/PyValueInstances.hs b/src/Ganeti/PyValueInstances.hs
new file mode 100644 (file)
index 0000000..b1ca816
--- /dev/null
@@ -0,0 +1,81 @@
+{-| PyValueInstances contains instances for the 'PyValue' typeclass.
+
+The typeclass 'PyValue' converts Haskell values to Python values.
+This module contains instances of this typeclass for several generic
+types.  These instances are used in the Haskell to Python generation
+of opcodes and constants, for example.
+
+-}
+
+{-
+
+Copyright (C) 2013 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.
+
+-}
+{-# LANGUAGE FlexibleInstances, OverlappingInstances,
+             TypeSynonymInstances, IncoherentInstances #-}
+{-# OPTIONS_GHC -fno-warn-orphans #-}
+module Ganeti.PyValueInstances where
+
+import Data.List (intercalate)
+import Data.Map (Map)
+import qualified Data.Map as Map
+import qualified Data.Set as Set (toList)
+
+import Ganeti.BasicTypes
+import Ganeti.THH
+
+instance PyValue Bool where
+  showValue = show
+
+instance PyValue Int where
+  showValue = show
+
+instance PyValue Integer where
+  showValue = show
+
+instance PyValue Double where
+  showValue = show
+
+instance PyValue Char where
+  showValue = show
+
+instance (PyValue a, PyValue b) => PyValue (a, b) where
+  showValue (x, y) = "(" ++ showValue x ++ "," ++ showValue y ++ ")"
+
+instance (PyValue a, PyValue b, PyValue c) => PyValue (a, b, c) where
+  showValue (x, y, z) =
+    "(" ++
+    showValue x ++ "," ++
+    showValue y ++ "," ++
+    showValue z ++
+    ")"
+
+instance PyValue String where
+  showValue = show
+
+instance PyValue a => PyValue [a] where
+  showValue xs = "[" ++ intercalate "," (map showValue xs) ++ "]"
+
+instance (PyValue k, PyValue a) => PyValue (Map k a) where
+  showValue mp =
+    "{" ++ intercalate ", " (map showPair (Map.assocs mp)) ++ "}"
+    where showPair (k, x) = showValue k ++ ":" ++ showValue x
+
+instance PyValue a => PyValue (ListSet a) where
+  showValue = showValue . Set.toList . unListSet
index 086ecad..f710d71 100644 (file)
@@ -37,7 +37,6 @@ module Ganeti.Query.Common
   , serialFields
   , tagsFields
   , dictFieldGetter
-  , buildQFTLookup
   , buildNdParamField
   ) where
 
@@ -51,6 +50,7 @@ import Ganeti.Objects
 import Ganeti.Rpc
 import Ganeti.Query.Language
 import Ganeti.Query.Types
+import Ganeti.Types
 
 -- * Generic functions
 
@@ -146,20 +146,13 @@ tagsFields =
 dictFieldGetter :: (DictObject a) => String -> Maybe a -> ResultEntry
 dictFieldGetter k = maybe rsNoData (rsMaybeNoData . lookup k . toDict)
 
--- | Build an optimised lookup map from a Python _PARAMETER_TYPES
--- association list.
-buildQFTLookup :: [(String, String)] -> Map.Map String FieldType
-buildQFTLookup =
-  Map.fromList .
-  map (\(k, v) -> (k, maybe QFTOther vTypeToQFT (vTypeFromRaw v)))
-
 -- | Ndparams optimised lookup map.
 ndParamTypes :: Map.Map String FieldType
-ndParamTypes = buildQFTLookup C.ndsParameterTypes
+ndParamTypes = Map.map vTypeToQFT C.ndsParameterTypes
 
 -- | Ndparams title map.
 ndParamTitles :: Map.Map String FieldTitle
-ndParamTitles = Map.fromList C.ndsParameterTitles
+ndParamTitles = C.ndsParameterTitles
 
 -- | Ndparam getter builder: given a field, it returns a FieldConfig
 -- getter, that is a function that takes the config and the object and
index 26ce552..e28c6aa 100644 (file)
@@ -36,6 +36,7 @@ import Control.Concurrent
 import Control.Exception
 import Control.Monad (forever)
 import Data.Bits (bitSize)
+import qualified Data.Set as Set (toList)
 import Data.IORef
 import qualified Network.Socket as S
 import qualified Text.JSON as J
@@ -43,6 +44,7 @@ import Text.JSON (showJSON, JSValue(..))
 import System.Info (arch)
 
 import qualified Ganeti.Constants as C
+import qualified Ganeti.ConstantUtils as ConstantUtils (unFrozenSet)
 import Ganeti.Errors
 import qualified Ganeti.Path as Path
 import Ganeti.Daemon
@@ -52,11 +54,11 @@ import Ganeti.ConfigReader
 import Ganeti.BasicTypes
 import Ganeti.Logging
 import Ganeti.Luxi
-import Ganeti.OpCodes (TagObject(..))
 import qualified Ganeti.Query.Language as Qlang
 import qualified Ganeti.Query.Cluster as QCluster
 import Ganeti.Query.Query
 import Ganeti.Query.Filter (makeSimpleFilter)
+import Ganeti.Types
 
 -- | Helper for classic queries.
 handleClassicQuery :: ConfigData      -- ^ Cluster config
@@ -96,7 +98,9 @@ handleCall cdata QueryClusterInfo =
       obj = [ ("software_version", showJSON C.releaseVersion)
             , ("protocol_version", showJSON C.protocolVersion)
             , ("config_version", showJSON C.configVersion)
-            , ("os_api_version", showJSON $ maximum C.osApiVersions)
+            , ("os_api_version", showJSON . maximum .
+                                 Set.toList . ConstantUtils.unFrozenSet $
+                                 C.osApiVersions)
             , ("export_version", showJSON C.exportVersion)
             , ("vcs_version", showJSON C.vcsVersion)
             , ("architecture", showJSON arch_tuple)
@@ -150,13 +154,16 @@ handleCall cdata QueryClusterInfo =
     Ok _ -> return . Ok . J.makeObj $ obj
     Bad ex -> return $ Bad ex
 
-handleCall cfg (QueryTags kind) =
+handleCall cfg (QueryTags kind name) = do
   let tags = case kind of
-               TagCluster       -> Ok . clusterTags $ configCluster cfg
-               TagGroup    name -> groupTags <$> Config.getGroup    cfg name
-               TagNode     name -> nodeTags  <$> Config.getNode     cfg name
-               TagInstance name -> instTags  <$> Config.getInstance cfg name
-  in return (J.showJSON <$> tags)
+               TagKindCluster  -> Ok . clusterTags $ configCluster cfg
+               TagKindGroup    -> groupTags <$> Config.getGroup    cfg name
+               TagKindNode     -> nodeTags  <$> Config.getNode     cfg name
+               TagKindInstance -> instTags  <$> Config.getInstance cfg name
+               TagKindNetwork  -> Bad $ OpPrereqError
+                                        "Network tag is not allowed"
+                                        ECodeInval
+  return (J.showJSON <$> tags)
 
 handleCall cfg (Query qkind qfields qfilter) = do
   result <- query cfg True (Qlang.Query qkind qfields qfilter)
index 192ea7b..d69c9b7 100644 (file)
@@ -60,7 +60,6 @@ module Ganeti.Rpc
   , RpcCallVersion(..)
   , RpcResultVersion(..)
 
-  , StorageField(..)
   , RpcCallStorageList(..)
   , RpcResultStorageList(..)
 
@@ -69,8 +68,6 @@ module Ganeti.Rpc
 
   , RpcCallExportList(..)
   , RpcResultExportList(..)
-
-  , rpcTimeoutFromRaw -- FIXME: Not used anywhere
   ) where
 
 import Control.Arrow (second)
@@ -124,16 +121,6 @@ explainRpcError OfflineNodeError =
 
 type ERpcError = Either RpcError
 
--- | Basic timeouts for RPC calls.
-$(declareIADT "RpcTimeout"
-  [ ( "Urgent",    'C.rpcTmoUrgent )
-  , ( "Fast",      'C.rpcTmoFast )
-  , ( "Normal",    'C.rpcTmoNormal )
-  , ( "Slow",      'C.rpcTmoSlow )
-  , ( "FourHours", 'C.rpcTmo4hrs )
-  , ( "OneDay",    'C.rpcTmo1day )
-  ])
-
 -- | A generic class for RPC calls.
 class (J.JSON a) => RpcCall a where
   -- | Give the (Python) name of the procedure.
@@ -162,7 +149,7 @@ data HttpClientRequest = HttpClientRequest
 prepareUrl :: (RpcCall a) => Node -> a -> String
 prepareUrl node call =
   let node_ip = nodePrimaryIp node
-      port = snd C.daemonsPortsGanetiNoded
+      port = C.defaultNodedPort
       path_prefix = "https://" ++ node_ip ++ ":" ++ show port
   in path_prefix ++ "/" ++ rpcCallName call
 
@@ -414,18 +401,6 @@ instance Rpc RpcCallVersion RpcResultVersion where
 
 -- ** StorageList
 
--- | StorageList
-
--- FIXME: This may be moved to Objects
-$(declareSADT "StorageField"
-  [ ( "SFUsed",        'C.sfUsed)
-  , ( "SFName",        'C.sfName)
-  , ( "SFAllocatable", 'C.sfAllocatable)
-  , ( "SFFree",        'C.sfFree)
-  , ( "SFSize",        'C.sfSize)
-  ])
-$(makeJSONInstance ''StorageField)
-
 $(buildObject "RpcCallStorageList" "rpcCallStorageList"
   [ simpleField "su_name" [t| StorageType |]
   , simpleField "su_args" [t| [String] |]
index bf24e94..33da35e 100644 (file)
@@ -30,9 +30,13 @@ module Ganeti.Runtime
   , RuntimeEnts
   , daemonName
   , daemonOnlyOnMaster
+  , daemonLogBase
   , daemonUser
   , daemonGroup
+  , ExtraLogReason(..)
   , daemonLogFile
+  , daemonsExtraLogbase
+  , daemonsExtraLogFile
   , daemonPidFile
   , getEnts
   , verifyDaemonUser
@@ -49,10 +53,12 @@ import System.Posix.Types
 import System.Posix.User
 import Text.Printf
 
-import qualified Ganeti.Constants as C
+import qualified Ganeti.ConstantUtils as ConstantUtils
 import qualified Ganeti.Path as Path
 import Ganeti.BasicTypes
 
+import AutoConf
+
 data GanetiDaemon = GanetiMasterd
                   | GanetiNoded
                   | GanetiRapi
@@ -73,12 +79,12 @@ 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
-daemonName GanetiLuxid   = C.luxid
-daemonName GanetiMond    = C.mond
+daemonName GanetiMasterd = "ganeti-masterd"
+daemonName GanetiNoded   = "ganeti-noded"
+daemonName GanetiRapi    = "ganeti-rapi"
+daemonName GanetiConfd   = "ganeti-confd"
+daemonName GanetiLuxid   = "ganeti-luxid"
+daemonName GanetiMond    = "ganeti-mond"
 
 -- | Returns whether the daemon only runs on the master node.
 daemonOnlyOnMaster :: GanetiDaemon -> Bool
@@ -91,32 +97,41 @@ daemonOnlyOnMaster GanetiMond    = False
 
 -- | Returns the log file base for a daemon.
 daemonLogBase :: GanetiDaemon -> String
-daemonLogBase GanetiMasterd = C.daemonsLogbaseGanetiMasterd
-daemonLogBase GanetiNoded   = C.daemonsLogbaseGanetiNoded
-daemonLogBase GanetiRapi    = C.daemonsLogbaseGanetiRapi
-daemonLogBase GanetiConfd   = C.daemonsLogbaseGanetiConfd
-daemonLogBase GanetiLuxid   = C.daemonsLogbaseGanetiLuxid
-daemonLogBase GanetiMond    = C.daemonsLogbaseGanetiMond
+daemonLogBase GanetiMasterd = "master-daemon"
+daemonLogBase GanetiNoded   = "node-daemon"
+daemonLogBase GanetiRapi    = "rapi-daemon"
+daemonLogBase GanetiConfd   = "conf-daemon"
+daemonLogBase GanetiLuxid   = "luxi-daemon"
+daemonLogBase GanetiMond    = "monitoring-daemon"
 
 -- | 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
-daemonUser GanetiLuxid   = C.luxidUser
-daemonUser GanetiMond    = C.mondUser
+daemonUser GanetiMasterd = AutoConf.masterdUser
+daemonUser GanetiNoded   = AutoConf.nodedUser
+daemonUser GanetiRapi    = AutoConf.rapiUser
+daemonUser GanetiConfd   = AutoConf.confdUser
+daemonUser GanetiLuxid   = AutoConf.luxidUser
+daemonUser GanetiMond    = AutoConf.mondUser
 
 -- | 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 (DaemonGroup GanetiLuxid)   = C.luxidGroup
-daemonGroup (DaemonGroup GanetiMond)    = C.mondGroup
-daemonGroup (ExtraGroup  DaemonsGroup)  = C.daemonsGroup
-daemonGroup (ExtraGroup  AdminGroup)    = C.adminGroup
+daemonGroup (DaemonGroup GanetiMasterd) = AutoConf.masterdGroup
+daemonGroup (DaemonGroup GanetiNoded)   = AutoConf.nodedGroup
+daemonGroup (DaemonGroup GanetiRapi)    = AutoConf.rapiGroup
+daemonGroup (DaemonGroup GanetiConfd)   = AutoConf.confdGroup
+daemonGroup (DaemonGroup GanetiLuxid)   = AutoConf.luxidGroup
+daemonGroup (DaemonGroup GanetiMond)    = AutoConf.mondGroup
+daemonGroup (ExtraGroup  DaemonsGroup)  = AutoConf.daemonsGroup
+daemonGroup (ExtraGroup  AdminGroup)    = AutoConf.adminGroup
+
+data ExtraLogReason = AccessLog | ErrorLog
+
+-- | Some daemons might require more than one logfile.  Specifically,
+-- right now only the Haskell http library "snap", used by the
+-- monitoring daemon, requires multiple log files.
+daemonsExtraLogbase :: GanetiDaemon -> ExtraLogReason -> String
+daemonsExtraLogbase daemon AccessLog = daemonLogBase daemon ++ "-access"
+daemonsExtraLogbase daemon ErrorLog = daemonLogBase daemon ++ "-error"
 
 -- | Returns the log file for a daemon.
 daemonLogFile :: GanetiDaemon -> IO FilePath
@@ -124,6 +139,12 @@ daemonLogFile daemon = do
   logDir <- Path.logDir
   return $ logDir </> daemonLogBase daemon <.> "log"
 
+-- | Returns the extra log files for a daemon.
+daemonsExtraLogFile :: GanetiDaemon -> ExtraLogReason -> IO FilePath
+daemonsExtraLogFile daemon logreason = do
+  logDir <- Path.logDir
+  return $ logDir </> daemonsExtraLogbase daemon logreason <.> "log"
+
 -- | Returns the pid file name for a daemon.
 daemonPidFile :: GanetiDaemon -> IO FilePath
 daemonPidFile daemon = do
@@ -182,4 +203,4 @@ checkUidMatch name expected actual =
                    \expected %d\n" name
               (fromIntegral actual::Int)
               (fromIntegral expected::Int) :: IO ()
-    exitWith $ ExitFailure C.exitFailure
+    exitWith $ ExitFailure ConstantUtils.exitFailure
index 1199f32..4f88ef3 100644 (file)
@@ -27,38 +27,37 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 -}
 module Ganeti.Storage.Diskstats.Parser (diskstatsParser) where
 
-import Control.Applicative ((<*>), (*>), (<*), (<$>))
+import Control.Applicative ((<*>), (<*), (<$>))
 import qualified Data.Attoparsec.Text as A
 import qualified Data.Attoparsec.Combinator as AC
 import Data.Attoparsec.Text (Parser)
-import Data.Text (unpack)
 
+import Ganeti.Parsers
 import Ganeti.Storage.Diskstats.Types
 
--- * Utility functions
-
--- | Our own space-skipping function, because A.skipSpace also skips
--- newline characters. It skips ZERO or more spaces, so it does not
--- fail if there are no spaces.
-skipSpaces :: Parser ()
-skipSpaces = A.skipWhile A.isHorizontalSpace
-
--- | A parser recognizing a number preceeded by spaces.
-numberP :: Parser Int
-numberP = skipSpaces *> A.decimal
-
--- | A parser recognizing a word preceded by spaces, and closed by a space.
-stringP :: Parser String
-stringP = skipSpaces *> fmap unpack (A.takeWhile $ not . A.isHorizontalSpace)
-
 -- * Parser implementation
 
 -- | The parser for one line of the diskstatus file.
 oneDiskstatsParser :: Parser Diskstats
 oneDiskstatsParser =
-  Diskstats <$> numberP <*> numberP <*> stringP <*> numberP <*> numberP
-    <*> numberP <*> numberP <*> numberP <*> numberP <*> numberP <*> numberP
-    <*> numberP <*> numberP <*> numberP <* A.endOfLine
+  let majorP = numberP
+      minorP = numberP
+      nameP = stringP
+      readsNumP = numberP
+      mergedReadsP = numberP
+      secReadP = numberP
+      timeReadP = numberP
+      writesP = numberP
+      mergedWritesP = numberP
+      secWrittenP = numberP
+      timeWriteP = numberP
+      iosP = numberP
+      timeIOP = numberP
+      wIOmillisP = numberP
+  in
+    Diskstats <$> majorP <*> minorP <*> nameP <*> readsNumP <*> mergedReadsP
+      <*> secReadP <*> timeReadP <*> writesP <*> mergedWritesP <*> secWrittenP
+      <*> timeWriteP <*> iosP <*> timeIOP <*> wIOmillisP <* A.endOfLine
 
 -- | The parser for a whole diskstatus file.
 diskstatsParser :: Parser [Diskstats]
index aa14642..31c6123 100644 (file)
@@ -1,4 +1,4 @@
-{-# LANGUAGE TemplateHaskell #-}
+{-# LANGUAGE ExistentialQuantification, ParallelListComp, TemplateHaskell #-}
 
 {-| TemplateHaskell helper for Ganeti Haskell code.
 
@@ -30,17 +30,24 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 -}
 
 module Ganeti.THH ( declareSADT
+                  , declareLADT
+                  , declareILADT
                   , declareIADT
                   , makeJSONInstance
+                  , deCamelCase
                   , genOpID
                   , genAllConstr
                   , genAllOpIDs
+                  , PyValue(..)
+                  , PyValueEx(..)
+                  , OpCodeDescriptor
                   , genOpCode
                   , genStrOfOp
                   , genStrOfKey
                   , genLuxiOp
-                  , Field
+                  , Field (..)
                   , simpleField
+                  , withDoc
                   , defaultField
                   , optionalField
                   , optionalNullSerField
@@ -62,7 +69,6 @@ module Ganeti.THH ( declareSADT
 import Control.Monad (liftM)
 import Data.Char
 import Data.List
-import Data.Maybe (fromMaybe)
 import qualified Data.Set as Set
 import Language.Haskell.TH
 
@@ -71,6 +77,9 @@ import Text.JSON.Pretty (pp_value)
 
 import Ganeti.JSON
 
+import Data.Maybe
+import Data.Functor ((<$>))
+
 -- * Exported types
 
 -- | Class of objects that can be converted to 'JSObject'
@@ -94,6 +103,7 @@ data Field = Field { fieldName        :: String
                    , fieldDefault     :: Maybe (Q Exp)
                    , fieldConstr      :: Maybe String
                    , fieldIsOptional  :: OptionalType
+                   , fieldDoc         :: String
                    }
 
 -- | Generates a simple field.
@@ -107,8 +117,13 @@ simpleField fname ftype =
         , fieldDefault     = Nothing
         , fieldConstr      = Nothing
         , fieldIsOptional  = NotOptional
+        , fieldDoc         = ""
         }
 
+withDoc :: String -> Field -> Field
+withDoc doc field =
+  field { fieldDoc = doc }
+
 -- | Sets the renamed constructor field.
 renameField :: String -> Field -> Field
 renameField constrName field = field { fieldConstr = Just constrName }
@@ -222,8 +237,11 @@ type SimpleConstructor = (String, [SimpleField])
 -- | A definition for ADTs with simple fields.
 type SimpleObject = [SimpleConstructor]
 
--- | A type alias for a constructor of a regular object.
-type Constructor = (String, [Field])
+-- | A type alias for an opcode constructor of a regular object.
+type OpCodeConstructor = (String, Q Type, String, [Field], String)
+
+-- | A type alias for a Luxi constructor of a regular object.
+type LuxiConstructor = (String, [Field])
 
 -- * Helper functions
 
@@ -344,7 +362,7 @@ genToRaw traw fname tname constructors = do
 --               | s == \"value2\" = Cons2
 --               | otherwise = fail /.../
 -- @
-genFromRaw :: Name -> Name -> Name -> [(String, Name)] -> Q [Dec]
+genFromRaw :: Name -> Name -> Name -> [(String, Either 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) |]
@@ -353,7 +371,7 @@ genFromRaw traw fname tname constructors = do
       varpe = varE varp
   clauses <- mapM (\(c, v) -> do
                      -- the clause match condition
-                     g <- normalG [| $varpe == $(varE v) |]
+                     g <- normalG [| $varpe == $(reprE v) |]
                      -- the clause result
                      r <- [| return $(conE (mkName c)) |]
                      return (g, r)) constructors
@@ -383,21 +401,38 @@ genFromRaw traw fname tname constructors = do
 --
 -- 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
+declareADT
+  :: (a -> Either String Name) -> Name -> String -> [(String, a)] -> Q [Dec]
+declareADT fn 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
+      cons' = map (\(a, b) -> (a, fn b)) cons
   toraw <- genToRaw traw (toRawName sname) name cons'
-  fromraw <- genFromRaw traw (fromRawName sname) name cons
+  fromraw <- genFromRaw traw (fromRawName sname) name cons'
   return $ ddecl:toraw ++ fromraw
 
+declareLADT :: Name -> String -> [(String, String)] -> Q [Dec]
+declareLADT = declareADT Left
+
+declareILADT :: String -> [(String, Int)] -> Q [Dec]
+declareILADT sname cons = do
+  consNames <- sequence [ newName ('_':n) | (n, _) <- cons ]
+  consFns <- concat <$> sequence
+             [ do sig <- sigD n [t| Int |]
+                  let expr = litE (IntegerL (toInteger i))
+                  fn <- funD n [clause [] (normalB expr) []]
+                  return [sig, fn]
+             | n <- consNames
+             | (_, i) <- cons ]
+  let cons' = [ (n, n') | (n, _) <- cons | n' <- consNames ]
+  (consFns ++) <$> declareADT Right ''Int sname cons'
+
 declareIADT :: String -> [(String, Name)] -> Q [Dec]
-declareIADT = declareADT ''Int
+declareIADT = declareADT Right ''Int
 
 declareSADT :: String -> [(String, Name)] -> Q [Dec]
-declareSADT = declareADT ''String
+declareSADT = declareADT Right ''String
 
 -- | Creates the showJSON member of a JSON instance declaration.
 --
@@ -520,42 +555,207 @@ genAllOpIDs = genAllConstr deCamelCase
 -- | OpCode parameter (field) type.
 type OpParam = (String, Q Type, Q Exp)
 
+-- * Python code generation
+
+-- | Converts Haskell values into Python values
+--
+-- This is necessary for the default values of opcode parameters and
+-- return values.  For example, if a default value or return type is a
+-- Data.Map, then it must be shown as a Python dictioanry.
+class PyValue a where
+  showValue :: a -> String
+
+-- | Encapsulates Python default values
+data PyValueEx = forall a. PyValue a => PyValueEx a
+
+instance PyValue PyValueEx where
+  showValue (PyValueEx x) = showValue x
+
+-- | Transfers opcode data between the opcode description (through
+-- @genOpCode@) and the Python code generation functions.
+type OpCodeDescriptor =
+  (String, String, String, [String],
+   [String], [Maybe PyValueEx], [String], String)
+
+-- | Strips out the module name
+--
+-- @
+-- pyBaseName "Data.Map" = "Map"
+-- @
+pyBaseName :: String -> String
+pyBaseName str =
+  case span (/= '.') str of
+    (x, []) -> x
+    (_, _:x) -> pyBaseName x
+
+-- | Converts a Haskell type name into a Python type name.
+--
+-- @
+-- pyTypename "Bool" = "ht.TBool"
+-- @
+pyTypeName :: Show a => a -> String
+pyTypeName name =
+  "ht.T" ++ (case pyBaseName (show name) of
+                "()" -> "None"
+                "Map" -> "DictOf"
+                "Set" -> "SetOf"
+                "ListSet" -> "SetOf"
+                "Either" -> "Or"
+                "GenericContainer" -> "DictOf"
+                "JSValue" -> "Any"
+                "JSObject" -> "Object"
+                str -> str)
+
+-- | Converts a Haskell type into a Python type.
+--
+-- @
+-- pyType [Int] = "ht.TListOf(ht.TInt)"
+-- @
+pyType :: Type -> Q String
+pyType (AppT typ1 typ2) =
+  do t <- pyCall typ1 typ2
+     return $ t ++ ")"
+
+pyType (ConT name) = return (pyTypeName name)
+pyType ListT = return "ht.TListOf"
+pyType (TupleT 0) = return "ht.TNone"
+pyType (TupleT _) = return "ht.TTupleOf"
+pyType typ = error $ "unhandled case for type " ++ show typ
+        
+-- | Converts a Haskell type application into a Python type.
+--
+-- @
+-- Maybe Int = "ht.TMaybe(ht.TInt)"
+-- @
+pyCall :: Type -> Type -> Q String
+pyCall (AppT typ1 typ2) arg =
+  do t <- pyCall typ1 typ2
+     targ <- pyType arg
+     return $ t ++ ", " ++ targ
+
+pyCall typ1 typ2 =
+  do t1 <- pyType typ1
+     t2 <- pyType typ2
+     return $ t1 ++ "(" ++ t2
+
+-- | @pyType opt typ@ converts Haskell type @typ@ into a Python type,
+-- where @opt@ determines if the converted type is optional (i.e.,
+-- Maybe).
+--
+-- @
+-- pyType False [Int] = "ht.TListOf(ht.TInt)" (mandatory)
+-- pyType True [Int] = "ht.TMaybe(ht.TListOf(ht.TInt))" (optional)
+-- @
+pyOptionalType :: Bool -> Type -> Q String
+pyOptionalType opt typ
+  | opt = do t <- pyType typ
+             return $ "ht.TMaybe(" ++ t ++ ")"
+  | otherwise = pyType typ
+
+-- | Optionally encapsulates default values in @PyValueEx@.
+--
+-- @maybeApp exp typ@ returns a quoted expression that encapsulates
+-- the default value @exp@ of an opcode parameter cast to @typ@ in a
+-- @PyValueEx@, if @exp@ is @Just@.  Otherwise, it returns a quoted
+-- expression with @Nothing@.
+maybeApp :: Maybe (Q Exp) -> Q Type -> Q Exp
+maybeApp Nothing _ =
+  [| Nothing |]
+
+maybeApp (Just expr) typ =
+  [| Just ($(conE (mkName "PyValueEx")) ($expr :: $typ)) |]
+
+
+-- | Generates a Python type according to whether the field is
+-- optional
+genPyType :: OptionalType -> Q Type -> Q ExpQ
+genPyType opt typ =
+  do t <- typ
+     stringE <$> pyOptionalType (opt /= NotOptional) t
+
+-- | Generates Python types from opcode parameters.
+genPyTypes :: [Field] -> Q ExpQ
+genPyTypes fs =
+  listE <$> mapM (\f -> genPyType (fieldIsOptional f) (fieldType f)) fs
+
+-- | Generates Python default values from opcode parameters.
+genPyDefaults :: [Field] -> ExpQ
+genPyDefaults fs =
+  listE $ map (\f -> maybeApp (fieldDefault f) (fieldType f)) fs
+
+-- | Generates a Haskell function call to "showPyClass" with the
+-- necessary information on how to build the Python class string.
+pyClass :: OpCodeConstructor -> ExpQ
+pyClass (consName, consType, consDoc, consFields, consDscField) =
+  do let pyClassVar = varNameE "showPyClass"
+         consName' = stringE consName
+     consType' <- genPyType NotOptional consType
+     let consDoc' = stringE consDoc
+         consFieldNames = listE $ map (stringE . fieldName) consFields
+         consFieldDocs = listE $ map (stringE . fieldDoc) consFields
+     consFieldTypes <- genPyTypes consFields
+     let consFieldDefaults = genPyDefaults consFields
+     [| ($consName',
+         $consType',
+         $consDoc',
+         $consFieldNames,
+         $consFieldTypes,
+         $consFieldDefaults,
+         $consFieldDocs,
+         consDscField) |]
+
+-- | Generates a function called "pyClasses" that holds the list of
+-- all the opcode descriptors necessary for generating the Python
+-- opcodes.
+pyClasses :: [OpCodeConstructor] -> Q [Dec]
+pyClasses cons =
+  do let name = mkName "pyClasses"
+         sig = SigD name (AppT ListT (ConT ''OpCodeDescriptor))
+     fn <- FunD name <$> (:[]) <$> declClause cons
+     return [sig, fn]
+  where declClause c =
+          clause [] (normalB (ListE <$> mapM pyClass c)) []
+
+-- | Converts from an opcode constructor to a Luxi constructor.
+opcodeConsToLuxiCons :: (a, b, c, d, e) -> (a, d)
+opcodeConsToLuxiCons (x, _, _, y, _) = (x, y)
+
 -- | 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
-          -> [Constructor] -- ^ Constructor name and parameters
+genOpCode :: String              -- ^ Type name to use
+          -> [OpCodeConstructor] -- ^ Constructor name and parameters
           -> Q [Dec]
 genOpCode name cons = do
   let tname = mkName name
-  decl_d <- mapM (\(cname, fields) -> do
+  decl_d <- mapM (\(cname, _, _, fields, _) -> do
                     -- we only need the type of the field, without Q
                     fields' <- mapM (fieldTypeInfo "op") fields
                     return $ RecC (mkName cname) fields')
             cons
   let declD = DataD [] tname [] decl_d [''Show, ''Eq]
-
   let (allfsig, allffn) = genAllOpFields "allOpFields" cons
   save_decs <- genSaveOpCode tname "saveOpCode" "toDictOpCode"
-               cons (uncurry saveConstructor) True
+               (map opcodeConsToLuxiCons cons) saveConstructor True
   (loadsig, loadfn) <- genLoadOpCode cons
-  return $ [declD, allfsig, allffn, loadsig, loadfn] ++ save_decs
+  pyDecls <- pyClasses cons
+  return $ [declD, allfsig, allffn, loadsig, loadfn] ++ save_decs ++ pyDecls
 
 -- | Generates the function pattern returning the list of fields for a
 -- given constructor.
-genOpConsFields :: Constructor -> Clause
-genOpConsFields (cname, fields) =
+genOpConsFields :: OpCodeConstructor -> Clause
+genOpConsFields (cname, _, _, fields, _) =
   let op_id = deCamelCase cname
       fvals = map (LitE . StringL) . sort . nub $
               concatMap (\f -> fieldName f:fieldExtraKeys f) fields
   in Clause [LitP (StringL op_id)] (NormalB $ ListE fvals) []
 
 -- | Generates a list of all fields of an opcode constructor.
-genAllOpFields  :: String        -- ^ Function name
-                -> [Constructor] -- ^ Object definition
+genAllOpFields  :: String              -- ^ Function name
+                -> [OpCodeConstructor] -- ^ Object definition
                 -> (Dec, Dec)
 genAllOpFields sname opdefs =
   let cclauses = map genOpConsFields opdefs
@@ -569,11 +769,9 @@ genAllOpFields sname opdefs =
 -- 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
+saveConstructor :: LuxiConstructor -- ^ The constructor
+                -> Q Clause        -- ^ Resulting clause
+saveConstructor (sname, fields) = do
   let cname = mkName sname
   fnames <- mapM (newName . fieldVariable) fields
   let pat = conP cname (map varP fnames)
@@ -590,14 +788,14 @@ saveConstructor sname fields = do
 --
 -- This builds a per-constructor match clause that contains the
 -- respective constructor-serialisation code.
-genSaveOpCode :: Name                      -- ^ Object ype
-              -> String                    -- ^ To 'JSValue' function name
-              -> String                    -- ^ To 'JSObject' function name
-              -> [Constructor]             -- ^ Object definition
-              -> (Constructor -> Q Clause) -- ^ Constructor save fn
-              -> Bool                      -- ^ Whether to generate
-                                           -- obj or just a
-                                           -- list\/tuple of values
+genSaveOpCode :: Name                          -- ^ Object ype
+              -> String                        -- ^ To 'JSValue' function name
+              -> String                        -- ^ To 'JSObject' function name
+              -> [LuxiConstructor]             -- ^ Object definition
+              -> (LuxiConstructor -> Q Clause) -- ^ Constructor save fn
+              -> Bool                          -- ^ Whether to generate
+                                               -- obj or just a
+                                               -- list\/tuple of values
               -> Q [Dec]
 genSaveOpCode tname jvalstr tdstr opdefs fn gen_object = do
   tdclauses <- mapM fn opdefs
@@ -615,8 +813,8 @@ genSaveOpCode tname jvalstr tdstr opdefs fn gen_object = do
          , ValD (VarP jvalname) (NormalB jvalclause) []]
 
 -- | Generates load code for a single constructor of the opcode data type.
-loadConstructor :: String -> [Field] -> Q Exp
-loadConstructor sname fields = do
+loadConstructor :: OpCodeConstructor -> Q Exp
+loadConstructor (sname, _, _, fields, _) = do
   let name = mkName sname
   fbinds <- mapM loadObjectField fields
   let (fnames, fstmts) = unzip fbinds
@@ -625,7 +823,7 @@ loadConstructor sname fields = do
   return $ DoE fstmts'
 
 -- | Generates the loadOpCode function.
-genLoadOpCode :: [Constructor] -> Q (Dec, Dec)
+genLoadOpCode :: [OpCodeConstructor] -> Q (Dec, Dec)
 genLoadOpCode opdefs = do
   let fname = mkName "loadOpCode"
       arg1 = mkName "v"
@@ -635,10 +833,10 @@ genLoadOpCode opdefs = do
                                  (JSON.readJSON $(varE arg1)) |]
   st2 <- bindS (varP opid) [| $fromObjE $(varE objname) $(stringE "OP_ID") |]
   -- the match results (per-constructor blocks)
-  mexps <- mapM (uncurry loadConstructor) opdefs
+  mexps <- mapM loadConstructor opdefs
   fails <- [| fail $ "Unknown opcode " ++ $(varE opid) |]
-  let mpats = map (\(me, c) ->
-                       let mp = LitP . StringL . deCamelCase . fst $ c
+  let mpats = map (\(me, (consName, _, _, _, _)) ->
+                       let mp = LitP . StringL . deCamelCase $ consName
                        in Match mp (NormalB me) []
                   ) $ zip mexps opdefs
       defmatch = Match WildP (NormalB fails) []
@@ -670,7 +868,7 @@ genStrOfKey = genConstrToStr ensureLower
 --
 -- * type
 --
-genLuxiOp :: String -> [Constructor] -> Q [Dec]
+genLuxiOp :: String -> [LuxiConstructor] -> Q [Dec]
 genLuxiOp name cons = do
   let tname = mkName name
   decl_d <- mapM (\(cname, fields) -> do
@@ -688,7 +886,7 @@ genLuxiOp name cons = do
   return $ declD:save_decs ++ req_defs
 
 -- | Generates the \"save\" clause for entire LuxiOp constructor.
-saveLuxiConstructor :: Constructor -> Q Clause
+saveLuxiConstructor :: LuxiConstructor -> Q Clause
 saveLuxiConstructor (sname, fields) = do
   let cname = mkName sname
   fnames <- mapM (newName . fieldVariable) fields
index 70de831..54b118f 100644 (file)
@@ -40,6 +40,9 @@ module Ganeti.Types
   , DiskTemplate(..)
   , diskTemplateToRaw
   , diskTemplateFromRaw
+  , TagKind(..)
+  , tagKindToRaw
+  , tagKindFromRaw
   , NonNegative
   , fromNonNegative
   , mkNonNegative
@@ -53,23 +56,44 @@ module Ganeti.Types
   , fromNonEmpty
   , mkNonEmpty
   , NonEmptyString
+  , QueryResultCode
+  , IPv4Address
+  , mkIPv4Address
+  , IPv4Network
+  , mkIPv4Network
+  , IPv6Address
+  , mkIPv6Address
+  , IPv6Network
+  , mkIPv6Network
   , MigrationMode(..)
+  , migrationModeToRaw
   , VerifyOptionalChecks(..)
+  , verifyOptionalChecksToRaw
   , DdmSimple(..)
   , DdmFull(..)
+  , ddmFullToRaw
   , CVErrorCode(..)
   , cVErrorCodeToRaw
   , Hypervisor(..)
   , hypervisorToRaw
   , OobCommand(..)
+  , oobCommandToRaw
+  , OobStatus(..)
+  , oobStatusToRaw
   , StorageType(..)
   , storageTypeToRaw
-  , NodeEvacMode(..)
+  , EvacMode(..)
+  , evacModeToRaw
   , FileDriver(..)
+  , fileDriverToRaw
   , InstCreateMode(..)
+  , instCreateModeToRaw
   , RebootType(..)
+  , rebootTypeToRaw
   , ExportMode(..)
+  , exportModeToRaw
   , IAllocatorTestDir(..)
+  , iAllocatorTestDirToRaw
   , IAllocatorMode(..)
   , iAllocatorModeToRaw
   , NICMode(..)
@@ -94,6 +118,7 @@ module Ganeti.Types
   , opStatusToRaw
   , opStatusFromRaw
   , ELogType(..)
+  , eLogTypeToRaw
   , ReasonElem
   , ReasonTrail
   , StorageUnit(..)
@@ -101,6 +126,36 @@ module Ganeti.Types
   , StorageKey
   , addParamsToStorageUnit
   , diskTemplateToStorageType
+  , VType(..)
+  , vTypeFromRaw
+  , vTypeToRaw
+  , NodeRole(..)
+  , nodeRoleToRaw
+  , roleDescription
+  , DiskMode(..)
+  , diskModeToRaw
+  , BlockDriver(..)
+  , blockDriverToRaw
+  , AdminState(..)
+  , adminStateFromRaw
+  , adminStateToRaw
+  , StorageField(..)
+  , storageFieldToRaw
+  , DiskAccessMode(..)
+  , diskAccessModeToRaw
+  , LocalDiskStatus(..)
+  , localDiskStatusFromRaw
+  , localDiskStatusToRaw
+  , localDiskStatusName
+  , ReplaceDisksMode(..)
+  , replaceDisksModeToRaw
+  , RpcTimeout(..)
+  , rpcTimeoutFromRaw -- FIXME: no used anywhere
+  , rpcTimeoutToRaw
+  , HotplugTarget(..)
+  , hotplugTargetToRaw
+  , HotplugAction(..)
+  , hotplugActionToRaw
   ) where
 
 import Control.Monad (liftM)
@@ -108,9 +163,9 @@ import qualified Text.JSON as JSON
 import Text.JSON (JSON, readJSON, showJSON)
 import Data.Ratio (numerator, denominator)
 
-import qualified Ganeti.Constants as C
-import qualified Ganeti.THH as THH
+import qualified Ganeti.ConstantUtils as ConstantUtils
 import Ganeti.JSON
+import qualified Ganeti.THH as THH
 import Ganeti.Utils
 
 -- * Generic types
@@ -166,6 +221,10 @@ mkNonEmpty :: (Monad m) => [a] -> m (NonEmpty a)
 mkNonEmpty [] = fail "Received empty value for non-empty list"
 mkNonEmpty xs = return (NonEmpty xs)
 
+instance (Eq a, Ord a) => Ord (NonEmpty a) where
+  NonEmpty { fromNonEmpty = x1 } `compare` NonEmpty { fromNonEmpty = x2 } =
+    x1 `compare` x2
+
 instance (JSON.JSON a) => JSON.JSON (NonEmpty a) where
   showJSON = JSON.showJSON . fromNonEmpty
   readJSON v = JSON.readJSON v >>= mkNonEmpty
@@ -173,150 +232,231 @@ instance (JSON.JSON a) => JSON.JSON (NonEmpty a) where
 -- | A simple type alias for non-empty strings.
 type NonEmptyString = NonEmpty Char
 
+type QueryResultCode = Int
+
+newtype IPv4Address = IPv4Address { fromIPv4Address :: String }
+  deriving (Show, Eq)
+
+-- FIXME: this should check that 'address' is a valid ip
+mkIPv4Address :: Monad m => String -> m IPv4Address
+mkIPv4Address address =
+  return IPv4Address { fromIPv4Address = address }
+
+instance JSON.JSON IPv4Address where
+  showJSON = JSON.showJSON . fromIPv4Address
+  readJSON v = JSON.readJSON v >>= mkIPv4Address
+
+newtype IPv4Network = IPv4Network { fromIPv4Network :: String }
+  deriving (Show, Eq)
+
+-- FIXME: this should check that 'address' is a valid ip
+mkIPv4Network :: Monad m => String -> m IPv4Network
+mkIPv4Network address =
+  return IPv4Network { fromIPv4Network = address }
+
+instance JSON.JSON IPv4Network where
+  showJSON = JSON.showJSON . fromIPv4Network
+  readJSON v = JSON.readJSON v >>= mkIPv4Network
+
+newtype IPv6Address = IPv6Address { fromIPv6Address :: String }
+  deriving (Show, Eq)
+
+-- FIXME: this should check that 'address' is a valid ip
+mkIPv6Address :: Monad m => String -> m IPv6Address
+mkIPv6Address address =
+  return IPv6Address { fromIPv6Address = address }
+
+instance JSON.JSON IPv6Address where
+  showJSON = JSON.showJSON . fromIPv6Address
+  readJSON v = JSON.readJSON v >>= mkIPv6Address
+
+newtype IPv6Network = IPv6Network { fromIPv6Network :: String }
+  deriving (Show, Eq)
+
+-- FIXME: this should check that 'address' is a valid ip
+mkIPv6Network :: Monad m => String -> m IPv6Network
+mkIPv6Network address =
+  return IPv6Network { fromIPv6Network = address }
+
+instance JSON.JSON IPv6Network where
+  showJSON = JSON.showJSON . fromIPv6Network
+  readJSON v = JSON.readJSON v >>= mkIPv6Network
+
 -- * Ganeti types
 
 -- | 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)
-       , ("DTExt",        'C.dtExt)
+$(THH.declareLADT ''String "DiskTemplate"
+       [ ("DTDiskless",   "diskless")
+       , ("DTFile",       "file")
+       , ("DTSharedFile", "sharedfile")
+       , ("DTPlain",      "plain")
+       , ("DTBlock",      "blockdev")
+       , ("DTDrbd8",      "drbd")
+       , ("DTRbd",        "rbd")
+       , ("DTExt",        "ext")
        ])
 $(THH.makeJSONInstance ''DiskTemplate)
 
+instance THH.PyValue DiskTemplate where
+  showValue = show . diskTemplateToRaw
+
 instance HasStringRepr DiskTemplate where
   fromStringRepr = diskTemplateFromRaw
   toStringRepr = diskTemplateToRaw
 
+-- | Data type representing what items the tag operations apply to.
+$(THH.declareLADT ''String "TagKind"
+  [ ("TagKindInstance", "instance")
+  , ("TagKindNode",     "node")
+  , ("TagKindGroup",    "nodegroup")
+  , ("TagKindCluster",  "cluster")
+  , ("TagKindNetwork",  "network")
+  ])
+$(THH.makeJSONInstance ''TagKind)
+
 -- | 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.
-$(THH.declareSADT "AllocPolicy"
-       [ ("AllocPreferred",   'C.allocPolicyPreferred)
-       , ("AllocLastResort",  'C.allocPolicyLastResort)
-       , ("AllocUnallocable", 'C.allocPolicyUnallocable)
+$(THH.declareLADT ''String "AllocPolicy"
+       [ ("AllocPreferred",   "preferred")
+       , ("AllocLastResort",  "last_resort")
+       , ("AllocUnallocable", "unallocable")
        ])
 $(THH.makeJSONInstance ''AllocPolicy)
 
 -- | The Instance real state type. FIXME: this could be improved to
 -- just wrap a /NormalState AdminStatus | ErrorState ErrorCondition/.
-$(THH.declareSADT "InstanceStatus"
-       [ ("StatusDown",    'C.inststAdmindown)
-       , ("StatusOffline", 'C.inststAdminoffline)
-       , ("ErrorDown",     'C.inststErrordown)
-       , ("ErrorUp",       'C.inststErrorup)
-       , ("NodeDown",      'C.inststNodedown)
-       , ("NodeOffline",   'C.inststNodeoffline)
-       , ("Running",       'C.inststRunning)
-       , ("WrongNode",     'C.inststWrongnode)
+$(THH.declareLADT ''String "InstanceStatus"
+       [ ("StatusDown",    "ADMIN_down")
+       , ("StatusOffline", "ADMIN_offline")
+       , ("ErrorDown",     "ERROR_down")
+       , ("ErrorUp",       "ERROR_up")
+       , ("NodeDown",      "ERROR_nodedown")
+       , ("NodeOffline",   "ERROR_nodeoffline")
+       , ("Running",       "running")
+       , ("WrongNode",     "ERROR_wrongnode")
        ])
 $(THH.makeJSONInstance ''InstanceStatus)
 
 -- | Migration mode.
-$(THH.declareSADT "MigrationMode"
-     [ ("MigrationLive",    'C.htMigrationLive)
-     , ("MigrationNonLive", 'C.htMigrationNonlive)
+$(THH.declareLADT ''String "MigrationMode"
+     [ ("MigrationLive",    "live")
+     , ("MigrationNonLive", "non-live")
      ])
 $(THH.makeJSONInstance ''MigrationMode)
 
 -- | Verify optional checks.
-$(THH.declareSADT "VerifyOptionalChecks"
-     [ ("VerifyNPlusOneMem", 'C.verifyNplusoneMem)
+$(THH.declareLADT ''String "VerifyOptionalChecks"
+     [ ("VerifyNPlusOneMem", "nplusone_mem")
      ])
 $(THH.makeJSONInstance ''VerifyOptionalChecks)
 
 -- | Cluster verify error codes.
-$(THH.declareSADT "CVErrorCode"
-  [ ("CvECLUSTERCFG",                  'C.cvEclustercfgCode)
-  , ("CvECLUSTERCERT",                 'C.cvEclustercertCode)
-  , ("CvECLUSTERFILECHECK",            'C.cvEclusterfilecheckCode)
-  , ("CvECLUSTERDANGLINGNODES",        'C.cvEclusterdanglingnodesCode)
-  , ("CvECLUSTERDANGLINGINST",         'C.cvEclusterdanglinginstCode)
-  , ("CvEINSTANCEBADNODE",             'C.cvEinstancebadnodeCode)
-  , ("CvEINSTANCEDOWN",                'C.cvEinstancedownCode)
-  , ("CvEINSTANCELAYOUT",              'C.cvEinstancelayoutCode)
-  , ("CvEINSTANCEMISSINGDISK",         'C.cvEinstancemissingdiskCode)
-  , ("CvEINSTANCEFAULTYDISK",          'C.cvEinstancefaultydiskCode)
-  , ("CvEINSTANCEWRONGNODE",           'C.cvEinstancewrongnodeCode)
-  , ("CvEINSTANCESPLITGROUPS",         'C.cvEinstancesplitgroupsCode)
-  , ("CvEINSTANCEPOLICY",              'C.cvEinstancepolicyCode)
-  , ("CvENODEDRBD",                    'C.cvEnodedrbdCode)
-  , ("CvENODEDRBDHELPER",              'C.cvEnodedrbdhelperCode)
-  , ("CvENODEFILECHECK",               'C.cvEnodefilecheckCode)
-  , ("CvENODEHOOKS",                   'C.cvEnodehooksCode)
-  , ("CvENODEHV",                      'C.cvEnodehvCode)
-  , ("CvENODELVM",                     'C.cvEnodelvmCode)
-  , ("CvENODEN1",                      'C.cvEnoden1Code)
-  , ("CvENODENET",                     'C.cvEnodenetCode)
-  , ("CvENODEOS",                      'C.cvEnodeosCode)
-  , ("CvENODEORPHANINSTANCE",          'C.cvEnodeorphaninstanceCode)
-  , ("CvENODEORPHANLV",                'C.cvEnodeorphanlvCode)
-  , ("CvENODERPC",                     'C.cvEnoderpcCode)
-  , ("CvENODESSH",                     'C.cvEnodesshCode)
-  , ("CvENODEVERSION",                 'C.cvEnodeversionCode)
-  , ("CvENODESETUP",                   'C.cvEnodesetupCode)
-  , ("CvENODETIME",                    'C.cvEnodetimeCode)
-  , ("CvENODEOOBPATH",                 'C.cvEnodeoobpathCode)
-  , ("CvENODEUSERSCRIPTS",             'C.cvEnodeuserscriptsCode)
-  , ("CvENODEFILESTORAGEPATHS",        'C.cvEnodefilestoragepathsCode)
-  , ("CvENODEFILESTORAGEPATHUNUSABLE", 'C.cvEnodefilestoragepathunusableCode)
+$(THH.declareLADT ''String "CVErrorCode"
+  [ ("CvECLUSTERCFG",                  "ECLUSTERCFG")
+  , ("CvECLUSTERCERT",                 "ECLUSTERCERT")
+  , ("CvECLUSTERFILECHECK",            "ECLUSTERFILECHECK")
+  , ("CvECLUSTERDANGLINGNODES",        "ECLUSTERDANGLINGNODES")
+  , ("CvECLUSTERDANGLINGINST",         "ECLUSTERDANGLINGINST")
+  , ("CvEINSTANCEBADNODE",             "EINSTANCEBADNODE")
+  , ("CvEINSTANCEDOWN",                "EINSTANCEDOWN")
+  , ("CvEINSTANCELAYOUT",              "EINSTANCELAYOUT")
+  , ("CvEINSTANCEMISSINGDISK",         "EINSTANCEMISSINGDISK")
+  , ("CvEINSTANCEFAULTYDISK",          "EINSTANCEFAULTYDISK")
+  , ("CvEINSTANCEWRONGNODE",           "EINSTANCEWRONGNODE")
+  , ("CvEINSTANCESPLITGROUPS",         "EINSTANCESPLITGROUPS")
+  , ("CvEINSTANCEPOLICY",              "EINSTANCEPOLICY")
+  , ("CvEINSTANCEUNSUITABLENODE",      "EINSTANCEUNSUITABLENODE")
+  , ("CvEINSTANCEMISSINGCFGPARAMETER", "EINSTANCEMISSINGCFGPARAMETER")
+  , ("CvENODEDRBD",                    "ENODEDRBD")
+  , ("CvENODEDRBDVERSION",             "ENODEDRBDVERSION")
+  , ("CvENODEDRBDHELPER",              "ENODEDRBDHELPER")
+  , ("CvENODEFILECHECK",               "ENODEFILECHECK")
+  , ("CvENODEHOOKS",                   "ENODEHOOKS")
+  , ("CvENODEHV",                      "ENODEHV")
+  , ("CvENODELVM",                     "ENODELVM")
+  , ("CvENODEN1",                      "ENODEN1")
+  , ("CvENODENET",                     "ENODENET")
+  , ("CvENODEOS",                      "ENODEOS")
+  , ("CvENODEORPHANINSTANCE",          "ENODEORPHANINSTANCE")
+  , ("CvENODEORPHANLV",                "ENODEORPHANLV")
+  , ("CvENODERPC",                     "ENODERPC")
+  , ("CvENODESSH",                     "ENODESSH")
+  , ("CvENODEVERSION",                 "ENODEVERSION")
+  , ("CvENODESETUP",                   "ENODESETUP")
+  , ("CvENODETIME",                    "ENODETIME")
+  , ("CvENODEOOBPATH",                 "ENODEOOBPATH")
+  , ("CvENODEUSERSCRIPTS",             "ENODEUSERSCRIPTS")
+  , ("CvENODEFILESTORAGEPATHS",        "ENODEFILESTORAGEPATHS")
+  , ("CvENODEFILESTORAGEPATHUNUSABLE", "ENODEFILESTORAGEPATHUNUSABLE")
   , ("CvENODESHAREDFILESTORAGEPATHUNUSABLE",
-     'C.cvEnodesharedfilestoragepathunusableCode)
+     "ENODESHAREDFILESTORAGEPATHUNUSABLE")
+  , ("CvEGROUPDIFFERENTPVSIZE",        "EGROUPDIFFERENTPVSIZE")
   ])
 $(THH.makeJSONInstance ''CVErrorCode)
 
 -- | Dynamic device modification, just add\/remove version.
-$(THH.declareSADT "DdmSimple"
-     [ ("DdmSimpleAdd",    'C.ddmAdd)
-     , ("DdmSimpleRemove", 'C.ddmRemove)
+$(THH.declareLADT ''String "DdmSimple"
+     [ ("DdmSimpleAdd",    "add")
+     , ("DdmSimpleRemove", "remove")
      ])
 $(THH.makeJSONInstance ''DdmSimple)
 
 -- | Dynamic device modification, all operations version.
-$(THH.declareSADT "DdmFull"
-     [ ("DdmFullAdd",    'C.ddmAdd)
-     , ("DdmFullRemove", 'C.ddmRemove)
-     , ("DdmFullModify", 'C.ddmModify)
+--
+-- TODO: DDM_SWAP, DDM_MOVE?
+$(THH.declareLADT ''String "DdmFull"
+     [ ("DdmFullAdd",    "add")
+     , ("DdmFullRemove", "remove")
+     , ("DdmFullModify", "modify")
      ])
 $(THH.makeJSONInstance ''DdmFull)
 
 -- | Hypervisor type definitions.
-$(THH.declareSADT "Hypervisor"
-  [ ( "Kvm",    'C.htKvm )
-  , ( "XenPvm", 'C.htXenPvm )
-  , ( "Chroot", 'C.htChroot )
-  , ( "XenHvm", 'C.htXenHvm )
-  , ( "Lxc",    'C.htLxc )
-  , ( "Fake",   'C.htFake )
+$(THH.declareLADT ''String "Hypervisor"
+  [ ("Kvm",    "kvm")
+  , ("XenPvm", "xen-pvm")
+  , ("Chroot", "chroot")
+  , ("XenHvm", "xen-hvm")
+  , ("Lxc",    "lxc")
+  , ("Fake",   "fake")
   ])
 $(THH.makeJSONInstance ''Hypervisor)
 
+instance THH.PyValue Hypervisor where
+  showValue = show . hypervisorToRaw
+
 -- | Oob command type.
-$(THH.declareSADT "OobCommand"
-  [ ("OobHealth",      'C.oobHealth)
-  , ("OobPowerCycle",  'C.oobPowerCycle)
-  , ("OobPowerOff",    'C.oobPowerOff)
-  , ("OobPowerOn",     'C.oobPowerOn)
-  , ("OobPowerStatus", 'C.oobPowerStatus)
+$(THH.declareLADT ''String "OobCommand"
+  [ ("OobHealth",      "health")
+  , ("OobPowerCycle",  "power-cycle")
+  , ("OobPowerOff",    "power-off")
+  , ("OobPowerOn",     "power-on")
+  , ("OobPowerStatus", "power-status")
   ])
 $(THH.makeJSONInstance ''OobCommand)
 
+-- | Oob command status
+$(THH.declareLADT ''String "OobStatus"
+  [ ("OobStatusCritical", "CRITICAL")
+  , ("OobStatusOk",       "OK")
+  , ("OobStatusUnknown",  "UNKNOWN")
+  , ("OobStatusWarning",  "WARNING")
+  ])
+$(THH.makeJSONInstance ''OobStatus)
+
 -- | Storage type.
-$(THH.declareSADT "StorageType"
-  [ ("StorageFile", 'C.stFile)
-  , ("StorageLvmPv", 'C.stLvmPv)
-  , ("StorageLvmVg", 'C.stLvmVg)
-  , ("StorageDiskless", 'C.stDiskless)
-  , ("StorageBlock", 'C.stBlock)
-  , ("StorageRados", 'C.stRados)
-  , ("StorageExt", 'C.stExt)
+$(THH.declareLADT ''String "StorageType"
+  [ ("StorageFile", "file")
+  , ("StorageLvmPv", "lvm-pv")
+  , ("StorageLvmVg", "lvm-vg")
+  , ("StorageDiskless", "diskless")
+  , ("StorageBlock", "blockdev")
+  , ("StorageRados", "rados")
+  , ("StorageExt", "ext")
   ])
 $(THH.makeJSONInstance ''StorageType)
 
@@ -370,7 +510,7 @@ showSUSimple st sk = show (storageTypeToRaw st, sk, []::[String])
 showSULvm :: StorageType -> StorageKey -> SPExclusiveStorage -> String
 showSULvm st sk es = show (storageTypeToRaw st, sk, [es])
 
--- | Mapping fo disk templates to storage type
+-- | Mapping from disk templates to storage types
 -- FIXME: This is semantically the same as the constant
 -- C.diskTemplatesStorageType, remove this when python constants
 -- are generated from haskell constants
@@ -395,87 +535,93 @@ addParamsToStorageUnit es (SURaw StorageLvmVg key) = SULvmVg key es
 addParamsToStorageUnit _ (SURaw StorageRados key) = SURados key
 
 -- | Node evac modes.
-$(THH.declareSADT "NodeEvacMode"
-  [ ("NEvacPrimary",   'C.iallocatorNevacPri)
-  , ("NEvacSecondary", 'C.iallocatorNevacSec)
-  , ("NEvacAll",       'C.iallocatorNevacAll)
+--
+-- This is part of the 'IAllocator' interface and it is used, for
+-- example, in 'Ganeti.HTools.Loader.RqType'.  However, it must reside
+-- in this module, and not in 'Ganeti.HTools.Types', because it is
+-- also used by 'Ganeti.HsConstants'.
+$(THH.declareLADT ''String "EvacMode"
+  [ ("ChangePrimary",   "primary-only")
+  , ("ChangeSecondary", "secondary-only")
+  , ("ChangeAll",       "all")
   ])
-$(THH.makeJSONInstance ''NodeEvacMode)
+$(THH.makeJSONInstance ''EvacMode)
 
 -- | The file driver type.
-$(THH.declareSADT "FileDriver"
-  [ ("FileLoop",   'C.fdLoop)
-  , ("FileBlktap", 'C.fdBlktap)
+$(THH.declareLADT ''String "FileDriver"
+  [ ("FileLoop",   "loop")
+  , ("FileBlktap", "blktap")
   ])
 $(THH.makeJSONInstance ''FileDriver)
 
 -- | The instance create mode.
-$(THH.declareSADT "InstCreateMode"
-  [ ("InstCreate",       'C.instanceCreate)
-  , ("InstImport",       'C.instanceImport)
-  , ("InstRemoteImport", 'C.instanceRemoteImport)
+$(THH.declareLADT ''String "InstCreateMode"
+  [ ("InstCreate",       "create")
+  , ("InstImport",       "import")
+  , ("InstRemoteImport", "remote-import")
   ])
 $(THH.makeJSONInstance ''InstCreateMode)
 
 -- | Reboot type.
-$(THH.declareSADT "RebootType"
-  [ ("RebootSoft", 'C.instanceRebootSoft)
-  , ("RebootHard", 'C.instanceRebootHard)
-  , ("RebootFull", 'C.instanceRebootFull)
+$(THH.declareLADT ''String "RebootType"
+  [ ("RebootSoft", "soft")
+  , ("RebootHard", "hard")
+  , ("RebootFull", "full")
   ])
 $(THH.makeJSONInstance ''RebootType)
 
 -- | Export modes.
-$(THH.declareSADT "ExportMode"
-  [ ("ExportModeLocal",  'C.exportModeLocal)
-  , ("ExportModeRemove", 'C.exportModeRemote)
+$(THH.declareLADT ''String "ExportMode"
+  [ ("ExportModeLocal",  "local")
+  , ("ExportModeRemote", "remote")
   ])
 $(THH.makeJSONInstance ''ExportMode)
 
 -- | IAllocator run types (OpTestIAllocator).
-$(THH.declareSADT "IAllocatorTestDir"
-  [ ("IAllocatorDirIn",  'C.iallocatorDirIn)
-  , ("IAllocatorDirOut", 'C.iallocatorDirOut)
+$(THH.declareLADT ''String "IAllocatorTestDir"
+  [ ("IAllocatorDirIn",  "in")
+  , ("IAllocatorDirOut", "out")
   ])
 $(THH.makeJSONInstance ''IAllocatorTestDir)
 
 -- | IAllocator mode. FIXME: use this in "HTools.Backend.IAlloc".
-$(THH.declareSADT "IAllocatorMode"
-  [ ("IAllocatorAlloc",       'C.iallocatorModeAlloc)
-  , ("IAllocatorMultiAlloc",  'C.iallocatorModeMultiAlloc)
-  , ("IAllocatorReloc",       'C.iallocatorModeReloc)
-  , ("IAllocatorNodeEvac",    'C.iallocatorModeNodeEvac)
-  , ("IAllocatorChangeGroup", 'C.iallocatorModeChgGroup)
+$(THH.declareLADT ''String "IAllocatorMode"
+  [ ("IAllocatorAlloc",       "allocate")
+  , ("IAllocatorMultiAlloc",  "multi-allocate")
+  , ("IAllocatorReloc",       "relocate")
+  , ("IAllocatorNodeEvac",    "node-evacuate")
+  , ("IAllocatorChangeGroup", "change-group")
   ])
 $(THH.makeJSONInstance ''IAllocatorMode)
 
 -- | Network mode.
-$(THH.declareSADT "NICMode"
-  [ ("NMBridged", 'C.nicModeBridged)
-  , ("NMRouted",  'C.nicModeRouted)
-  , ("NMOvs",     'C.nicModeOvs)
+$(THH.declareLADT ''String "NICMode"
+  [ ("NMBridged", "bridged")
+  , ("NMRouted",  "routed")
+  , ("NMOvs",     "openvswitch")
+  , ("NMPool",    "pool")
   ])
 $(THH.makeJSONInstance ''NICMode)
 
 -- | The JobStatus data type. Note that this is ordered especially
 -- such that greater\/lesser comparison on values of this type makes
 -- sense.
-$(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.declareLADT ''String "JobStatus"
+  [ ("JOB_STATUS_QUEUED",    "queued")
+  , ("JOB_STATUS_WAITING",   "waiting")
+  , ("JOB_STATUS_CANCELING", "canceling")
+  , ("JOB_STATUS_RUNNING",   "running")
+  , ("JOB_STATUS_CANCELED",  "canceled")
+  , ("JOB_STATUS_SUCCESS",   "success")
+  , ("JOB_STATUS_ERROR",     "error")
+  ])
 $(THH.makeJSONInstance ''JobStatus)
 
 -- | Finalized job status.
-$(THH.declareSADT "FinalizedJobStatus"
-  [ ("JobStatusCanceled",   'C.jobStatusCanceled)
-  , ("JobStatusSuccessful", 'C.jobStatusSuccess)
-  , ("JobStatusFailed",     'C.jobStatusError)
+$(THH.declareLADT ''String "FinalizedJobStatus"
+  [ ("JobStatusCanceled",   "canceled")
+  , ("JobStatusSuccessful", "success")
+  , ("JobStatusFailed",     "error")
   ])
 $(THH.makeJSONInstance ''FinalizedJobStatus)
 
@@ -533,9 +679,9 @@ instance JSON JobDependency where
 
 -- | Valid opcode priorities for submit.
 $(THH.declareIADT "OpSubmitPriority"
-  [ ("OpPrioLow",    'C.opPrioLow)
-  , ("OpPrioNormal", 'C.opPrioNormal)
-  , ("OpPrioHigh",   'C.opPrioHigh)
+  [ ("OpPrioLow",    'ConstantUtils.priorityLow)
+  , ("OpPrioNormal", 'ConstantUtils.priorityNormal)
+  , ("OpPrioHigh",   'ConstantUtils.priorityHigh)
   ])
 $(THH.makeJSONInstance ''OpSubmitPriority)
 
@@ -553,22 +699,22 @@ fmtSubmitPriority OpPrioNormal = "normal"
 fmtSubmitPriority OpPrioHigh   = "high"
 
 -- | Our ADT for the OpCode status at runtime (while in a job).
-$(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.declareLADT ''String "OpStatus"
+  [ ("OP_STATUS_QUEUED",    "queued")
+  , ("OP_STATUS_WAITING",   "waiting")
+  , ("OP_STATUS_CANCELING", "canceling")
+  , ("OP_STATUS_RUNNING",   "running")
+  , ("OP_STATUS_CANCELED",  "canceled")
+  , ("OP_STATUS_SUCCESS",   "success")
+  , ("OP_STATUS_ERROR",     "error")
   ])
 $(THH.makeJSONInstance ''OpStatus)
 
 -- | Type for the job message type.
-$(THH.declareSADT "ELogType"
-  [ ("ELogMessage",      'C.elogMessage)
-  , ("ELogRemoteImport", 'C.elogRemoteImport)
-  , ("ELogJqueueTest",   'C.elogJqueueTest)
+$(THH.declareLADT ''String "ELogType"
+  [ ("ELogMessage",      "message")
+  , ("ELogRemoteImport", "remote-import")
+  , ("ELogJqueueTest",   "jqueue-test")
   ])
 $(THH.makeJSONInstance ''ELogType)
 
@@ -577,3 +723,131 @@ type ReasonElem = (String, String, Integer)
 
 -- | Type representing a reason trail.
 type ReasonTrail = [ReasonElem]
+
+-- | The VTYPES, a mini-type system in Python.
+$(THH.declareLADT ''String "VType"
+  [ ("VTypeString",      "string")
+  , ("VTypeMaybeString", "maybe-string")
+  , ("VTypeBool",        "bool")
+  , ("VTypeSize",        "size")
+  , ("VTypeInt",         "int")
+  ])
+$(THH.makeJSONInstance ''VType)
+
+instance THH.PyValue VType where
+  showValue = THH.showValue . vTypeToRaw
+
+-- * Node role type
+
+$(THH.declareLADT ''String "NodeRole"
+  [ ("NROffline",   "O")
+  , ("NRDrained",   "D")
+  , ("NRRegular",   "R")
+  , ("NRCandidate", "C")
+  , ("NRMaster",    "M")
+  ])
+$(THH.makeJSONInstance ''NodeRole)
+
+-- | The description of the node role.
+roleDescription :: NodeRole -> String
+roleDescription NROffline   = "offline"
+roleDescription NRDrained   = "drained"
+roleDescription NRRegular   = "regular"
+roleDescription NRCandidate = "master candidate"
+roleDescription NRMaster    = "master"
+
+-- * Disk types
+
+$(THH.declareLADT ''String "DiskMode"
+  [ ("DiskRdOnly", "ro")
+  , ("DiskRdWr",   "rw")
+  ])
+$(THH.makeJSONInstance ''DiskMode)
+
+-- | The persistent block driver type. Currently only one type is allowed.
+$(THH.declareLADT ''String "BlockDriver"
+  [ ("BlockDrvManual", "manual")
+  ])
+$(THH.makeJSONInstance ''BlockDriver)
+
+-- * Instance types
+
+$(THH.declareLADT ''String "AdminState"
+  [ ("AdminOffline", "offline")
+  , ("AdminDown",    "down")
+  , ("AdminUp",      "up")
+  ])
+$(THH.makeJSONInstance ''AdminState)
+
+-- * Storage field type
+
+$(THH.declareLADT ''String "StorageField"
+  [ ( "SFUsed",        "used")
+  , ( "SFName",        "name")
+  , ( "SFAllocatable", "allocatable")
+  , ( "SFFree",        "free")
+  , ( "SFSize",        "size")
+  ])
+$(THH.makeJSONInstance ''StorageField)
+
+-- * Disk access protocol
+
+$(THH.declareLADT ''String "DiskAccessMode"
+  [ ( "DiskUserspace",   "userspace")
+  , ( "DiskKernelspace", "kernelspace")
+  ])
+$(THH.makeJSONInstance ''DiskAccessMode)
+
+-- | Local disk status
+--
+-- Python code depends on:
+--   DiskStatusOk < DiskStatusUnknown < DiskStatusFaulty
+$(THH.declareILADT "LocalDiskStatus"
+  [ ("DiskStatusFaulty",  3)
+  , ("DiskStatusOk",      1)
+  , ("DiskStatusUnknown", 2)
+  ])
+
+localDiskStatusName :: LocalDiskStatus -> String
+localDiskStatusName DiskStatusFaulty = "faulty"
+localDiskStatusName DiskStatusOk = "ok"
+localDiskStatusName DiskStatusUnknown = "unknown"
+
+-- | Replace disks type.
+$(THH.declareLADT ''String "ReplaceDisksMode"
+  [ -- Replace disks on primary
+    ("ReplaceOnPrimary",    "replace_on_primary")
+    -- Replace disks on secondary
+  , ("ReplaceOnSecondary",  "replace_on_secondary")
+    -- Change secondary node
+  , ("ReplaceNewSecondary", "replace_new_secondary")
+  , ("ReplaceAuto",         "replace_auto")
+  ])
+$(THH.makeJSONInstance ''ReplaceDisksMode)
+
+-- | Basic timeouts for RPC calls.
+$(THH.declareILADT "RpcTimeout"
+  [ ("Urgent",    60)       -- 1 minute
+  , ("Fast",      5 * 60)   -- 5 minutes
+  , ("Normal",    15 * 60)  -- 15 minutes
+  , ("Slow",      3600)     -- 1 hour
+  , ("FourHours", 4 * 3600) -- 4 hours
+  , ("OneDay",    86400)    -- 1 day
+  ])
+
+-- | Hotplug action.
+
+$(THH.declareLADT ''String "HotplugAction"
+  [ ("HAAdd", "hotadd")
+  , ("HARemove",  "hotremove")
+  , ("HAMod",     "hotmod")
+  ])
+$(THH.makeJSONInstance ''HotplugAction)
+
+-- | Hotplug Device Target.
+
+$(THH.declareLADT ''String "HotplugTarget"
+  [ ("HTDisk", "hotdisk")
+  , ("HTNic",  "hotnic")
+  ])
+$(THH.makeJSONInstance ''HotplugTarget)
index 71b0bdd..4f67006 100644 (file)
@@ -71,7 +71,7 @@ import Debug.Trace
 import Network.Socket
 
 import Ganeti.BasicTypes
-import qualified Ganeti.Constants as C
+import qualified Ganeti.ConstantUtils as ConstantUtils
 import Ganeti.Logging
 import Ganeti.Runtime
 import System.IO
@@ -317,7 +317,7 @@ rStripSpace = reverse . dropWhile isSpace . reverse
 -- This is a Linux-specific method as it uses the /proc filesystem.
 newUUID :: IO String
 newUUID = do
-  contents <- readFile C.randomUuidFile
+  contents <- readFile ConstantUtils.randomUuidFile
   return $! rStripSpace $ take 128 contents
 
 -- | Returns the current time as an 'Integer' representing the number
diff --git a/src/hs2py-constants.hs b/src/hs2py-constants.hs
new file mode 100644 (file)
index 0000000..9eea997
--- /dev/null
@@ -0,0 +1,31 @@
+{-| hs2py-constants
+
+This program outputs the all the converted Haskell to Python
+constants.
+
+-}
+
+{-
+
+Copyright (C) 2013 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.
+
+-}
+import Ganeti.Hs2Py.ListConstants
+
+main :: IO ()
+main = putConstants
diff --git a/src/hs2py.hs b/src/hs2py.hs
new file mode 100644 (file)
index 0000000..c0c4d74
--- /dev/null
@@ -0,0 +1,29 @@
+{-| Haskell to Python opcode generation program.
+
+-}
+
+{-
+
+Copyright (C) 2011, 2012, 2013 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.
+
+-}
+
+import Ganeti.Hs2Py.GenOpCodes
+
+main :: IO ()
+main = putStrLn showPyClasses
diff --git a/test/data/htools/hbal-dyn.data b/test/data/htools/hbal-dyn.data
new file mode 100644 (file)
index 0000000..052cb0f
--- /dev/null
@@ -0,0 +1,14 @@
+group-01|fake-uuid-01|preferred||
+
+node-01-000|91552|0|91040|3100|3100|32|M|fake-uuid-01|1
+node-01-001|91552|0|91040|3100|3100|32|N|fake-uuid-01|1
+
+inst-00|128|0|1|running|Y|node-01-000||ext||1
+inst-01|128|0|1|running|Y|node-01-000||ext||1
+inst-02|128|0|1|running|Y|node-01-000||ext||1
+inst-03|128|0|1|running|Y|node-01-000||ext||1
+inst-10|256|0|2|running|Y|node-01-001||ext||1
+inst-11|256|0|2|running|Y|node-01-001||ext||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,ext|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,ext|4.0|32.0
diff --git a/test/data/kvm_runtime.json b/test/data/kvm_runtime.json
new file mode 100644 (file)
index 0000000..d9fb8c1
--- /dev/null
@@ -0,0 +1,139 @@
+[
+  [
+    "/usr/bin/kvm",
+    "-name",
+    "xen-test-inst2",
+    "-m",
+    1024,
+    "-smp",
+    "1",
+    "-pidfile",
+    "/var/run/ganeti/kvm-hypervisor/pid/xen-test-inst2",
+    "-balloon",
+    "virtio",
+    "-daemonize",
+    "-machine",
+    "pc-1.1",
+    "-monitor",
+    "unix:/var/run/ganeti/kvm-hypervisor/ctrl/xen-test-inst2.monitor,server,nowait",
+    "-serial",
+    "unix:/var/run/ganeti/kvm-hypervisor/ctrl/xen-test-inst2.serial,server,nowait",
+    "-usb",
+    "-usbdevice",
+    "tablet",
+    "-vnc",
+    ":5100"
+  ],
+  [
+    {
+      "mac": "aa:00:00:bf:2f:16",
+      "nicparams": {
+        "link": "br0",
+        "mode": "bridged"
+      },
+      "pci": 6,
+      "uuid": "003fc157-66a8-4e6d-8b7e-ec4f69751396"
+    }
+  ],
+  {
+    "acpi": true,
+    "boot_order": "disk",
+    "cdrom2_image_path": "",
+    "cdrom_disk_type": "",
+    "cdrom_image_path": "",
+    "cpu_cores": 0,
+    "cpu_mask": "all",
+    "cpu_sockets": 0,
+    "cpu_threads": 0,
+    "cpu_type": "",
+    "disk_cache": "default",
+    "disk_type": "paravirtual",
+    "floppy_image_path": "",
+    "initrd_path": "",
+    "kernel_args": "ro",
+    "kernel_path": "",
+    "keymap": "",
+    "kvm_extra": "",
+    "kvm_flag": "",
+    "kvm_path": "/usr/bin/kvm",
+    "machine_version": "",
+    "mem_path": "",
+    "migration_bandwidth": 32,
+    "migration_downtime": 30,
+    "migration_mode": "live",
+    "migration_port": 8102,
+    "nic_type": "paravirtual",
+    "reboot_behavior": "reboot",
+    "root_path": "/dev/vda1",
+    "security_domain": "",
+    "security_model": "none",
+    "serial_console": true,
+    "serial_speed": 38400,
+    "soundhw": "",
+    "spice_bind": "",
+    "spice_image_compression": "",
+    "spice_ip_version": 0,
+    "spice_jpeg_wan_compression": "",
+    "spice_password_file": "",
+    "spice_playback_compression": true,
+    "spice_streaming_video": "",
+    "spice_tls_ciphers": "HIGH:-DES:-3DES:-EXPORT:-ADH",
+    "spice_use_tls": false,
+    "spice_use_vdagent": true,
+    "spice_zlib_glz_wan_compression": "",
+    "usb_devices": "",
+    "usb_mouse": "",
+    "use_chroot": false,
+    "use_localtime": false,
+    "vga": "",
+    "vhost_net": false,
+    "vnc_bind_address": "0.0.0.0",
+    "vnc_password_file": "",
+    "vnc_tls": false,
+    "vnc_x509_path": "",
+    "vnc_x509_verify": false,
+    "vnet_hdr": true
+  },
+  [
+    [
+      {
+        "dev_type": "lvm",
+        "iv_name": "disk/0",
+        "logical_id": [
+          "autovg",
+          "b9d4ee8e-c81b-42eb-9899-60481886c7ac.disk0"
+        ],
+        "mode": "rw",
+        "name": "disk0",
+        "params": {
+          "stripes": 1
+        },
+        "pci": 4,
+        "size": 1024,
+        "uuid": "7c079136-2573-4112-82d0-0d3d2aa90d8f"
+      },
+      "/var/run/ganeti/instance-disks/xen-test-inst2:0",
+      "rbd://123451214123/"
+    ],
+    [
+      {
+        "dev_type": "lvm",
+        "iv_name": "disk/1",
+        "logical_id": [
+          "autovg",
+          "602c0a3b-d09b-4ebe-9774-5f12ef654a1f.disk1"
+        ],
+        "mode": "rw",
+        "name": "disk1",
+        "params": {
+          "stripes": 1
+        },
+        "pci": 5,
+        "size": 512,
+        "uuid": "9f5c5bd4-6f60-480b-acdc-9bb1a4b7df79"
+      },
+      "/var/run/ganeti/instance-disks/xen-test-inst2:1",
+      null
+    ]
+  ]
+]
diff --git a/test/data/mond-data.txt b/test/data/mond-data.txt
new file mode 100644 (file)
index 0000000..0f5447c
--- /dev/null
@@ -0,0 +1 @@
+[{"node":"node1.example.com","reports":[{"name":"cpu-avg-load","version":"B","format_version":1,"timestamp":1379507272000000000,"category":null,"kind":0,"data":{"cpu_number":4,"cpus":[4.108859597350646e-2,4.456554528165781e-2,6.203619909502262e-2,5.595448881893895e-2],"cpu_total":0.203643517607712}}]},{"node":"node2.example.com","reports":[{"name":"cpu-avg-load","version":"B","format_version":1,"timestamp":1379507280000000000,"category":null,"kind":0,"data":{"cpu_number":2,"cpus":[4.155409618511363e-3,3.4586452012150787e-3],"cpu_total":7.614031289927129e-3}}]}]
diff --git a/test/hs/Test/AutoConf.hs b/test/hs/Test/AutoConf.hs
new file mode 100644 (file)
index 0000000..e981575
--- /dev/null
@@ -0,0 +1,370 @@
+{-# LANGUAGE TemplateHaskell #-}
+{-| Unittests for 'AutoConf'
+
+-}
+
+{-
+
+Copyright (C) 2013 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 Test.AutoConf where
+
+import qualified Data.Char as Char (isAlpha)
+import Test.HUnit as HUnit
+
+import qualified AutoConf
+import qualified Test.Ganeti.TestHelper as TestHelper
+
+{-# ANN module "HLint: ignore Use camelCase" #-}
+
+-- | 'isFilePath x' tests whether @x@ is a valid filepath
+--
+-- A valid filepath must be absolute and must not contain commas.
+isFilePath :: String -> Bool
+isFilePath ('/':str) = ',' `notElem` str
+isFilePath _ = False
+
+-- | 'isGntScript x' tests whether @x@ is a valid Ganeti script
+--
+-- A valid Ganeti script is prefixed by "gnt-" and the rest of the
+-- 'String' contains only alphabetic 'Char's.
+isGntScript :: String -> Bool
+isGntScript str =
+  case span (/= '-') str of
+    (x, '-':y) -> x == "gnt" && all Char.isAlpha y
+    _ -> False
+
+-- | 'isGroup x' tests whether @x@ is a valid group name
+--
+-- A valid group name name is an alphabetic 'String' possibly
+-- containing '-'.
+isGroup :: String -> Bool
+isGroup = all (\c -> Char.isAlpha c || c == '-')
+
+-- | 'isProgram x' tests whether @x@ is a valid program name
+--
+-- A valid program name is an alphabetic 'String'.
+isProgram :: String -> Bool
+isProgram = all Char.isAlpha
+
+-- | 'isUser x' tests whether @x@ is a valid username
+--
+-- See 'isGroup'.
+isUser :: String -> Bool
+isUser = isGroup
+
+case_versionSuffix :: Assertion
+case_versionSuffix =
+  HUnit.assertBool
+    "'versionSuffix' is invalid"
+    (case AutoConf.versionSuffix of
+        "" -> True
+        '~':x -> not (null x)
+        _ -> False)
+
+case_localstatedir :: Assertion
+case_localstatedir =
+  HUnit.assertBool
+    "'localstatedir' is invalid"
+    (isFilePath AutoConf.localstatedir)
+
+case_sysconfdir :: Assertion
+case_sysconfdir =
+  HUnit.assertBool
+    "'sysconfdir' is invalid"
+    (isFilePath AutoConf.sysconfdir)
+
+case_sshConfigDir :: Assertion
+case_sshConfigDir =
+  HUnit.assertBool
+    "'sshConfigDir' is invalid"
+    (isFilePath AutoConf.sshConfigDir)
+
+case_sshLoginUser :: Assertion
+case_sshLoginUser =
+  HUnit.assertBool
+    "'sshLoginUser' is invalid"
+    (isUser AutoConf.sshLoginUser)
+
+case_sshConsoleUser :: Assertion
+case_sshConsoleUser =
+  HUnit.assertBool
+    "'sshConsoleUser' is invalid"
+    (isUser AutoConf.sshConsoleUser)
+
+case_exportDir :: Assertion
+case_exportDir =
+  HUnit.assertBool
+    "'exportDir' is invalid"
+    (isFilePath AutoConf.exportDir)
+
+case_osSearchPath :: Assertion
+case_osSearchPath =
+  HUnit.assertBool
+    "'osSearchPath' is invalid"
+    (all isFilePath AutoConf.osSearchPath)
+
+case_esSearchPath :: Assertion
+case_esSearchPath =
+  HUnit.assertBool
+    "'esSearchPath' is invalid"
+    (all isFilePath AutoConf.esSearchPath)
+
+case_xenBootloader :: Assertion
+case_xenBootloader =
+  HUnit.assertBool
+    "'xenBootloader' is invalid"
+    (null AutoConf.xenBootloader || isFilePath AutoConf.xenBootloader)
+
+case_xenConfigDir :: Assertion
+case_xenConfigDir =
+  HUnit.assertBool
+    "'xenConfigDir' is invalid"
+    (isFilePath AutoConf.xenConfigDir)
+
+case_xenKernel :: Assertion
+case_xenKernel =
+  HUnit.assertBool
+    "'xenKernel' is invalid"
+    (isFilePath AutoConf.xenKernel)
+
+case_xenInitrd :: Assertion
+case_xenInitrd =
+  HUnit.assertBool
+    "'xenInitrd' is invalid"
+    (isFilePath AutoConf.xenInitrd)
+
+case_kvmKernel :: Assertion
+case_kvmKernel =
+  HUnit.assertBool
+    "'kvmKernel' is invalid"
+    (isFilePath AutoConf.kvmKernel)
+
+case_iallocatorSearchPath :: Assertion
+case_iallocatorSearchPath =
+  HUnit.assertBool
+    "'iallocatorSearchPath' is invalid"
+    (all isFilePath AutoConf.iallocatorSearchPath)
+
+case_kvmPath :: Assertion
+case_kvmPath =
+  HUnit.assertBool
+    "'kvmPath' is invalid"
+    (isFilePath AutoConf.kvmPath)
+
+case_ipPath :: Assertion
+case_ipPath =
+  HUnit.assertBool
+    "'ipPath' is invalid"
+    (isFilePath AutoConf.ipPath)
+
+case_socatPath :: Assertion
+case_socatPath =
+  HUnit.assertBool
+    "'socatPath' is invalid"
+    (isFilePath AutoConf.socatPath)
+
+case_toolsdir :: Assertion
+case_toolsdir =
+  HUnit.assertBool
+    "'toolsdir' is invalid"
+    (isFilePath AutoConf.toolsdir)
+
+case_gntScripts :: Assertion
+case_gntScripts =
+  HUnit.assertBool
+    "'gntScripts' is invalid"
+    (all isGntScript AutoConf.gntScripts)
+
+case_htoolsProgs :: Assertion
+case_htoolsProgs =
+  HUnit.assertBool
+    "'htoolsProgs' is invalid"
+    (all isProgram AutoConf.htoolsProgs)
+
+case_pkglibdir :: Assertion
+case_pkglibdir =
+  HUnit.assertBool
+    "'pkglibdir' is invalid"
+    (isFilePath AutoConf.pkglibdir)
+
+case_sharedir :: Assertion
+case_sharedir =
+  HUnit.assertBool
+    "'sharedir' is invalid"
+    (isFilePath AutoConf.sharedir)
+
+case_versionedsharedir :: Assertion
+case_versionedsharedir =
+  HUnit.assertBool
+    "'versionedsharedir' is invalid"
+    (isFilePath AutoConf.versionedsharedir)
+
+case_drbdBarriers :: Assertion
+case_drbdBarriers =
+  HUnit.assertBool
+    "'drbdBarriers' is invalid"
+    (AutoConf.drbdBarriers `elem` ["n", "bf"])
+
+case_syslogUsage :: Assertion
+case_syslogUsage =
+  HUnit.assertBool
+    "'syslogUsage' is invalid"
+    (AutoConf.syslogUsage `elem` ["no", "yes", "only"])
+
+case_daemonsGroup :: Assertion
+case_daemonsGroup =
+  HUnit.assertBool
+    "'daemonsGroup' is invalid"
+    (isGroup AutoConf.daemonsGroup)
+
+case_adminGroup :: Assertion
+case_adminGroup =
+  HUnit.assertBool
+    "'adminGroup' is invalid"
+    (isGroup AutoConf.adminGroup)
+
+case_masterdUser :: Assertion
+case_masterdUser =
+  HUnit.assertBool
+    "'masterdUser' is invalid"
+    (isUser AutoConf.masterdUser)
+
+case_masterdGroup :: Assertion
+case_masterdGroup =
+  HUnit.assertBool
+    "'masterdGroup' is invalid"
+    (isGroup AutoConf.masterdGroup)
+
+case_rapiUser :: Assertion
+case_rapiUser =
+  HUnit.assertBool
+    "'rapiUser' is invalid"
+    (isUser AutoConf.rapiUser)
+
+case_rapiGroup :: Assertion
+case_rapiGroup =
+  HUnit.assertBool
+    "'rapiGroup' is invalid"
+    (isGroup AutoConf.rapiGroup)
+
+case_confdUser :: Assertion
+case_confdUser =
+  HUnit.assertBool
+    "'confdUser' is invalid"
+    (isUser AutoConf.confdUser)
+
+case_confdGroup :: Assertion
+case_confdGroup =
+  HUnit.assertBool
+    "'confdGroup' is invalid"
+    (isGroup AutoConf.confdGroup)
+
+case_luxidUser :: Assertion
+case_luxidUser =
+  HUnit.assertBool
+    "'luxidUser' is invalid"
+    (isUser AutoConf.luxidUser)
+
+case_luxidGroup :: Assertion
+case_luxidGroup =
+  HUnit.assertBool
+    "'luxidGroup' is invalid"
+    (isGroup AutoConf.luxidGroup)
+
+case_nodedUser :: Assertion
+case_nodedUser =
+  HUnit.assertBool
+    "'nodedUser' is invalid"
+    (isUser AutoConf.nodedUser)
+
+case_nodedGroup :: Assertion
+case_nodedGroup =
+  HUnit.assertBool
+    "'nodedGroup' is invalid"
+    (isGroup AutoConf.nodedGroup)
+
+case_mondUser :: Assertion
+case_mondUser =
+  HUnit.assertBool
+    "'mondUser' is invalid"
+    (isUser AutoConf.mondUser)
+
+case_mondGroup :: Assertion
+case_mondGroup =
+  HUnit.assertBool
+    "'mondGroup' is invalid"
+    (isUser AutoConf.mondGroup)
+
+case_diskSeparator :: Assertion
+case_diskSeparator =
+  HUnit.assertBool
+    "'diskSeparator' is invalid"
+    (not (null AutoConf.diskSeparator))
+
+case_qemuimgPath :: Assertion
+case_qemuimgPath =
+  HUnit.assertBool
+    "'qemuimgPath' is invalid"
+    (isFilePath AutoConf.qemuimgPath)
+
+TestHelper.testSuite "AutoConf"
+  [ 'case_versionSuffix
+  , 'case_localstatedir
+  , 'case_sysconfdir
+  , 'case_sshConfigDir
+  , 'case_sshLoginUser
+  , 'case_sshConsoleUser
+  , 'case_exportDir
+  , 'case_osSearchPath
+  , 'case_esSearchPath
+  , 'case_xenBootloader
+  , 'case_xenConfigDir
+  , 'case_xenKernel
+  , 'case_xenInitrd
+  , 'case_kvmKernel
+  , 'case_iallocatorSearchPath
+  , 'case_kvmPath
+  , 'case_ipPath
+  , 'case_socatPath
+  , 'case_toolsdir
+  , 'case_gntScripts
+  , 'case_htoolsProgs
+  , 'case_pkglibdir
+  , 'case_sharedir
+  , 'case_versionedsharedir
+  , 'case_drbdBarriers
+  , 'case_syslogUsage
+  , 'case_daemonsGroup
+  , 'case_adminGroup
+  , 'case_masterdUser
+  , 'case_masterdGroup
+  , 'case_rapiUser
+  , 'case_rapiGroup
+  , 'case_confdUser
+  , 'case_confdGroup
+  , 'case_luxidUser
+  , 'case_luxidGroup
+  , 'case_nodedUser
+  , 'case_nodedGroup
+  , 'case_mondUser
+  , 'case_mondGroup
+  , 'case_diskSeparator
+  , 'case_qemuimgPath ]
diff --git a/test/hs/Test/Ganeti/Constants.hs b/test/hs/Test/Ganeti/Constants.hs
new file mode 100644 (file)
index 0000000..2ac639c
--- /dev/null
@@ -0,0 +1,77 @@
+{-# LANGUAGE TemplateHaskell #-}
+{-| Unittests for constants
+
+-}
+
+{-
+
+Copyright (C) 2013 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 Test.Ganeti.Constants (testConstants) where
+
+import Test.HUnit (Assertion)
+import qualified Test.HUnit as HUnit
+
+import qualified Ganeti.Constants as Constants
+import qualified Ganeti.ConstantUtils as ConstantUtils
+import qualified Test.Ganeti.TestHelper as TestHelper
+
+{-# ANN module "HLint: ignore Use camelCase" #-}
+
+case_buildVersion :: Assertion
+case_buildVersion = do
+  HUnit.assertBool "Config major lower-bound violation"
+                   (Constants.configMajor >= 0)
+  HUnit.assertBool "Config major upper-bound violation"
+                   (Constants.configMajor <= 99)
+  HUnit.assertBool "Config minor lower-bound violation"
+                   (Constants.configMinor >= 0)
+  HUnit.assertBool "Config minor upper-bound violation"
+                   (Constants.configMinor <= 99)
+  HUnit.assertBool "Config revision lower-bound violation"
+                   (Constants.configRevision >= 0)
+  HUnit.assertBool "Config revision upper-bound violation"
+                   (Constants.configRevision <= 9999)
+  HUnit.assertBool "Config version lower-bound violation"
+                   (Constants.configVersion >= 0)
+  HUnit.assertBool "Config version upper-bound violation"
+                   (Constants.configVersion <= 99999999)
+  HUnit.assertEqual "Build version"
+                    (ConstantUtils.buildVersion 0 0 0) 0
+  HUnit.assertEqual "Build version"
+                    (ConstantUtils.buildVersion 10 10 1010) 10101010
+  HUnit.assertEqual "Build version"
+                    (ConstantUtils.buildVersion 12 34 5678) 12345678
+  HUnit.assertEqual "Build version"
+                    (ConstantUtils.buildVersion 99 99 9999) 99999999
+  HUnit.assertEqual "Build version"
+                    (ConstantUtils.buildVersion
+                     Constants.configMajor
+                     Constants.configMinor
+                     Constants.configRevision) Constants.configVersion
+  HUnit.assertEqual "Build version"
+                    (ConstantUtils.buildVersion
+                     Constants.configMajor
+                     Constants.configMinor
+                     Constants.configRevision) Constants.protocolVersion
+
+TestHelper.testSuite "Constants"
+  [ 'case_buildVersion
+  ]
index fe63b79..0555f7c 100644 (file)
@@ -48,6 +48,7 @@ import qualified Ganeti.HTools.Group as Group
 import qualified Ganeti.HTools.Instance as Instance
 import qualified Ganeti.HTools.Node as Node
 import qualified Ganeti.HTools.Types as Types
+import qualified Ganeti.Types as Types (EvacMode(..))
 
 {-# ANN module "HLint: ignore Use camelCase" #-}
 
diff --git a/test/hs/Test/Ganeti/HTools/ExtLoader.hs b/test/hs/Test/Ganeti/HTools/ExtLoader.hs
new file mode 100644 (file)
index 0000000..5a8162c
--- /dev/null
@@ -0,0 +1,112 @@
+{-| Unittests for the MonD data parse function -}
+
+{-
+
+Copyright (C) 2013 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 Test.Ganeti.HTools.ExtLoader where
+
+import Data.Ratio
+
+import qualified Test.HUnit as HUnit
+import qualified Text.JSON as J
+
+import qualified Ganeti.BasicTypes as BT
+import qualified Ganeti.DataCollectors.CPUload as CPUload
+
+import Ganeti.Cpu.Types (CPUavgload(..))
+import Ganeti.DataCollectors.Types (DCReport(..))
+import Ganeti.HTools.ExtLoader
+import Ganeti.JSON
+import Test.Ganeti.TestCommon
+
+{-# ANN module "HLint: ignore Use camelCase" #-}
+
+-- | Test a MonD data file.
+case_parseMonDData :: HUnit.Assertion
+case_parseMonDData = do
+  let mond_data_file = "mond-data.txt"
+      n1 = "node1.example.com"
+      n2 = "node2.example.com"
+      t1 = 1379507272000000000
+      t2 = 1379507280000000000
+      cpu_number1 = 4
+      cpu_number2 = 2
+      cpus1 = [ 0.04108859597350646,0.04456554528165781
+               , 0.06203619909502262,0.05595448881893895]
+      cpus2 = [0.004155409618511363,0.0034586452012150787]
+      cpu_total1 = 0.203643517607712
+      cpu_total2 = 0.007614031289927129
+      dcr1 = DCReport CPUload.dcName CPUload.dcVersion CPUload.dcFormatVersion
+               t1 CPUload.dcCategory CPUload.dcKind
+               (J.showJSON (CPUavgload cpu_number1 cpus1 cpu_total1))
+      dcr2 = DCReport CPUload.dcName CPUload.dcVersion CPUload.dcFormatVersion
+               t2 CPUload.dcCategory CPUload.dcKind
+               (J.showJSON (CPUavgload cpu_number2 cpus2 cpu_total2))
+      expected_list = [(n1,[dcr1]),(n2,[dcr2])]
+  ans <- readTestData mond_data_file
+  case pMonDData ans of
+    BT.Ok l -> HUnit.assertBool ("Parsing " ++ mond_data_file ++ " failed")
+                 (isAlEqual expected_list l)
+    BT.Bad s -> HUnit.assertFailure $ "Parsing failed: " ++ s
+
+-- | Check for quality two list of tuples.
+isAlEqual :: [(String, [DCReport])] -> [(String, [DCReport])] -> Bool
+isAlEqual a b = and (zipWith tupleIsAlEqual a b)
+
+-- | Check a tuple for quality.
+tupleIsAlEqual :: (String, [DCReport]) -> (String, [DCReport]) -> Bool
+tupleIsAlEqual (na, a) (nb, b) =
+  na == nb
+  && and (zipWith dcReportIsAlmostEqual a b)
+
+-- | Check if two DCReports are equal. Only reports from CPUload Data
+-- Collectors are supported.
+dcReportIsAlmostEqual :: DCReport -> DCReport -> Bool
+dcReportIsAlmostEqual a b =
+  dcReportName a == dcReportName b
+  && dcReportVersion a == dcReportVersion b
+  && dcReportFormatVersion a == dcReportFormatVersion b
+  && dcReportTimestamp a == dcReportTimestamp b
+  && dcReportCategory a == dcReportCategory b
+  && dcReportKind a == dcReportKind b
+  && case () of
+       _ | CPUload.dcName == dcReportName a ->
+             cpuavgloadDataIsAlmostEq (dcReportData a) (dcReportData b)
+         | otherwise -> False
+
+-- | Converts two JSValue objects and compares them.
+cpuavgloadDataIsAlmostEq :: J.JSValue -> J.JSValue -> Bool
+cpuavgloadDataIsAlmostEq a b =
+  case fromJVal a :: BT.Result CPUavgload of
+    BT.Bad _ -> False
+    BT.Ok cavA ->
+      case fromJVal b :: BT.Result CPUavgload of
+           BT.Bad _ -> False
+           BT.Ok cavB -> compareCPUavgload cavA cavB
+
+-- | Compares two CPuavgload objects.
+compareCPUavgload :: CPUavgload -> CPUavgload -> Bool
+compareCPUavgload a b =
+  let relError x y = relativeError x y <= 1e-9
+  in cavCpuNumber a == cavCpuNumber b
+     && relError (cavCpuTotal a) (cavCpuTotal b)
+     && length (cavCpus a) == length (cavCpus b)
+     && and (zipWith relError (cavCpus a) (cavCpus b))
index 1379878..4556a13 100644 (file)
@@ -31,7 +31,6 @@ module Test.Ganeti.HTools.Types
   , Types.AllocPolicy(..)
   , Types.DiskTemplate(..)
   , Types.FailMode(..)
-  , Types.EvacMode(..)
   , Types.ISpec(..)
   , Types.IPolicy(..)
   , nullIPolicy
@@ -41,7 +40,6 @@ import Test.QuickCheck hiding (Result)
 import Test.HUnit
 
 import Control.Applicative
-import Data.List (sort)
 import Control.Monad (replicateM)
 
 import Test.Ganeti.TestHelper
@@ -51,6 +49,7 @@ import Test.Ganeti.Types (allDiskTemplates)
 
 import Ganeti.BasicTypes
 import qualified Ganeti.Constants as C
+import Ganeti.ConstantUtils
 import qualified Ganeti.HTools.Types as Types
 
 {-# ANN module "HLint: ignore Use camelCase" #-}
@@ -61,8 +60,6 @@ import qualified Ganeti.HTools.Types as Types
 
 $(genArbitrary ''Types.FailMode)
 
-$(genArbitrary ''Types.EvacMode)
-
 instance Arbitrary a => Arbitrary (Types.OpResult a) where
   arbitrary = arbitrary >>= \c ->
               if c
@@ -155,9 +152,6 @@ prop_ISpec_serialisation = testSerialisation
 prop_IPolicy_serialisation :: Types.IPolicy -> Property
 prop_IPolicy_serialisation = testSerialisation
 
-prop_EvacMode_serialisation :: Types.EvacMode -> Property
-prop_EvacMode_serialisation = testSerialisation
-
 prop_opToResult :: Types.OpResult Int -> Property
 prop_opToResult op =
   case op of
@@ -185,22 +179,21 @@ case_AutoRepairType_sort = do
                  , Types.ArFailover
                  , Types.ArReinstall
                  ]
-      all_hs_raw = map Types.autoRepairTypeToRaw [minBound..maxBound]
+      all_hs_raw = mkSet $ map Types.autoRepairTypeToRaw [minBound..maxBound]
   assertEqual "Haskell order" expected [minBound..maxBound]
   assertEqual "consistent with Python" C.autoRepairAllTypes all_hs_raw
 
 -- | Test 'AutoRepairResult' type is equivalent with Python codebase.
 case_AutoRepairResult_pyequiv :: Assertion
 case_AutoRepairResult_pyequiv = do
-  let all_py_results = sort C.autoRepairAllResults
-      all_hs_results = sort $
+  let all_py_results = C.autoRepairAllResults
+      all_hs_results = mkSet $
                        map Types.autoRepairResultToRaw [minBound..maxBound]
   assertEqual "for AutoRepairResult equivalence" all_py_results all_hs_results
 
 testSuite "HTools/Types"
             [ 'prop_ISpec_serialisation
             , 'prop_IPolicy_serialisation
-            , 'prop_EvacMode_serialisation
             , 'prop_opToResult
             , 'prop_eitherToResult
             , 'case_AutoRepairType_sort
index 1951174..4d87e54 100644 (file)
@@ -120,22 +120,6 @@ testUptimeInfo fileName expectedContent = do
     Left msg -> assertFailure $ "Parsing failed: " ++ msg
     Right obtained -> assertEqual fileName expectedContent obtained
 
--- | Computes the relative error of two 'Double' numbers.
---
--- This is the \"relative error\" algorithm in
--- http:\/\/randomascii.wordpress.com\/2012\/02\/25\/
--- comparing-floating-point-numbers-2012-edition (URL split due to too
--- long line).
-relativeError :: Double -> Double -> Double
-relativeError d1 d2 =
-  let delta = abs $ d1 - d2
-      a1 = abs d1
-      a2 = abs d2
-      greatest = max a1 a2
-  in if delta == 0
-       then 0
-       else delta / greatest
-
 -- | Determines whether two LispConfig are equal, with the exception of Double
 -- values, that just need to be \"almost equal\".
 --
index e237336..9959ee5 100644 (file)
@@ -161,7 +161,7 @@ case_JobStatusPri_py_equiv = do
                Text.JSON.Ok jobs' -> return jobs'
                Error msg ->
                  assertFailure ("Unable to decode jobs: " ++ msg)
-                 -- this already raised an expection, but we need it
+                 -- this already raised an exception, but we need it
                  -- for proper types
                  >> fail "Unable to decode jobs"
   assertEqual "Mismatch in number of returned jobs"
index 6eb8c12..f687b05 100644 (file)
@@ -73,8 +73,12 @@ instance Arbitrary Luxi.LuxiOp where
                               listOf genFQDN <*> arbitrary
       Luxi.ReqQueryConfigValues -> Luxi.QueryConfigValues <$> genFields
       Luxi.ReqQueryClusterInfo -> pure Luxi.QueryClusterInfo
-      Luxi.ReqQueryTags -> Luxi.QueryTags <$> arbitrary
+      Luxi.ReqQueryTags -> do
+        kind <- arbitrary
+        Luxi.QueryTags kind <$> genLuxiTagName kind
       Luxi.ReqSubmitJob -> Luxi.SubmitJob <$> resize maxOpCodes arbitrary
+      Luxi.ReqSubmitJobToDrainedQueue -> Luxi.SubmitJobToDrainedQueue <$>
+                                         resize maxOpCodes arbitrary
       Luxi.ReqSubmitManyJobs -> Luxi.SubmitManyJobs <$>
                                 resize maxOpCodes arbitrary
       Luxi.ReqWaitForJobChange -> Luxi.WaitForJobChange <$> arbitrary <*>
index c583ef3..950e0e4 100644 (file)
@@ -45,7 +45,7 @@ import Text.Printf (printf)
 import Test.Ganeti.TestHelper
 import Test.Ganeti.TestCommon
 import Test.Ganeti.Types ()
-import Test.Ganeti.Query.Language
+import Test.Ganeti.Query.Language ()
 
 import Ganeti.BasicTypes
 import qualified Ganeti.Constants as C
@@ -58,12 +58,23 @@ import Ganeti.JSON
 
 -- * Arbitrary instances
 
-instance Arbitrary OpCodes.TagObject where
-  arbitrary = oneof [ OpCodes.TagInstance <$> genFQDN
-                    , OpCodes.TagNode     <$> genFQDN
-                    , OpCodes.TagGroup    <$> genFQDN
-                    , pure OpCodes.TagCluster
-                    ]
+instance (Ord k, Arbitrary k, Arbitrary a) => Arbitrary (Map.Map k a) where
+  arbitrary = Map.fromList <$> arbitrary
+
+arbitraryOpTagsGet :: Gen OpCodes.OpCode
+arbitraryOpTagsGet = do
+  kind <- arbitrary
+  OpCodes.OpTagsSet kind <$> arbitrary <*> genOpCodesTagName kind
+
+arbitraryOpTagsSet :: Gen OpCodes.OpCode
+arbitraryOpTagsSet = do
+  kind <- arbitrary
+  OpCodes.OpTagsSet kind <$> genTags <*> genOpCodesTagName kind
+
+arbitraryOpTagsDel :: Gen OpCodes.OpCode
+arbitraryOpTagsDel = do
+  kind <- arbitrary
+  OpCodes.OpTagsDel kind <$> genTags <*> genOpCodesTagName kind
 
 $(genArbitrary ''OpCodes.ReplaceDisksMode)
 
@@ -74,7 +85,9 @@ instance Arbitrary OpCodes.DiskIndex where
 
 instance Arbitrary INicParams where
   arbitrary = INicParams <$> genMaybe genNameNE <*> genMaybe genName <*>
-              genMaybe genNameNE <*> genMaybe genNameNE <*> genMaybe genNameNE
+              genMaybe genNameNE <*> genMaybe genNameNE <*>
+              genMaybe genNameNE <*> genMaybe genNameNE <*>
+              genMaybe genNameNE
 
 instance Arbitrary IDiskParams where
   arbitrary = IDiskParams <$> arbitrary <*> arbitrary <*>
@@ -117,33 +130,33 @@ instance Arbitrary OpCodes.OpCode where
       "OP_INSTANCE_FAILOVER" ->
         OpCodes.OpInstanceFailover <$> genFQDN <*> return Nothing <*>
         arbitrary <*> arbitrary <*> genMaybe genNodeNameNE <*>
-        return Nothing <*> arbitrary <*> genMaybe genNameNE <*> arbitrary
+        return Nothing <*> arbitrary <*> arbitrary <*> genMaybe genNameNE
       "OP_INSTANCE_MIGRATE" ->
         OpCodes.OpInstanceMigrate <$> genFQDN <*> return Nothing <*>
           arbitrary <*> arbitrary <*> genMaybe genNodeNameNE <*>
           return Nothing <*> arbitrary <*> arbitrary <*> arbitrary <*>
           genMaybe genNameNE <*> arbitrary
       "OP_TAGS_GET" ->
-        OpCodes.OpTagsGet <$> arbitrary <*> arbitrary
+        arbitraryOpTagsGet
       "OP_TAGS_SEARCH" ->
         OpCodes.OpTagsSearch <$> genNameNE
       "OP_TAGS_SET" ->
-        OpCodes.OpTagsSet <$> arbitrary <*> genTags
+        arbitraryOpTagsSet
       "OP_TAGS_DEL" ->
-        OpCodes.OpTagsSet <$> arbitrary <*> genTags
+        arbitraryOpTagsDel
       "OP_CLUSTER_POST_INIT" -> pure OpCodes.OpClusterPostInit
       "OP_CLUSTER_DESTROY" -> pure OpCodes.OpClusterDestroy
       "OP_CLUSTER_QUERY" -> pure OpCodes.OpClusterQuery
       "OP_CLUSTER_VERIFY" ->
         OpCodes.OpClusterVerify <$> arbitrary <*> arbitrary <*>
-          genSet Nothing <*> genSet Nothing <*> arbitrary <*>
+          genListSet Nothing <*> genListSet Nothing <*> arbitrary <*>
           genMaybe genNameNE
       "OP_CLUSTER_VERIFY_CONFIG" ->
         OpCodes.OpClusterVerifyConfig <$> arbitrary <*> arbitrary <*>
-          genSet Nothing <*> arbitrary
+          genListSet Nothing <*> arbitrary
       "OP_CLUSTER_VERIFY_GROUP" ->
         OpCodes.OpClusterVerifyGroup <$> genNameNE <*> arbitrary <*>
-          arbitrary <*> genSet Nothing <*> genSet Nothing <*> arbitrary
+          arbitrary <*> genListSet Nothing <*> genListSet Nothing <*> arbitrary
       "OP_CLUSTER_VERIFY_DISKS" -> pure OpCodes.OpClusterVerifyDisks
       "OP_GROUP_VERIFY_DISKS" ->
         OpCodes.OpGroupVerifyDisks <$> genNameNE
@@ -155,7 +168,7 @@ instance Arbitrary OpCodes.OpCode where
         OpCodes.OpClusterRename <$> genNameNE
       "OP_CLUSTER_SET_PARAMS" ->
         OpCodes.OpClusterSetParams <$> arbitrary <*> emptyMUD <*> emptyMUD <*>
-          arbitrary <*> genMaybe (listOf1 arbitrary >>= mkNonEmpty) <*>
+          arbitrary <*> genMaybe arbitrary <*>
           genMaybe genEmptyContainer <*> emptyMUD <*>
           genMaybe genEmptyContainer <*> genMaybe genEmptyContainer <*>
           genMaybe genEmptyContainer <*> genMaybe arbitrary <*>
@@ -164,15 +177,16 @@ instance Arbitrary OpCodes.OpCode where
           emptyMUD <*> emptyMUD <*> arbitrary <*>
           arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*>
           arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*>
-          genMaybe (genName >>= mkNonEmpty) <*>
-          genMaybe (genName >>= mkNonEmpty)
+          genMaybe genName <*>
+          genMaybe genName
       "OP_CLUSTER_REDIST_CONF" -> pure OpCodes.OpClusterRedistConf
       "OP_CLUSTER_ACTIVATE_MASTER_IP" ->
         pure OpCodes.OpClusterActivateMasterIp
       "OP_CLUSTER_DEACTIVATE_MASTER_IP" ->
         pure OpCodes.OpClusterDeactivateMasterIp
       "OP_QUERY" ->
-        OpCodes.OpQuery <$> arbitrary <*> arbitrary <*> arbitrary <*> genFilter
+        OpCodes.OpQuery <$> arbitrary <*> arbitrary <*> arbitrary <*>
+        pure Nothing
       "OP_QUERY_FIELDS" ->
         OpCodes.OpQueryFields <$> arbitrary <*> arbitrary
       "OP_OOB_COMMAND" ->
@@ -183,7 +197,7 @@ instance Arbitrary OpCodes.OpCode where
         OpCodes.OpNodeRemove <$> genNodeNameNE <*> return Nothing
       "OP_NODE_ADD" ->
         OpCodes.OpNodeAdd <$> genNodeNameNE <*> emptyMUD <*> emptyMUD <*>
-          genMaybe genName <*> genMaybe genNameNE <*> arbitrary <*>
+          genMaybe genNameNE <*> genMaybe genNameNE <*> arbitrary <*>
           genMaybe genNameNE <*> arbitrary <*> arbitrary <*> emptyMUD
       "OP_NODE_QUERY" ->
         OpCodes.OpNodeQuery <$> genFieldsNE <*> genNamesNE <*> arbitrary
@@ -191,13 +205,13 @@ instance Arbitrary OpCodes.OpCode where
         OpCodes.OpNodeQueryvols <$> arbitrary <*> genNodeNamesNE
       "OP_NODE_QUERY_STORAGE" ->
         OpCodes.OpNodeQueryStorage <$> arbitrary <*> arbitrary <*>
-          genNodeNamesNE <*> genNameNE
+          genNodeNamesNE <*> genMaybe genNameNE
       "OP_NODE_MODIFY_STORAGE" ->
         OpCodes.OpNodeModifyStorage <$> genNodeNameNE <*> return Nothing <*>
-          arbitrary <*> genNameNE <*> pure emptyJSObject
+          arbitrary <*> genMaybe genNameNE <*> pure emptyJSObject
       "OP_REPAIR_NODE_STORAGE" ->
         OpCodes.OpRepairNodeStorage <$> genNodeNameNE <*> return Nothing <*>
-          arbitrary <*> genNameNE <*> arbitrary
+          arbitrary <*> genMaybe genNameNE <*> arbitrary
       "OP_NODE_SET_PARAMS" ->
         OpCodes.OpNodeSetParams <$> genNodeNameNE <*> return Nothing <*>
           arbitrary <*> emptyMUD <*> emptyMUD <*> arbitrary <*> arbitrary <*>
@@ -216,21 +230,19 @@ instance Arbitrary OpCodes.OpCode where
           genMaybe genNameNE <*> arbitrary
       "OP_INSTANCE_CREATE" ->
         OpCodes.OpInstanceCreate <$> genFQDN <*> arbitrary <*>
-          arbitrary <*> arbitrary <*> arbitrary <*> pure emptyJSObject <*>
-          arbitrary <*> arbitrary <*> arbitrary <*> genMaybe genNameNE <*>
-          pure emptyJSObject <*> arbitrary <*> genMaybe genNameNE <*>
           arbitrary <*> arbitrary <*> arbitrary <*> arbitrary <*>
-          arbitrary <*> arbitrary <*> pure emptyJSObject <*>
-          genMaybe genNameNE <*>
-          genMaybe genNodeNameNE <*> return Nothing <*>
-          genMaybe genNodeNameNE <*> return Nothing <*>
-          genMaybe (pure []) <*> genMaybe genNodeNameNE <*>
-          arbitrary <*> genMaybe genNodeNameNE <*> return Nothing <*>
-          genMaybe genNodeNameNE <*> genMaybe genNameNE <*>
-          arbitrary <*> arbitrary <*> (genTags >>= mapM mkNonEmpty)
+          pure emptyJSObject <*> arbitrary <*> arbitrary <*> arbitrary <*>
+          genMaybe genNameNE <*> pure emptyJSObject <*> arbitrary <*>
+          genMaybe genNameNE <*> arbitrary <*> arbitrary <*> arbitrary <*>
+          arbitrary <*> arbitrary <*> arbitrary <*> pure emptyJSObject <*>
+          genMaybe genNameNE <*> genMaybe genNodeNameNE <*> return Nothing <*>
+          genMaybe genNodeNameNE <*> return Nothing <*> genMaybe (pure []) <*>
+          genMaybe genNodeNameNE <*> arbitrary <*> genMaybe genNodeNameNE <*>
+          return Nothing <*> genMaybe genNodeNameNE <*> genMaybe genNameNE <*>
+          arbitrary <*> (genTags >>= mapM mkNonEmpty)
       "OP_INSTANCE_MULTI_ALLOC" ->
-        OpCodes.OpInstanceMultiAlloc <$> genMaybe genNameNE <*> pure [] <*>
-          arbitrary
+        OpCodes.OpInstanceMultiAlloc <$> arbitrary <*> genMaybe genNameNE <*>
+        pure []
       "OP_INSTANCE_REINSTALL" ->
         OpCodes.OpInstanceReinstall <$> genFQDN <*> return Nothing <*>
           arbitrary <*> genMaybe genNameNE <*> genMaybe (pure emptyJSObject)
@@ -267,7 +279,7 @@ instance Arbitrary OpCodes.OpCode where
           arbitrary <*> genNodeNamesNE <*> return Nothing <*>
           genMaybe genNameNE
       "OP_INSTANCE_QUERY" ->
-        OpCodes.OpInstanceQuery <$> genFieldsNE <*> genNamesNE <*> arbitrary
+        OpCodes.OpInstanceQuery <$> genFieldsNE <*> arbitrary <*> genNamesNE
       "OP_INSTANCE_QUERY_DATA" ->
         OpCodes.OpInstanceQueryData <$> arbitrary <*>
           genNodeNamesNE <*> arbitrary
@@ -278,7 +290,7 @@ instance Arbitrary OpCodes.OpCode where
           pure emptyJSObject <*> arbitrary <*> genMaybe genNodeNameNE <*>
           return Nothing <*> genMaybe genNodeNameNE <*> return Nothing <*>
           genMaybe genNameNE <*> pure emptyJSObject <*> arbitrary <*>
-          arbitrary <*> arbitrary
+          arbitrary <*> arbitrary <*> arbitrary
       "OP_INSTANCE_GROW_DISK" ->
         OpCodes.OpInstanceGrowDisk <$> genFQDN <*> return Nothing <*>
           arbitrary <*> arbitrary <*> arbitrary <*> arbitrary
@@ -323,7 +335,7 @@ instance Arbitrary OpCodes.OpCode where
         OpCodes.OpBackupRemove <$> genFQDN <*> return Nothing
       "OP_TEST_ALLOCATOR" ->
         OpCodes.OpTestAllocator <$> arbitrary <*> arbitrary <*>
-          genNameNE <*> pure [] <*> pure [] <*>
+          genNameNE <*> genMaybe (pure []) <*> genMaybe (pure []) <*>
           arbitrary <*> genMaybe genNameNE <*>
           (genTags >>= mapM mkNonEmpty) <*>
           arbitrary <*> arbitrary <*> genMaybe genNameNE <*>
@@ -336,24 +348,24 @@ instance Arbitrary OpCodes.OpCode where
         OpCodes.OpTestDummy <$> pure J.JSNull <*> pure J.JSNull <*>
           pure J.JSNull <*> pure J.JSNull
       "OP_NETWORK_ADD" ->
-        OpCodes.OpNetworkAdd <$> genNameNE <*> genIp4Net <*>
-          genMaybe genIp4Addr <*> pure Nothing <*> pure Nothing <*>
-          genMaybe genMacPrefix <*> genMaybe (listOf genIp4Addr) <*>
+        OpCodes.OpNetworkAdd <$> genNameNE <*> genIPv4Network <*>
+          genMaybe genIPv4Address <*> pure Nothing <*> pure Nothing <*>
+          genMaybe genMacPrefix <*> genMaybe (listOf genIPv4Address) <*>
           arbitrary <*> (genTags >>= mapM mkNonEmpty)
       "OP_NETWORK_REMOVE" ->
         OpCodes.OpNetworkRemove <$> genNameNE <*> arbitrary
       "OP_NETWORK_SET_PARAMS" ->
         OpCodes.OpNetworkSetParams <$> genNameNE <*>
-          genMaybe genIp4Addr <*> pure Nothing <*> pure Nothing <*>
-          genMaybe genMacPrefix <*> genMaybe (listOf genIp4Addr) <*>
-          genMaybe (listOf genIp4Addr)
+          genMaybe genIPv4Address <*> pure Nothing <*> pure Nothing <*>
+          genMaybe genMacPrefix <*> genMaybe (listOf genIPv4Address) <*>
+          genMaybe (listOf genIPv4Address)
       "OP_NETWORK_CONNECT" ->
         OpCodes.OpNetworkConnect <$> genNameNE <*> genNameNE <*>
           arbitrary <*> genNameNE <*> arbitrary
       "OP_NETWORK_DISCONNECT" ->
         OpCodes.OpNetworkDisconnect <$> genNameNE <*> genNameNE
       "OP_NETWORK_QUERY" ->
-        OpCodes.OpNetworkQuery <$> genFieldsNE <*> genNamesNE <*> arbitrary
+        OpCodes.OpNetworkQuery <$> genFieldsNE <*> arbitrary <*> genNamesNE
       "OP_RESTRICTED_COMMAND" ->
         OpCodes.OpRestrictedCommand <$> arbitrary <*> genNodeNamesNE <*>
           return Nothing <*> genNameNE
@@ -441,8 +453,21 @@ prop_serialization = testSerialisation
 -- | Check that Python and Haskell defined the same opcode list.
 case_AllDefined :: HUnit.Assertion
 case_AllDefined = do
-  let py_ops = sort C.opcodesOpIds
-      hs_ops = sort OpCodes.allOpIDs
+  py_stdout <-
+     runPython "from ganeti import opcodes\n\
+               \from ganeti import serializer\n\
+               \import sys\n\
+               \print serializer.Dump([opid for opid in opcodes.OP_MAPPING])\n"
+               ""
+     >>= checkPythonResult
+  py_ops <- case J.decode py_stdout::J.Result [String] of
+               J.Ok ops -> return ops
+               J.Error msg ->
+                 HUnit.assertFailure ("Unable to decode opcode names: " ++ msg)
+                 -- this already raised an expection, but we need it
+                 -- for proper types
+                 >> fail "Unable to decode opcode names"
+  let hs_ops = sort OpCodes.allOpIDs
       extra_py = py_ops \\ hs_ops
       extra_hs = hs_ops \\ py_ops
   HUnit.assertBool ("Missing OpCodes from the Haskell code:\n" ++
@@ -481,8 +506,8 @@ case_py_compat_types = do
         ) opcodes
   py_stdout <-
      runPython "from ganeti import opcodes\n\
-               \import sys\n\
                \from ganeti import serializer\n\
+               \import sys\n\
                \op_data = serializer.Load(sys.stdin.read())\n\
                \decoded = [opcodes.OpCode.LoadOpCode(o) for o in op_data]\n\
                \for op in decoded:\n\
@@ -544,18 +569,6 @@ prop_setOpComment op comment =
   let (OpCodes.MetaOpCode common _) = OpCodes.setOpComment comment op
   in OpCodes.opComment common ==? Just comment
 
--- | Tests wrong tag object building (cluster takes only jsnull, the
--- other take a string, so we test the opposites).
-case_TagObject_fail :: Assertion
-case_TagObject_fail =
-  mapM_ (\(t, j) -> assertEqual (show t ++ "/" ++ J.encode j) Nothing $
-                    tagObjectFrom t j)
-    [ (TagTypeCluster,  J.showJSON "abc")
-    , (TagTypeInstance, J.JSNull)
-    , (TagTypeNode,     J.JSNull)
-    , (TagTypeGroup,    J.JSNull)
-    ]
-
 -- | Tests wrong (negative) disk index.
 prop_mkDiskIndex_fail :: QuickCheck.Positive Int -> Property
 prop_mkDiskIndex_fail (Positive i) =
@@ -595,7 +608,6 @@ testSuite "OpCodes"
             , 'case_py_compat_types
             , 'case_py_compat_fields
             , 'prop_setOpComment
-            , 'case_TagObject_fail
             , 'prop_mkDiskIndex_fail
             , 'case_readRecreateDisks_fail
             , 'case_readDdmOldChanges_fail
index 4246ffe..0f310de 100644 (file)
@@ -50,12 +50,13 @@ module Test.Ganeti.TestCommon
   , SmallRatio(..)
   , genSetHelper
   , genSet
-  , genIp4AddrStr
-  , genIp4Addr
-  , genIp4NetWithNetmask
-  , genIp4Net
+  , genListSet
+  , genIPv4Address
+  , genIPv4Network
   , genIp6Addr
   , genIp6Net
+  , genOpCodesTagName
+  , genLuxiTagName
   , netmask2NumHosts
   , testSerialisation
   , resultProp
@@ -64,6 +65,7 @@ module Test.Ganeti.TestCommon
   , testParser
   , genPropParser
   , genNonNegative
+  , relativeError
   ) where
 
 import Control.Applicative
@@ -279,34 +281,36 @@ genSetHelper candidates size = do
            newelem <- elements candidates `suchThat` (`Set.notMember` set)
            return (Set.insert newelem set)) Set.empty [1..size']
 
--- | Generates a set of arbitrary elements.
+-- | Generates a 'Set' of arbitrary elements.
 genSet :: (Ord a, Bounded a, Enum a) => Maybe Int -> Gen (Set.Set a)
 genSet = genSetHelper [minBound..maxBound]
 
--- | Generate an arbitrary IPv4 address in textual form (non empty).
-genIp4Addr :: Gen NonEmptyString
-genIp4Addr = genIp4AddrStr >>= mkNonEmpty
+-- | Generates a 'Set' of arbitrary elements wrapped in a 'ListSet'
+genListSet :: (Ord a, Bounded a, Enum a) => Maybe Int
+              -> Gen (BasicTypes.ListSet a)
+genListSet is = BasicTypes.ListSet <$> genSet is
 
 -- | Generate an arbitrary IPv4 address in textual form.
-genIp4AddrStr :: Gen String
-genIp4AddrStr = do
+genIPv4 :: Gen String
+genIPv4 = do
   a <- choose (1::Int, 255)
   b <- choose (0::Int, 255)
   c <- choose (0::Int, 255)
   d <- choose (0::Int, 255)
-  return $ intercalate "." (map show [a, b, c, d])
+  return . intercalate "." $ map show [a, b, c, d]
 
--- | Generates an arbitrary IPv4 address with a given netmask in textual form.
-genIp4NetWithNetmask :: Int -> Gen NonEmptyString
-genIp4NetWithNetmask netmask = do
-  ip <- genIp4AddrStr
-  mkNonEmpty $ ip ++ "/" ++ show netmask
+genIPv4Address :: Gen IPv4Address
+genIPv4Address = mkIPv4Address =<< genIPv4
 
 -- | Generate an arbitrary IPv4 network in textual form.
-genIp4Net :: Gen NonEmptyString
-genIp4Net = do
+genIPv4AddrRange :: Gen String
+genIPv4AddrRange = do
+  ip <- genIPv4
   netmask <- choose (8::Int, 30)
-  genIp4NetWithNetmask netmask
+  return $ ip ++ "/" ++ show netmask
+
+genIPv4Network :: Gen IPv4Network
+genIPv4Network = mkIPv4Network =<< genIPv4AddrRange
 
 -- | Helper function to compute the number of hosts in a network
 -- given the netmask. (For IPv4 only.)
@@ -329,6 +333,18 @@ genIp6Net = do
   ip <- genIp6Addr
   return $ ip ++ "/" ++ show netmask
 
+-- | Generates a valid, arbitrary tag name with respect to the given
+-- 'TagKind' for opcodes.
+genOpCodesTagName :: TagKind -> Gen (Maybe String)
+genOpCodesTagName TagKindCluster = return Nothing
+genOpCodesTagName _ = Just <$> genFQDN
+
+-- | Generates a valid, arbitrary tag name with respect to the given
+-- 'TagKind' for Luxi.
+genLuxiTagName :: TagKind -> Gen String
+genLuxiTagName TagKindCluster = return ""
+genLuxiTagName _ = genFQDN
+
 -- * Helper functions
 
 -- | Checks for serialisation idempotence.
@@ -389,3 +405,19 @@ genPropParser parser s expected =
 genNonNegative :: Gen Int
 genNonNegative =
   fmap fromIntegral (arbitrary::Gen (Test.QuickCheck.NonNegative Int))
+
+-- | Computes the relative error of two 'Double' numbers.
+--
+-- This is the \"relative error\" algorithm in
+-- http:\/\/randomascii.wordpress.com\/2012\/02\/25\/
+-- comparing-floating-point-numbers-2012-edition (URL split due to too
+-- long line).
+relativeError :: Double -> Double -> Double
+relativeError d1 d2 =
+  let delta = abs $ d1 - d2
+      a1 = abs d1
+      a2 = abs d2
+      greatest = max a1 a2
+  in if delta == 0
+       then 0
+       else delta / greatest
index c5ece4b..4315ecb 100644 (file)
@@ -37,7 +37,6 @@ module Test.Ganeti.Types
   , JobId(..)
   ) where
 
-import Data.List (sort)
 import Test.QuickCheck as QuickCheck hiding (Result)
 import Test.HUnit
 import qualified Text.JSON as J
@@ -47,6 +46,7 @@ import Test.Ganeti.TestCommon
 
 import Ganeti.BasicTypes
 import qualified Ganeti.Constants as C
+import qualified Ganeti.ConstantUtils as ConstantUtils
 import Ganeti.Types as Types
 import Ganeti.JSON
 
@@ -102,6 +102,8 @@ $(genArbitrary ''CVErrorCode)
 
 $(genArbitrary ''Hypervisor)
 
+$(genArbitrary ''TagKind)
+
 $(genArbitrary ''OobCommand)
 
 -- | Valid storage types.
@@ -113,7 +115,7 @@ allStorageTypes = [minBound..maxBound]::[StorageType]
 instance Arbitrary StorageType where
   arbitrary = elements allStorageTypes
 
-$(genArbitrary ''NodeEvacMode)
+$(genArbitrary ''EvacMode)
 
 $(genArbitrary ''FileDriver)
 
@@ -249,8 +251,9 @@ prop_CVErrorCode_serialisation = testSerialisation
 -- | Tests equivalence with Python, based on Constants.hs code.
 case_CVErrorCode_pyequiv :: Assertion
 case_CVErrorCode_pyequiv = do
-  let all_py_codes = sort C.cvAllEcodesStrings
-      all_hs_codes = sort $ map Types.cVErrorCodeToRaw [minBound..maxBound]
+  let all_py_codes = C.cvAllEcodesStrings
+      all_hs_codes = ConstantUtils.mkSet $
+                     map Types.cVErrorCodeToRaw [minBound..maxBound]
   assertEqual "for CVErrorCode equivalence" all_py_codes all_hs_codes
 
 -- | Test 'Hypervisor' serialisation.
@@ -266,7 +269,7 @@ prop_StorageType_serialisation :: StorageType -> Property
 prop_StorageType_serialisation = testSerialisation
 
 -- | Test 'NodeEvacMode' serialisation.
-prop_NodeEvacMode_serialisation :: NodeEvacMode -> Property
+prop_NodeEvacMode_serialisation :: EvacMode -> Property
 prop_NodeEvacMode_serialisation = testSerialisation
 
 -- | Test 'FileDriver' serialisation.
@@ -296,8 +299,9 @@ prop_IAllocatorMode_serialisation = testSerialisation
 -- | Tests equivalence with Python, based on Constants.hs code.
 case_IAllocatorMode_pyequiv :: Assertion
 case_IAllocatorMode_pyequiv = do
-  let all_py_codes = sort C.validIallocatorModes
-      all_hs_codes = sort $ map Types.iAllocatorModeToRaw [minBound..maxBound]
+  let all_py_codes = C.validIallocatorModes
+      all_hs_codes = ConstantUtils.mkSet $
+                     map Types.iAllocatorModeToRaw [minBound..maxBound]
   assertEqual "for IAllocatorMode equivalence" all_py_codes all_hs_codes
 
 -- | Test 'NICMode' serialisation.
@@ -327,8 +331,9 @@ case_JobStatus_order =
 -- | Tests equivalence with Python, based on Constants.hs code.
 case_NICMode_pyequiv :: Assertion
 case_NICMode_pyequiv = do
-  let all_py_codes = sort C.nicValidModes
-      all_hs_codes = sort $ map Types.nICModeToRaw [minBound..maxBound]
+  let all_py_codes = C.nicValidModes
+      all_hs_codes = ConstantUtils.mkSet $
+                     map Types.nICModeToRaw [minBound..maxBound]
   assertEqual "for NICMode equivalence" all_py_codes all_hs_codes
 
 -- | Test 'FinalizedJobStatus' serialisation.
@@ -338,9 +343,9 @@ prop_FinalizedJobStatus_serialisation = testSerialisation
 -- | Tests equivalence with Python, based on Constants.hs code.
 case_FinalizedJobStatus_pyequiv :: Assertion
 case_FinalizedJobStatus_pyequiv = do
-  let all_py_codes = sort C.jobsFinalized
-      all_hs_codes = sort $ map Types.finalizedJobStatusToRaw
-                            [minBound..maxBound]
+  let all_py_codes = C.jobsFinalized
+      all_hs_codes = ConstantUtils.mkSet $
+                     map Types.finalizedJobStatusToRaw [minBound..maxBound]
   assertEqual "for FinalizedJobStatus equivalence" all_py_codes all_hs_codes
 
 -- | Tests JobId serialisation (both from string and ints).
index 9c417bb..4f69197 100644 (file)
@@ -30,10 +30,12 @@ import Test.Framework
 import System.Environment (getArgs)
 import System.Log.Logger
 
+import Test.AutoConf
 import Test.Ganeti.TestImports ()
 import Test.Ganeti.Attoparsec
 import Test.Ganeti.BasicTypes
 import Test.Ganeti.Common
+import Test.Ganeti.Constants
 import Test.Ganeti.Confd.Utils
 import Test.Ganeti.Confd.Types
 import Test.Ganeti.Daemon
@@ -87,9 +89,11 @@ defOpts = TestOptions
 -- | All our defined tests.
 allTests :: [Test]
 allTests =
-  [ testBasicTypes
+  [ testAutoConf
+  , testBasicTypes
   , testAttoparsec
   , testCommon
+  , testConstants
   , testConfd_Types
   , testConfd_Utils
   , testDaemon
index 98b87b4..7208c48 100644 (file)
@@ -127,3 +127,13 @@ diff -u $T/simu-rebal-merged.tiered $T/simu-rebal.tiered.original
 ./test/hs/hbal -t$TESTDATA_DIR/n1-failure.data -G group-02
 >>>/Group size 0 nodes, 0 instances/
 >>>= 0
+
+# By default, hbal should assume equal, non-zero utilisation
+./test/hs/hbal -t$TESTDATA_DIR/hbal-dyn.data
+>>>/Solution length=1/
+>>>=0
+
+# ...but the --ignore-dynu option should be honored
+./test/hs/hbal -t$TESTDATA_DIR/hbal-dyn.data --ignore-dynu
+>>>/Cluster is already well balanced/
+>>>=0
diff --git a/test/py/__init__.py b/test/py/__init__.py
new file mode 100644 (file)
index 0000000..5e12339
--- /dev/null
@@ -0,0 +1,21 @@
+#
+#
+
+# Copyright (C) 2013 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 module contains all python test code"""
index ca1121c..9b15083 100755 (executable)
@@ -33,6 +33,8 @@ from ganeti import utils
 from ganeti import serializer
 from ganeti import netutils
 
+from ganeti.utils import version
+
 import testutils
 
 
@@ -40,9 +42,11 @@ def GetMinimalConfig():
   return {
     "version": constants.CONFIG_VERSION,
     "cluster": {
-      "master_node": "node1-uuid"
+      "master_node": "node1-uuid",
+      "ipolicy": None
     },
     "instances": {},
+    "networks": {},
     "nodegroups": {},
     "nodes": {
       "node1-uuid": {
@@ -196,7 +200,7 @@ class TestCfgupgrade(unittest.TestCase):
     self.assertFalse(os.path.exists(os.path.dirname(self.rapi_users_path)))
 
     utils.WriteFile(self.rapi_users_path_pre24, data="some user\n")
-    self._TestSimpleUpgrade(constants.BuildVersion(2, 3, 0), False)
+    self._TestSimpleUpgrade(version.BuildVersion(2, 3, 0), False)
 
     self.assertTrue(os.path.isdir(os.path.dirname(self.rapi_users_path)))
     self.assert_(os.path.islink(self.rapi_users_path_pre24))
@@ -212,7 +216,7 @@ class TestCfgupgrade(unittest.TestCase):
 
     os.mkdir(os.path.dirname(self.rapi_users_path))
     utils.WriteFile(self.rapi_users_path, data="other user\n")
-    self._TestSimpleUpgrade(constants.BuildVersion(2, 3, 0), False)
+    self._TestSimpleUpgrade(version.BuildVersion(2, 3, 0), False)
 
     self.assert_(os.path.islink(self.rapi_users_path_pre24))
     self.assert_(os.path.isfile(self.rapi_users_path))
@@ -229,7 +233,7 @@ class TestCfgupgrade(unittest.TestCase):
     os.symlink(self.rapi_users_path, self.rapi_users_path_pre24)
     utils.WriteFile(self.rapi_users_path, data="hello world\n")
 
-    self._TestSimpleUpgrade(constants.BuildVersion(2, 2, 0), False)
+    self._TestSimpleUpgrade(version.BuildVersion(2, 2, 0), False)
 
     self.assert_(os.path.isfile(self.rapi_users_path) and
                  not os.path.islink(self.rapi_users_path))
@@ -248,7 +252,7 @@ class TestCfgupgrade(unittest.TestCase):
     utils.WriteFile(self.rapi_users_path_pre24, data="hello world\n")
 
     self.assertRaises(Exception, self._TestSimpleUpgrade,
-                      constants.BuildVersion(2, 2, 0), False)
+                      version.BuildVersion(2, 2, 0), False)
 
     for path in [self.rapi_users_path, self.rapi_users_path_pre24]:
       self.assert_(os.path.isfile(path) and not os.path.islink(path))
@@ -261,7 +265,7 @@ class TestCfgupgrade(unittest.TestCase):
     self.assertFalse(os.path.exists(self.rapi_users_path_pre24))
 
     utils.WriteFile(self.rapi_users_path_pre24, data="some user\n")
-    self._TestSimpleUpgrade(constants.BuildVersion(2, 3, 0), True)
+    self._TestSimpleUpgrade(version.BuildVersion(2, 3, 0), True)
 
     self.assertFalse(os.path.isdir(os.path.dirname(self.rapi_users_path)))
     self.assertTrue(os.path.isfile(self.rapi_users_path_pre24) and
@@ -274,7 +278,7 @@ class TestCfgupgrade(unittest.TestCase):
 
     os.mkdir(os.path.dirname(self.rapi_users_path))
     utils.WriteFile(self.rapi_users_path, data="other user\n")
-    self._TestSimpleUpgrade(constants.BuildVersion(2, 3, 0), True)
+    self._TestSimpleUpgrade(version.BuildVersion(2, 3, 0), True)
 
     self.assertTrue(os.path.isfile(self.rapi_users_path) and
                     not os.path.islink(self.rapi_users_path))
@@ -289,7 +293,7 @@ class TestCfgupgrade(unittest.TestCase):
     os.symlink(self.rapi_users_path, self.rapi_users_path_pre24)
     utils.WriteFile(self.rapi_users_path, data="hello world\n")
 
-    self._TestSimpleUpgrade(constants.BuildVersion(2, 2, 0), True)
+    self._TestSimpleUpgrade(version.BuildVersion(2, 2, 0), True)
 
     self.assertTrue(os.path.islink(self.rapi_users_path_pre24))
     self.assertTrue(os.path.isfile(self.rapi_users_path) and
@@ -302,7 +306,7 @@ class TestCfgupgrade(unittest.TestCase):
   def testFileStoragePathsDryRun(self):
     self.assertFalse(os.path.exists(self.file_storage_paths))
 
-    self._TestSimpleUpgrade(constants.BuildVersion(2, 6, 0), True,
+    self._TestSimpleUpgrade(version.BuildVersion(2, 6, 0), True,
                             file_storage_dir=self.tmpdir,
                             shared_file_storage_dir="/tmp")
 
@@ -311,7 +315,7 @@ class TestCfgupgrade(unittest.TestCase):
   def testFileStoragePathsBoth(self):
     self.assertFalse(os.path.exists(self.file_storage_paths))
 
-    self._TestSimpleUpgrade(constants.BuildVersion(2, 6, 0), False,
+    self._TestSimpleUpgrade(version.BuildVersion(2, 6, 0), False,
                             file_storage_dir=self.tmpdir,
                             shared_file_storage_dir="/tmp")
 
@@ -327,7 +331,7 @@ class TestCfgupgrade(unittest.TestCase):
   def testFileStoragePathsSharedOnly(self):
     self.assertFalse(os.path.exists(self.file_storage_paths))
 
-    self._TestSimpleUpgrade(constants.BuildVersion(2, 5, 0), False,
+    self._TestSimpleUpgrade(version.BuildVersion(2, 5, 0), False,
                             file_storage_dir=None,
                             shared_file_storage_dir=self.tmpdir)
 
@@ -338,28 +342,28 @@ class TestCfgupgrade(unittest.TestCase):
     self.assertFalse(lines)
 
   def testUpgradeFrom_2_0(self):
-    self._TestSimpleUpgrade(constants.BuildVersion(2, 0, 0), False)
+    self._TestSimpleUpgrade(version.BuildVersion(2, 0, 0), False)
 
   def testUpgradeFrom_2_1(self):
-    self._TestSimpleUpgrade(constants.BuildVersion(2, 1, 0), False)
+    self._TestSimpleUpgrade(version.BuildVersion(2, 1, 0), False)
 
   def testUpgradeFrom_2_2(self):
-    self._TestSimpleUpgrade(constants.BuildVersion(2, 2, 0), False)
+    self._TestSimpleUpgrade(version.BuildVersion(2, 2, 0), False)
 
   def testUpgradeFrom_2_3(self):
-    self._TestSimpleUpgrade(constants.BuildVersion(2, 3, 0), False)
+    self._TestSimpleUpgrade(version.BuildVersion(2, 3, 0), False)
 
   def testUpgradeFrom_2_4(self):
-    self._TestSimpleUpgrade(constants.BuildVersion(2, 4, 0), False)
+    self._TestSimpleUpgrade(version.BuildVersion(2, 4, 0), False)
 
   def testUpgradeFrom_2_5(self):
-    self._TestSimpleUpgrade(constants.BuildVersion(2, 5, 0), False)
+    self._TestSimpleUpgrade(version.BuildVersion(2, 5, 0), False)
 
   def testUpgradeFrom_2_6(self):
-    self._TestSimpleUpgrade(constants.BuildVersion(2, 6, 0), False)
+    self._TestSimpleUpgrade(version.BuildVersion(2, 6, 0), False)
 
   def testUpgradeFrom_2_7(self):
-    self._TestSimpleUpgrade(constants.BuildVersion(2, 7, 0), False)
+    self._TestSimpleUpgrade(version.BuildVersion(2, 7, 0), False)
 
   def testUpgradeFullConfigFrom_2_7(self):
     self._TestUpgradeFromFile("cluster_config_2.7.json", False)
@@ -384,26 +388,27 @@ class TestCfgupgrade(unittest.TestCase):
   def testDowngradeFullConfig(self):
     """Test for upgrade + downgrade combination."""
     # This test can work only with the previous version of a configuration!
-    oldconfname = "cluster_config_2.8.json"
+    oldconfname = "cluster_config_2.9.json"
     self._TestUpgradeFromFile(oldconfname, False)
     _RunUpgrade(self.tmpdir, False, True, downgrade=True)
     oldconf = self._LoadTestDataConfig(oldconfname)
     newconf = self._LoadConfig()
-    self.assertEqual(oldconf, newconf)
 
-  def testDowngradeFrom_2_9(self):
-    cfg29_name = "cluster_config_2.9.json"
-    cfg29 = self._LoadTestDataConfig(cfg29_name)
-    self._CreateValidConfigDir()
-    utils.WriteFile(self.config_path, data=serializer.DumpJson(cfg29))
-    _RunUpgrade(self.tmpdir, False, True, downgrade=True)
-    cfg28 = self._LoadConfig()
+    # downgrade from 2.10 to 2.9 does not add physical_id to disks, which is ok
+    # TODO (2.11): Remove this code, it's not required to downgrade from 2.11
+    #              to 2.10
+    def RemovePhysicalId(disk):
+      if "children" in disk:
+        for d in disk["children"]:
+          RemovePhysicalId(d)
+      if "physical_id" in disk:
+        del disk["physical_id"]
+
+    for inst in oldconf["instances"].values():
+      for disk in inst["disks"]:
+        RemovePhysicalId(disk)
 
-    hvparams = cfg28["cluster"]["hvparams"]
-    for xen_variant in [constants.HT_XEN_PVM, constants.HT_XEN_HVM]:
-      xen_params = hvparams[xen_variant]
-      self.assertTrue(constants.HV_XEN_CMD not in xen_params)
-      self.assertTrue(constants.HV_VIF_SCRIPT not in xen_params)
+    self.assertEqual(oldconf, newconf)
 
   def testDowngradeFullConfigBackwardFrom_2_7(self):
     """Test for upgrade + downgrade + upgrade combination."""
@@ -427,25 +432,25 @@ class TestCfgupgrade(unittest.TestCase):
     self._RunDowngradeTwice()
 
   def testUpgradeDryRunFrom_2_0(self):
-    self._TestSimpleUpgrade(constants.BuildVersion(2, 0, 0), True)
+    self._TestSimpleUpgrade(version.BuildVersion(2, 0, 0), True)
 
   def testUpgradeDryRunFrom_2_1(self):
-    self._TestSimpleUpgrade(constants.BuildVersion(2, 1, 0), True)
+    self._TestSimpleUpgrade(version.BuildVersion(2, 1, 0), True)
 
   def testUpgradeDryRunFrom_2_2(self):
-    self._TestSimpleUpgrade(constants.BuildVersion(2, 2, 0), True)
+    self._TestSimpleUpgrade(version.BuildVersion(2, 2, 0), True)
 
   def testUpgradeDryRunFrom_2_3(self):
-    self._TestSimpleUpgrade(constants.BuildVersion(2, 3, 0), True)
+    self._TestSimpleUpgrade(version.BuildVersion(2, 3, 0), True)
 
   def testUpgradeDryRunFrom_2_4(self):
-    self._TestSimpleUpgrade(constants.BuildVersion(2, 4, 0), True)
+    self._TestSimpleUpgrade(version.BuildVersion(2, 4, 0), True)
 
   def testUpgradeDryRunFrom_2_5(self):
-    self._TestSimpleUpgrade(constants.BuildVersion(2, 5, 0), True)
+    self._TestSimpleUpgrade(version.BuildVersion(2, 5, 0), True)
 
   def testUpgradeDryRunFrom_2_6(self):
-    self._TestSimpleUpgrade(constants.BuildVersion(2, 6, 0), True)
+    self._TestSimpleUpgrade(version.BuildVersion(2, 6, 0), True)
 
   def testUpgradeCurrentDryRun(self):
     self._TestSimpleUpgrade(constants.CONFIG_VERSION, True)
diff --git a/test/py/cmdlib/__init__.py b/test/py/cmdlib/__init__.py
new file mode 100644 (file)
index 0000000..5d00d83
--- /dev/null
@@ -0,0 +1,21 @@
+#
+#
+
+# Copyright (C) 2013 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 module contains all cmdlib unit tests"""
diff --git a/test/py/cmdlib/backup_unittest.py b/test/py/cmdlib/backup_unittest.py
new file mode 100644 (file)
index 0000000..411f1ea
--- /dev/null
@@ -0,0 +1,210 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2013 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.
+
+
+"""Tests for LUBackup*"""
+
+from ganeti import constants
+from ganeti import objects
+from ganeti import opcodes
+from ganeti import query
+
+from testsupport import *
+
+import testutils
+
+
+class TestLUBackupQuery(CmdlibTestCase):
+  def setUp(self):
+    super(TestLUBackupQuery, self).setUp()
+
+    self.fields = query._BuildExportFields().keys()
+
+  def testFailingExportList(self):
+    self.rpc.call_export_list.return_value = \
+      self.RpcResultsBuilder() \
+        .AddFailedNode(self.master) \
+        .Build()
+    op = opcodes.OpBackupQuery(nodes=[self.master.name])
+    ret = self.ExecOpCode(op)
+    self.assertEqual({self.master.name: False}, ret)
+
+  def testQueryOneNode(self):
+    self.rpc.call_export_list.return_value = \
+      self.RpcResultsBuilder() \
+        .AddSuccessfulNode(self.master,
+                           ["mock_export1", "mock_export2"]) \
+        .Build()
+    op = opcodes.OpBackupQuery(nodes=[self.master.name])
+    ret = self.ExecOpCode(op)
+    self.assertEqual({self.master.name: ["mock_export1", "mock_export2"]}, ret)
+
+  def testQueryAllNodes(self):
+    node = self.cfg.AddNewNode()
+    self.rpc.call_export_list.return_value = \
+      self.RpcResultsBuilder() \
+        .AddSuccessfulNode(self.master, ["mock_export1"]) \
+        .AddSuccessfulNode(node, ["mock_export2"]) \
+        .Build()
+    op = opcodes.OpBackupQuery()
+    ret = self.ExecOpCode(op)
+    self.assertEqual({
+                       self.master.name: ["mock_export1"],
+                       node.name: ["mock_export2"]
+                     }, ret)
+
+
+class TestLUBackupPrepare(CmdlibTestCase):
+  @patchUtils("instance_utils")
+  def testPrepareLocalExport(self, utils):
+    utils.ReadOneLineFile.return_value = "cluster_secret"
+    inst = self.cfg.AddNewInstance()
+    op = opcodes.OpBackupPrepare(instance_name=inst.name,
+                                 mode=constants.EXPORT_MODE_LOCAL)
+    self.ExecOpCode(op)
+
+  @patchUtils("instance_utils")
+  def testPrepareRemoteExport(self, utils):
+    utils.ReadOneLineFile.return_value = "cluster_secret"
+    inst = self.cfg.AddNewInstance()
+    self.rpc.call_x509_cert_create.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(inst.primary_node,
+                                    ("key_name",
+                                     testutils.ReadTestData("cert1.pem")))
+    op = opcodes.OpBackupPrepare(instance_name=inst.name,
+                                 mode=constants.EXPORT_MODE_REMOTE)
+    self.ExecOpCode(op)
+
+
+class TestLUBackupExportBase(CmdlibTestCase):
+  def setUp(self):
+    super(TestLUBackupExportBase, self).setUp()
+
+    self.rpc.call_instance_start.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, True)
+
+    self.rpc.call_blockdev_assemble.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, ("/dev/mock_path",
+                                                  "/dev/mock_link_name"))
+
+    self.rpc.call_blockdev_shutdown.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, None)
+
+    self.rpc.call_blockdev_snapshot.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, ("mock_vg", "mock_id"))
+
+    self.rpc.call_blockdev_remove.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, None)
+
+    self.rpc.call_export_start.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, "export_daemon")
+
+    def ImpExpStatus(node_uuid, name):
+      return self.RpcResultsBuilder() \
+               .CreateSuccessfulNodeResult(node_uuid,
+                                           [objects.ImportExportStatus(
+                                             exit_status=0
+                                           )])
+    self.rpc.call_impexp_status.side_effect = ImpExpStatus
+
+    def ImpExpCleanup(node_uuid, name):
+      return self.RpcResultsBuilder() \
+               .CreateSuccessfulNodeResult(node_uuid)
+    self.rpc.call_impexp_cleanup.side_effect = ImpExpCleanup
+
+    self.rpc.call_finalize_export.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, None)
+
+  def testRemoveRunningInstanceWithoutShutdown(self):
+    inst = self.cfg.AddNewInstance(admin_state=constants.ADMINST_UP)
+    op = opcodes.OpBackupExport(instance_name=inst.name,
+                                target_node=self.master.name,
+                                shutdown=False,
+                                remove_instance=True)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Can not remove instance without shutting it down before")
+
+  def testUnsupportedDiskTemplate(self):
+    inst = self.cfg.AddNewInstance(disk_template=constants.DT_FILE)
+    op = opcodes.OpBackupExport(instance_name=inst.name,
+                                target_node=self.master.name)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Export not supported for instances with file-based disks")
+
+
+class TestLUBackupExportLocalExport(TestLUBackupExportBase):
+  def setUp(self):
+    super(TestLUBackupExportLocalExport, self).setUp()
+
+    self.inst = self.cfg.AddNewInstance()
+    self.target_node = self.cfg.AddNewNode()
+    self.op = opcodes.OpBackupExport(mode=constants.EXPORT_MODE_LOCAL,
+                                     instance_name=self.inst.name,
+                                     target_node=self.target_node.name)
+
+    self.rpc.call_import_start.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.target_node, "import_daemon")
+
+  def testExportWithShutdown(self):
+    inst = self.cfg.AddNewInstance(admin_state=constants.ADMINST_UP)
+    op = self.CopyOpCode(self.op, instance_name=inst.name, shutdown=True)
+    self.ExecOpCode(op)
+
+  def testExportDeactivatedDisks(self):
+    self.ExecOpCode(self.op)
+
+  def testExportRemoveInstance(self):
+    op = self.CopyOpCode(self.op, remove_instance=True)
+    self.ExecOpCode(op)
+
+
+class TestLUBackupExportRemoteExport(TestLUBackupExportBase):
+  def setUp(self):
+    super(TestLUBackupExportRemoteExport, self).setUp()
+
+    self.inst = self.cfg.AddNewInstance()
+    self.op = opcodes.OpBackupExport(mode=constants.EXPORT_MODE_REMOTE,
+                                     instance_name=self.inst.name,
+                                     target_node=[],
+                                     x509_key_name=["mock_key_name"],
+                                     destination_x509_ca="mock_dest_ca")
+
+  def testRemoteExportWithoutX509KeyName(self):
+    op = self.CopyOpCode(self.op, x509_key_name=self.REMOVE)
+    self.ExecOpCodeExpectOpPrereqError(op,
+                                       "Missing X509 key name for encryption")
+
+  def testRemoteExportWithoutX509DestCa(self):
+    op = self.CopyOpCode(self.op, destination_x509_ca=self.REMOVE)
+    self.ExecOpCodeExpectOpPrereqError(op,
+                                       "Missing destination X509 CA")
+
+
+if __name__ == "__main__":
+  testutils.GanetiTestProgram()
diff --git a/test/py/cmdlib/cluster_unittest.py b/test/py/cmdlib/cluster_unittest.py
new file mode 100644 (file)
index 0000000..15e504f
--- /dev/null
@@ -0,0 +1,2141 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2008, 2011, 2012, 2013 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.
+
+
+"""Tests for LUCluster*
+
+"""
+
+import OpenSSL
+
+import unittest
+import operator
+import os
+import tempfile
+import shutil
+
+from ganeti import constants
+from ganeti import errors
+from ganeti import netutils
+from ganeti import objects
+from ganeti import opcodes
+from ganeti import utils
+from ganeti import pathutils
+from ganeti import query
+from ganeti.cmdlib import cluster
+from ganeti.hypervisor import hv_xen
+
+from testsupport import *
+
+import testutils
+
+
+class TestCertVerification(testutils.GanetiTestCase):
+  def setUp(self):
+    testutils.GanetiTestCase.setUp(self)
+
+    self.tmpdir = tempfile.mkdtemp()
+
+  def tearDown(self):
+    shutil.rmtree(self.tmpdir)
+
+  def testVerifyCertificate(self):
+    cluster._VerifyCertificate(testutils.TestDataFilename("cert1.pem"))
+
+    nonexist_filename = os.path.join(self.tmpdir, "does-not-exist")
+
+    (errcode, msg) = cluster._VerifyCertificate(nonexist_filename)
+    self.assertEqual(errcode, cluster.LUClusterVerifyConfig.ETYPE_ERROR)
+
+    # Try to load non-certificate file
+    invalid_cert = testutils.TestDataFilename("bdev-net.txt")
+    (errcode, msg) = cluster._VerifyCertificate(invalid_cert)
+    self.assertEqual(errcode, cluster.LUClusterVerifyConfig.ETYPE_ERROR)
+
+
+class TestClusterVerifySsh(unittest.TestCase):
+  def testMultipleGroups(self):
+    fn = cluster.LUClusterVerifyGroup._SelectSshCheckNodes
+    mygroupnodes = [
+      objects.Node(name="node20", group="my", offline=False),
+      objects.Node(name="node21", group="my", offline=False),
+      objects.Node(name="node22", group="my", offline=False),
+      objects.Node(name="node23", group="my", offline=False),
+      objects.Node(name="node24", group="my", offline=False),
+      objects.Node(name="node25", group="my", offline=False),
+      objects.Node(name="node26", group="my", offline=True),
+      ]
+    nodes = [
+      objects.Node(name="node1", group="g1", offline=True),
+      objects.Node(name="node2", group="g1", offline=False),
+      objects.Node(name="node3", group="g1", offline=False),
+      objects.Node(name="node4", group="g1", offline=True),
+      objects.Node(name="node5", group="g1", offline=False),
+      objects.Node(name="node10", group="xyz", offline=False),
+      objects.Node(name="node11", group="xyz", offline=False),
+      objects.Node(name="node40", group="alloff", offline=True),
+      objects.Node(name="node41", group="alloff", offline=True),
+      objects.Node(name="node50", group="aaa", offline=False),
+      ] + mygroupnodes
+    assert not utils.FindDuplicates(map(operator.attrgetter("name"), nodes))
+
+    (online, perhost) = fn(mygroupnodes, "my", nodes)
+    self.assertEqual(online, ["node%s" % i for i in range(20, 26)])
+    self.assertEqual(set(perhost.keys()), set(online))
+
+    self.assertEqual(perhost, {
+      "node20": ["node10", "node2", "node50"],
+      "node21": ["node11", "node3", "node50"],
+      "node22": ["node10", "node5", "node50"],
+      "node23": ["node11", "node2", "node50"],
+      "node24": ["node10", "node3", "node50"],
+      "node25": ["node11", "node5", "node50"],
+      })
+
+  def testSingleGroup(self):
+    fn = cluster.LUClusterVerifyGroup._SelectSshCheckNodes
+    nodes = [
+      objects.Node(name="node1", group="default", offline=True),
+      objects.Node(name="node2", group="default", offline=False),
+      objects.Node(name="node3", group="default", offline=False),
+      objects.Node(name="node4", group="default", offline=True),
+      ]
+    assert not utils.FindDuplicates(map(operator.attrgetter("name"), nodes))
+
+    (online, perhost) = fn(nodes, "default", nodes)
+    self.assertEqual(online, ["node2", "node3"])
+    self.assertEqual(set(perhost.keys()), set(online))
+
+    self.assertEqual(perhost, {
+      "node2": [],
+      "node3": [],
+      })
+
+
+class TestLUClusterActivateMasterIp(CmdlibTestCase):
+  def testSuccess(self):
+    op = opcodes.OpClusterActivateMasterIp()
+
+    self.rpc.call_node_activate_master_ip.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master)
+
+    self.ExecOpCode(op)
+
+    self.rpc.call_node_activate_master_ip.assert_called_once_with(
+      self.master_uuid, self.cfg.GetMasterNetworkParameters(), False)
+
+  def testFailure(self):
+    op = opcodes.OpClusterActivateMasterIp()
+
+    self.rpc.call_node_activate_master_ip.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateFailedNodeResult(self.master) \
+
+    self.ExecOpCodeExpectOpExecError(op)
+
+
+class TestLUClusterDeactivateMasterIp(CmdlibTestCase):
+  def testSuccess(self):
+    op = opcodes.OpClusterDeactivateMasterIp()
+
+    self.rpc.call_node_deactivate_master_ip.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master)
+
+    self.ExecOpCode(op)
+
+    self.rpc.call_node_deactivate_master_ip.assert_called_once_with(
+      self.master_uuid, self.cfg.GetMasterNetworkParameters(), False)
+
+  def testFailure(self):
+    op = opcodes.OpClusterDeactivateMasterIp()
+
+    self.rpc.call_node_deactivate_master_ip.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateFailedNodeResult(self.master) \
+
+    self.ExecOpCodeExpectOpExecError(op)
+
+
+class TestLUClusterConfigQuery(CmdlibTestCase):
+  def testInvalidField(self):
+    op = opcodes.OpClusterConfigQuery(output_fields=["pinky_bunny"])
+
+    self.ExecOpCodeExpectOpPrereqError(op, "pinky_bunny")
+
+  def testAllFields(self):
+    op = opcodes.OpClusterConfigQuery(output_fields=query.CLUSTER_FIELDS.keys())
+
+    self.rpc.call_get_watcher_pause.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, -1)
+
+    ret = self.ExecOpCode(op)
+
+    self.assertEqual(1, self.rpc.call_get_watcher_pause.call_count)
+    self.assertEqual(len(ret), len(query.CLUSTER_FIELDS))
+
+  def testEmpytFields(self):
+    op = opcodes.OpClusterConfigQuery(output_fields=[])
+
+    self.ExecOpCode(op)
+
+    self.assertFalse(self.rpc.call_get_watcher_pause.called)
+
+
+class TestLUClusterDestroy(CmdlibTestCase):
+  def testExistingNodes(self):
+    op = opcodes.OpClusterDestroy()
+
+    self.cfg.AddNewNode()
+    self.cfg.AddNewNode()
+
+    self.ExecOpCodeExpectOpPrereqError(op, "still 2 node\(s\)")
+
+  def testExistingInstances(self):
+    op = opcodes.OpClusterDestroy()
+
+    self.cfg.AddNewInstance()
+    self.cfg.AddNewInstance()
+
+    self.ExecOpCodeExpectOpPrereqError(op, "still 2 instance\(s\)")
+
+  def testEmptyCluster(self):
+    op = opcodes.OpClusterDestroy()
+
+    self.ExecOpCode(op)
+
+    self.assertSingleHooksCall([self.master.name],
+                               "cluster-destroy",
+                               constants.HOOKS_PHASE_POST)
+
+
+class TestLUClusterPostInit(CmdlibTestCase):
+  def testExecuion(self):
+    op = opcodes.OpClusterPostInit()
+
+    self.ExecOpCode(op)
+
+    self.assertSingleHooksCall([self.master.name],
+                               "cluster-init",
+                               constants.HOOKS_PHASE_POST)
+
+
+class TestLUClusterQuery(CmdlibTestCase):
+  def testSimpleInvocation(self):
+    op = opcodes.OpClusterQuery()
+
+    self.ExecOpCode(op)
+
+  def testIPv6Cluster(self):
+    op = opcodes.OpClusterQuery()
+
+    self.cluster.primary_ip_family = netutils.IP6Address.family
+
+    self.ExecOpCode(op)
+
+
+class TestLUClusterRedistConf(CmdlibTestCase):
+  def testSimpleInvocation(self):
+    op = opcodes.OpClusterRedistConf()
+
+    self.ExecOpCode(op)
+
+
+class TestLUClusterRename(CmdlibTestCase):
+  NEW_NAME = "new-name.example.com"
+  NEW_IP = "203.0.113.100"
+
+  def testNoChanges(self):
+    op = opcodes.OpClusterRename(name=self.cfg.GetClusterName())
+
+    self.ExecOpCodeExpectOpPrereqError(op, "name nor the IP address")
+
+  def testReachableIp(self):
+    op = opcodes.OpClusterRename(name=self.NEW_NAME)
+
+    self.netutils_mod.GetHostname.return_value = \
+      HostnameMock(self.NEW_NAME, self.NEW_IP)
+    self.netutils_mod.TcpPing.return_value = True
+
+    self.ExecOpCodeExpectOpPrereqError(op, "is reachable on the network")
+
+  def testValidRename(self):
+    op = opcodes.OpClusterRename(name=self.NEW_NAME)
+
+    self.netutils_mod.GetHostname.return_value = \
+      HostnameMock(self.NEW_NAME, self.NEW_IP)
+
+    self.ExecOpCode(op)
+
+    self.assertEqual(1, self.ssh_mod.WriteKnownHostsFile.call_count)
+    self.rpc.call_node_deactivate_master_ip.assert_called_once_with(
+      self.master_uuid, self.cfg.GetMasterNetworkParameters(), False)
+    self.rpc.call_node_activate_master_ip.assert_called_once_with(
+      self.master_uuid, self.cfg.GetMasterNetworkParameters(), False)
+
+  def testRenameOfflineMaster(self):
+    op = opcodes.OpClusterRename(name=self.NEW_NAME)
+
+    self.master.offline = True
+    self.netutils_mod.GetHostname.return_value = \
+      HostnameMock(self.NEW_NAME, self.NEW_IP)
+
+    self.ExecOpCode(op)
+
+
+class TestLUClusterRepairDiskSizes(CmdlibTestCase):
+  def testNoInstances(self):
+    op = opcodes.OpClusterRepairDiskSizes()
+
+    self.ExecOpCode(op)
+
+  def _SetUpInstanceSingleDisk(self, dev_type=constants.DT_PLAIN):
+    pnode = self.master
+    snode = self.cfg.AddNewNode()
+
+    disk = self.cfg.CreateDisk(dev_type=dev_type,
+                               primary_node=pnode,
+                               secondary_node=snode)
+    inst = self.cfg.AddNewInstance(disks=[disk])
+
+    return (inst, disk)
+
+  def testSingleInstanceOnFailingNode(self):
+    (inst, _) = self._SetUpInstanceSingleDisk()
+    op = opcodes.OpClusterRepairDiskSizes(instances=[inst.name])
+
+    self.rpc.call_blockdev_getdimensions.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateFailedNodeResult(self.master)
+
+    self.ExecOpCode(op)
+
+    self.mcpu.assertLogContainsRegex("Failure in blockdev_getdimensions")
+
+  def _ExecOpClusterRepairDiskSizes(self, node_data):
+    # not specifying instances repairs all
+    op = opcodes.OpClusterRepairDiskSizes()
+
+    self.rpc.call_blockdev_getdimensions.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, node_data)
+
+    return self.ExecOpCode(op)
+
+  def testInvalidResultData(self):
+    for data in [[], [None], ["invalid"], [("still", "invalid")]]:
+      self.ResetMocks()
+
+      self._SetUpInstanceSingleDisk()
+      self._ExecOpClusterRepairDiskSizes(data)
+
+      self.mcpu.assertLogContainsRegex("ignoring")
+
+  def testCorrectSize(self):
+    self._SetUpInstanceSingleDisk()
+    changed = self._ExecOpClusterRepairDiskSizes([(1024 * 1024 * 1024, None)])
+    self.mcpu.assertLogIsEmpty()
+    self.assertEqual(0, len(changed))
+
+  def testWrongSize(self):
+    self._SetUpInstanceSingleDisk()
+    changed = self._ExecOpClusterRepairDiskSizes([(512 * 1024 * 1024, None)])
+    self.assertEqual(1, len(changed))
+
+  def testCorrectDRBD(self):
+    self._SetUpInstanceSingleDisk(dev_type=constants.DT_DRBD8)
+    changed = self._ExecOpClusterRepairDiskSizes([(1024 * 1024 * 1024, None)])
+    self.mcpu.assertLogIsEmpty()
+    self.assertEqual(0, len(changed))
+
+  def testWrongDRBDChild(self):
+    (_, disk) = self._SetUpInstanceSingleDisk(dev_type=constants.DT_DRBD8)
+    disk.children[0].size = 512
+    changed = self._ExecOpClusterRepairDiskSizes([(1024 * 1024 * 1024, None)])
+    self.assertEqual(1, len(changed))
+
+  def testExclusiveStorageInvalidResultData(self):
+    self._SetUpInstanceSingleDisk()
+    self.master.ndparams[constants.ND_EXCLUSIVE_STORAGE] = True
+    self._ExecOpClusterRepairDiskSizes([(1024 * 1024 * 1024, None)])
+
+    self.mcpu.assertLogContainsRegex(
+      "did not return valid spindles information")
+
+  def testExclusiveStorageCorrectSpindles(self):
+    (_, disk) = self._SetUpInstanceSingleDisk()
+    disk.spindles = 1
+    self.master.ndparams[constants.ND_EXCLUSIVE_STORAGE] = True
+    changed = self._ExecOpClusterRepairDiskSizes([(1024 * 1024 * 1024, 1)])
+    self.assertEqual(0, len(changed))
+
+  def testExclusiveStorageWrongSpindles(self):
+    self._SetUpInstanceSingleDisk()
+    self.master.ndparams[constants.ND_EXCLUSIVE_STORAGE] = True
+    changed = self._ExecOpClusterRepairDiskSizes([(1024 * 1024 * 1024, 1)])
+    self.assertEqual(1, len(changed))
+
+
+class TestLUClusterSetParams(CmdlibTestCase):
+  UID_POOL = [(10, 1000)]
+
+  def testUidPool(self):
+    op = opcodes.OpClusterSetParams(uid_pool=self.UID_POOL)
+    self.ExecOpCode(op)
+    self.assertEqual(self.UID_POOL, self.cluster.uid_pool)
+
+  def testAddUids(self):
+    old_pool = [(1, 9)]
+    self.cluster.uid_pool = list(old_pool)
+    op = opcodes.OpClusterSetParams(add_uids=self.UID_POOL)
+    self.ExecOpCode(op)
+    self.assertEqual(set(self.UID_POOL + old_pool),
+                     set(self.cluster.uid_pool))
+
+  def testRemoveUids(self):
+    additional_pool = [(1, 9)]
+    self.cluster.uid_pool = self.UID_POOL + additional_pool
+    op = opcodes.OpClusterSetParams(remove_uids=self.UID_POOL)
+    self.ExecOpCode(op)
+    self.assertEqual(additional_pool, self.cluster.uid_pool)
+
+  def testMasterNetmask(self):
+    op = opcodes.OpClusterSetParams(master_netmask=26)
+    self.ExecOpCode(op)
+    self.assertEqual(26, self.cluster.master_netmask)
+
+  def testInvalidDiskparams(self):
+    for diskparams in [{constants.DT_DISKLESS: {constants.LV_STRIPES: 0}},
+                       {constants.DT_DRBD8: {constants.RBD_POOL: "pool"}},
+                       {constants.DT_DRBD8: {constants.RBD_ACCESS: "bunny"}}]:
+      self.ResetMocks()
+      op = opcodes.OpClusterSetParams(diskparams=diskparams)
+      self.ExecOpCodeExpectOpPrereqError(op, "verify diskparams")
+
+  def testValidDiskparams(self):
+    diskparams = {constants.DT_RBD: {constants.RBD_POOL: "mock_pool",
+                                     constants.RBD_ACCESS: "kernelspace"}}
+    op = opcodes.OpClusterSetParams(diskparams=diskparams)
+    self.ExecOpCode(op)
+    self.assertEqual(diskparams[constants.DT_RBD],
+                     self.cluster.diskparams[constants.DT_RBD])
+
+  def testMinimalDiskparams(self):
+    diskparams = {constants.DT_RBD: {constants.RBD_POOL: "mock_pool"}}
+    self.cluster.diskparams = {}
+    op = opcodes.OpClusterSetParams(diskparams=diskparams)
+    self.ExecOpCode(op)
+    self.assertEqual(diskparams, self.cluster.diskparams)
+
+  def testValidDiskparamsAccess(self):
+    for value in constants.DISK_VALID_ACCESS_MODES:
+      self.ResetMocks()
+      op = opcodes.OpClusterSetParams(diskparams={
+        constants.DT_RBD: {constants.RBD_ACCESS: value}
+      })
+      self.ExecOpCode(op)
+      got = self.cluster.diskparams[constants.DT_RBD][constants.RBD_ACCESS]
+      self.assertEqual(value, got)
+
+  def testInvalidDiskparamsAccess(self):
+    for value in ["default", "pinky_bunny"]:
+      self.ResetMocks()
+      op = opcodes.OpClusterSetParams(diskparams={
+        constants.DT_RBD: {constants.RBD_ACCESS: value}
+      })
+      self.ExecOpCodeExpectOpPrereqError(op, "Invalid value of 'rbd:access'")
+
+  def testUnsetDrbdHelperWithDrbdDisks(self):
+    self.cfg.AddNewInstance(disks=[
+      self.cfg.CreateDisk(dev_type=constants.DT_DRBD8, create_nodes=True)])
+    op = opcodes.OpClusterSetParams(drbd_helper="")
+    self.ExecOpCodeExpectOpPrereqError(op, "Cannot disable drbd helper")
+
+  def testFileStorageDir(self):
+    op = opcodes.OpClusterSetParams(file_storage_dir="/random/path")
+    self.ExecOpCode(op)
+
+  def testSetFileStorageDirToCurrentValue(self):
+    op = opcodes.OpClusterSetParams(
+           file_storage_dir=self.cluster.file_storage_dir)
+    self.ExecOpCode(op)
+
+    self.mcpu.assertLogContainsRegex("file storage dir already set to value")
+
+  def testUnsetFileStorageDirFileStorageEnabled(self):
+    self.cfg.SetEnabledDiskTemplates([constants.DT_FILE])
+    op = opcodes.OpClusterSetParams(file_storage_dir='')
+    self.ExecOpCodeExpectOpPrereqError(op, "Unsetting the 'file' storage")
+
+  def testUnsetFileStorageDirFileStorageDisabled(self):
+    self.cfg.SetEnabledDiskTemplates([constants.DT_PLAIN])
+    op = opcodes.OpClusterSetParams(file_storage_dir='')
+    self.ExecOpCode(op)
+
+  def testSetFileStorageDirFileStorageDisabled(self):
+    self.cfg.SetEnabledDiskTemplates([constants.DT_PLAIN])
+    op = opcodes.OpClusterSetParams(file_storage_dir='/some/path/')
+    self.ExecOpCode(op)
+    self.mcpu.assertLogContainsRegex("although file storage is not enabled")
+
+  def testValidDrbdHelper(self):
+    node1 = self.cfg.AddNewNode()
+    node1.offline = True
+    self.rpc.call_drbd_helper.return_value = \
+      self.RpcResultsBuilder() \
+        .AddSuccessfulNode(self.master, "/bin/true") \
+        .AddOfflineNode(node1) \
+        .Build()
+    op = opcodes.OpClusterSetParams(drbd_helper="/bin/true")
+    self.ExecOpCode(op)
+    self.mcpu.assertLogContainsRegex("Not checking drbd helper on offline node")
+
+  def testDrbdHelperFailingNode(self):
+    self.rpc.call_drbd_helper.return_value = \
+      self.RpcResultsBuilder() \
+        .AddFailedNode(self.master) \
+        .Build()
+    op = opcodes.OpClusterSetParams(drbd_helper="/bin/true")
+    self.ExecOpCodeExpectOpPrereqError(op, "Error checking drbd helper")
+
+  def testInvalidDrbdHelper(self):
+    self.rpc.call_drbd_helper.return_value = \
+      self.RpcResultsBuilder() \
+        .AddSuccessfulNode(self.master, "/bin/false") \
+        .Build()
+    op = opcodes.OpClusterSetParams(drbd_helper="/bin/true")
+    self.ExecOpCodeExpectOpPrereqError(op, "drbd helper is /bin/false")
+
+  def testDrbdHelperWithoutDrbdDiskTemplate(self):
+    drbd_helper = "/bin/random_helper"
+    self.cfg.SetEnabledDiskTemplates([constants.DT_DISKLESS])
+    self.rpc.call_drbd_helper.return_value = \
+      self.RpcResultsBuilder() \
+        .AddSuccessfulNode(self.master, drbd_helper) \
+        .Build()
+    op = opcodes.OpClusterSetParams(drbd_helper=drbd_helper)
+    self.ExecOpCode(op)
+
+    self.mcpu.assertLogContainsRegex("but did not enable")
+
+  def testResetDrbdHelperDrbdDisabled(self):
+    drbd_helper = ""
+    self.cfg.SetEnabledDiskTemplates([constants.DT_DISKLESS])
+    op = opcodes.OpClusterSetParams(drbd_helper=drbd_helper)
+    self.ExecOpCode(op)
+
+    self.assertEqual(None, self.cluster.drbd_usermode_helper)
+
+  def testResetDrbdHelperDrbdEnabled(self):
+    drbd_helper = ""
+    self.cluster.enabled_disk_templates = [constants.DT_DRBD8]
+    op = opcodes.OpClusterSetParams(drbd_helper=drbd_helper)
+    self.ExecOpCodeExpectOpPrereqError(
+        op, "Cannot disable drbd helper while DRBD is enabled.")
+
+  def testEnableDrbdNoHelper(self):
+    self.cluster.enabled_disk_templates = [constants.DT_DISKLESS]
+    self.cluster.drbd_usermode_helper = None
+    enabled_disk_templates = [constants.DT_DRBD8]
+    op = opcodes.OpClusterSetParams(
+        enabled_disk_templates=enabled_disk_templates)
+    self.ExecOpCodeExpectOpPrereqError(
+        op, "Cannot enable DRBD without a DRBD usermode helper set")
+
+  def testEnableDrbdHelperSet(self):
+    drbd_helper = "/bin/random_helper"
+    self.rpc.call_drbd_helper.return_value = \
+      self.RpcResultsBuilder() \
+        .AddSuccessfulNode(self.master, drbd_helper) \
+        .Build()
+    self.cfg.SetEnabledDiskTemplates([constants.DT_DISKLESS])
+    self.cluster.drbd_usermode_helper = drbd_helper
+    enabled_disk_templates = [constants.DT_DRBD8]
+    op = opcodes.OpClusterSetParams(
+        enabled_disk_templates=enabled_disk_templates,
+        ipolicy={constants.IPOLICY_DTS: enabled_disk_templates})
+    self.ExecOpCode(op)
+
+    self.assertEqual(drbd_helper, self.cluster.drbd_usermode_helper)
+
+  def testDrbdHelperAlreadySet(self):
+    drbd_helper = "/bin/true"
+    self.rpc.call_drbd_helper.return_value = \
+      self.RpcResultsBuilder() \
+        .AddSuccessfulNode(self.master, "/bin/true") \
+        .Build()
+    self.cfg.SetEnabledDiskTemplates([constants.DT_DISKLESS])
+    op = opcodes.OpClusterSetParams(drbd_helper=drbd_helper)
+    self.ExecOpCode(op)
+
+    self.assertEqual(drbd_helper, self.cluster.drbd_usermode_helper)
+    self.mcpu.assertLogContainsRegex("DRBD helper already in desired state")
+
+  def testSetDrbdHelper(self):
+    drbd_helper = "/bin/true"
+    self.rpc.call_drbd_helper.return_value = \
+      self.RpcResultsBuilder() \
+        .AddSuccessfulNode(self.master, "/bin/true") \
+        .Build()
+    self.cluster.drbd_usermode_helper = "/bin/false"
+    self.cfg.SetEnabledDiskTemplates([constants.DT_DRBD8])
+    op = opcodes.OpClusterSetParams(drbd_helper=drbd_helper)
+    self.ExecOpCode(op)
+
+    self.assertEqual(drbd_helper, self.cluster.drbd_usermode_helper)
+
+  def testBeparams(self):
+    beparams = {constants.BE_VCPUS: 32}
+    op = opcodes.OpClusterSetParams(beparams=beparams)
+    self.ExecOpCode(op)
+    self.assertEqual(32, self.cluster
+                           .beparams[constants.PP_DEFAULT][constants.BE_VCPUS])
+
+  def testNdparams(self):
+    ndparams = {constants.ND_EXCLUSIVE_STORAGE: True}
+    op = opcodes.OpClusterSetParams(ndparams=ndparams)
+    self.ExecOpCode(op)
+    self.assertEqual(True, self.cluster
+                             .ndparams[constants.ND_EXCLUSIVE_STORAGE])
+
+  def testNdparamsResetOobProgram(self):
+    ndparams = {constants.ND_OOB_PROGRAM: ""}
+    op = opcodes.OpClusterSetParams(ndparams=ndparams)
+    self.ExecOpCode(op)
+    self.assertEqual(constants.NDC_DEFAULTS[constants.ND_OOB_PROGRAM],
+                     self.cluster.ndparams[constants.ND_OOB_PROGRAM])
+
+  def testHvState(self):
+    hv_state = {constants.HT_FAKE: {constants.HVST_CPU_TOTAL: 8}}
+    op = opcodes.OpClusterSetParams(hv_state=hv_state)
+    self.ExecOpCode(op)
+    self.assertEqual(8, self.cluster.hv_state_static
+                          [constants.HT_FAKE][constants.HVST_CPU_TOTAL])
+
+  def testDiskState(self):
+    disk_state = {
+      constants.DT_PLAIN: {
+        "mock_vg": {constants.DS_DISK_TOTAL: 10}
+      }
+    }
+    op = opcodes.OpClusterSetParams(disk_state=disk_state)
+    self.ExecOpCode(op)
+    self.assertEqual(10, self.cluster
+                           .disk_state_static[constants.DT_PLAIN]["mock_vg"]
+                             [constants.DS_DISK_TOTAL])
+
+  def testDefaultIPolicy(self):
+    ipolicy = constants.IPOLICY_DEFAULTS
+    op = opcodes.OpClusterSetParams(ipolicy=ipolicy)
+    self.ExecOpCode(op)
+
+  def testIPolicyNewViolation(self):
+    import ganeti.constants as C
+    ipolicy = C.IPOLICY_DEFAULTS
+    ipolicy[C.ISPECS_MINMAX][0][C.ISPECS_MIN][C.ISPEC_MEM_SIZE] = 128
+    ipolicy[C.ISPECS_MINMAX][0][C.ISPECS_MAX][C.ISPEC_MEM_SIZE] = 128
+
+    self.cfg.AddNewInstance(beparams={C.BE_MINMEM: 512, C.BE_MAXMEM: 512})
+    op = opcodes.OpClusterSetParams(ipolicy=ipolicy)
+    self.ExecOpCode(op)
+
+    self.mcpu.assertLogContainsRegex("instances violate them")
+
+  def testNicparamsNoInstance(self):
+    nicparams = {
+      constants.NIC_LINK: "mock_bridge"
+    }
+    op = opcodes.OpClusterSetParams(nicparams=nicparams)
+    self.ExecOpCode(op)
+
+    self.assertEqual("mock_bridge",
+                     self.cluster.nicparams
+                       [constants.PP_DEFAULT][constants.NIC_LINK])
+
+  def testNicparamsInvalidConf(self):
+    nicparams = {
+      constants.NIC_MODE: constants.NIC_MODE_BRIDGED,
+      constants.NIC_LINK: ""
+    }
+    op = opcodes.OpClusterSetParams(nicparams=nicparams)
+    self.ExecOpCodeExpectException(op, errors.ConfigurationError, "NIC link")
+
+  def testNicparamsInvalidInstanceConf(self):
+    nicparams = {
+      constants.NIC_MODE: constants.NIC_MODE_BRIDGED,
+      constants.NIC_LINK: "mock_bridge"
+    }
+    self.cfg.AddNewInstance(nics=[
+      self.cfg.CreateNic(nicparams={constants.NIC_LINK: None})])
+    op = opcodes.OpClusterSetParams(nicparams=nicparams)
+    self.ExecOpCodeExpectOpPrereqError(op, "Missing bridged NIC link")
+
+  def testNicparamsMissingIp(self):
+    nicparams = {
+      constants.NIC_MODE: constants.NIC_MODE_ROUTED
+    }
+    self.cfg.AddNewInstance()
+    op = opcodes.OpClusterSetParams(nicparams=nicparams)
+    self.ExecOpCodeExpectOpPrereqError(op, "routed NIC with no ip address")
+
+  def testNicparamsWithInstance(self):
+    nicparams = {
+      constants.NIC_LINK: "mock_bridge"
+    }
+    self.cfg.AddNewInstance()
+    op = opcodes.OpClusterSetParams(nicparams=nicparams)
+    self.ExecOpCode(op)
+
+  def testDefaultHvparams(self):
+    hvparams = constants.HVC_DEFAULTS
+    op = opcodes.OpClusterSetParams(hvparams=hvparams)
+    self.ExecOpCode(op)
+
+    self.assertEqual(hvparams, self.cluster.hvparams)
+
+  def testMinimalHvparams(self):
+    hvparams = {
+      constants.HT_FAKE: {
+        constants.HV_MIGRATION_MODE: constants.HT_MIGRATION_NONLIVE
+      }
+    }
+    self.cluster.hvparams = {}
+    op = opcodes.OpClusterSetParams(hvparams=hvparams)
+    self.ExecOpCode(op)
+
+    self.assertEqual(hvparams, self.cluster.hvparams)
+
+  def testOsHvp(self):
+    os_hvp = {
+      "mocked_os": {
+        constants.HT_FAKE: {
+          constants.HV_MIGRATION_MODE: constants.HT_MIGRATION_NONLIVE
+        }
+      },
+      "other_os": constants.HVC_DEFAULTS
+    }
+    op = opcodes.OpClusterSetParams(os_hvp=os_hvp)
+    self.ExecOpCode(op)
+
+    self.assertEqual(constants.HT_MIGRATION_NONLIVE,
+                     self.cluster.os_hvp["mocked_os"][constants.HT_FAKE]
+                       [constants.HV_MIGRATION_MODE])
+    self.assertEqual(constants.HVC_DEFAULTS, self.cluster.os_hvp["other_os"])
+
+  def testRemoveOsHvp(self):
+    os_hvp = {"mocked_os": {constants.HT_FAKE: None}}
+    op = opcodes.OpClusterSetParams(os_hvp=os_hvp)
+    self.ExecOpCode(op)
+
+    assert constants.HT_FAKE not in self.cluster.os_hvp["mocked_os"]
+
+  def testDefaultOsHvp(self):
+    os_hvp = {"mocked_os": constants.HVC_DEFAULTS.copy()}
+    self.cluster.os_hvp = {"mocked_os": {}}
+    op = opcodes.OpClusterSetParams(os_hvp=os_hvp)
+    self.ExecOpCode(op)
+
+    self.assertEqual(os_hvp, self.cluster.os_hvp)
+
+  def testOsparams(self):
+    osparams = {
+      "mocked_os": {
+        "param1": "value1",
+        "param2": None
+      },
+      "other_os": {
+        "param1": None
+      }
+    }
+    self.cluster.osparams = {"other_os": {"param1": "value1"}}
+    op = opcodes.OpClusterSetParams(osparams=osparams)
+    self.ExecOpCode(op)
+
+    self.assertEqual({"mocked_os": {"param1": "value1"}}, self.cluster.osparams)
+
+  def testEnabledHypervisors(self):
+    enabled_hypervisors = [constants.HT_XEN_HVM, constants.HT_XEN_PVM]
+    op = opcodes.OpClusterSetParams(enabled_hypervisors=enabled_hypervisors)
+    self.ExecOpCode(op)
+
+    self.assertEqual(enabled_hypervisors, self.cluster.enabled_hypervisors)
+
+  def testEnabledHypervisorsWithoutHypervisorParams(self):
+    enabled_hypervisors = [constants.HT_FAKE]
+    self.cluster.hvparams = {}
+    op = opcodes.OpClusterSetParams(enabled_hypervisors=enabled_hypervisors)
+    self.ExecOpCode(op)
+
+    self.assertEqual(enabled_hypervisors, self.cluster.enabled_hypervisors)
+    self.assertEqual(constants.HVC_DEFAULTS[constants.HT_FAKE],
+                     self.cluster.hvparams[constants.HT_FAKE])
+
+  @testutils.patch_object(utils, "FindFile")
+  def testValidDefaultIallocator(self, find_file_mock):
+    find_file_mock.return_value = "/random/path"
+    default_iallocator = "/random/path"
+    op = opcodes.OpClusterSetParams(default_iallocator=default_iallocator)
+    self.ExecOpCode(op)
+
+    self.assertEqual(default_iallocator, self.cluster.default_iallocator)
+
+  @testutils.patch_object(utils, "FindFile")
+  def testInvalidDefaultIallocator(self, find_file_mock):
+    find_file_mock.return_value = None
+    default_iallocator = "/random/path"
+    op = opcodes.OpClusterSetParams(default_iallocator=default_iallocator)
+    self.ExecOpCodeExpectOpPrereqError(op, "Invalid default iallocator script")
+
+  def testEnabledDiskTemplates(self):
+    enabled_disk_templates = [constants.DT_DISKLESS, constants.DT_PLAIN]
+    op = opcodes.OpClusterSetParams(
+           enabled_disk_templates=enabled_disk_templates,
+           ipolicy={constants.IPOLICY_DTS: enabled_disk_templates})
+    self.ExecOpCode(op)
+
+    self.assertEqual(enabled_disk_templates,
+                     self.cluster.enabled_disk_templates)
+
+  def testEnabledDiskTemplatesVsIpolicy(self):
+    enabled_disk_templates = [constants.DT_DISKLESS, constants.DT_PLAIN]
+    op = opcodes.OpClusterSetParams(
+           enabled_disk_templates=enabled_disk_templates,
+           ipolicy={constants.IPOLICY_DTS: [constants.DT_FILE]})
+    self.ExecOpCodeExpectOpPrereqError(op, "but not enabled on the cluster")
+
+  def testDisablingDiskTemplatesOfInstances(self):
+    old_disk_templates = [constants.DT_DISKLESS, constants.DT_PLAIN]
+    self.cfg.SetEnabledDiskTemplates(old_disk_templates)
+    self.cfg.AddNewInstance(
+      disks=[self.cfg.CreateDisk(dev_type=constants.DT_PLAIN)])
+    new_disk_templates = [constants.DT_DISKLESS, constants.DT_DRBD8]
+    op = opcodes.OpClusterSetParams(
+           enabled_disk_templates=new_disk_templates,
+           ipolicy={constants.IPOLICY_DTS: new_disk_templates})
+    self.ExecOpCodeExpectOpPrereqError(op, "least one instance using it")
+
+  def testEnabledDiskTemplatesWithoutVgName(self):
+    enabled_disk_templates = [constants.DT_PLAIN]
+    self.cluster.volume_group_name = None
+    op = opcodes.OpClusterSetParams(
+           enabled_disk_templates=enabled_disk_templates)
+    self.ExecOpCodeExpectOpPrereqError(op, "specify a volume group")
+
+  def testDisableDiskTemplateWithExistingInstance(self):
+    enabled_disk_templates = [constants.DT_DISKLESS]
+    self.cfg.AddNewInstance(
+      disks=[self.cfg.CreateDisk(dev_type=constants.DT_PLAIN)])
+    op = opcodes.OpClusterSetParams(
+           enabled_disk_templates=enabled_disk_templates,
+           ipolicy={constants.IPOLICY_DTS: enabled_disk_templates})
+    self.ExecOpCodeExpectOpPrereqError(op, "Cannot disable disk template")
+
+  def testVgNameNoLvmDiskTemplateEnabled(self):
+    vg_name = "test_vg"
+    self.cfg.SetEnabledDiskTemplates([constants.DT_DISKLESS])
+    op = opcodes.OpClusterSetParams(vg_name=vg_name)
+    self.ExecOpCode(op)
+
+    self.assertEqual(vg_name, self.cluster.volume_group_name)
+    self.mcpu.assertLogIsEmpty()
+
+  def testUnsetVgNameWithLvmDiskTemplateEnabled(self):
+    vg_name = ""
+    self.cluster.enabled_disk_templates = [constants.DT_PLAIN]
+    op = opcodes.OpClusterSetParams(vg_name=vg_name)
+    self.ExecOpCodeExpectOpPrereqError(op, "Cannot unset volume group")
+
+  def testUnsetVgNameWithLvmInstance(self):
+    vg_name = ""
+    self.cfg.AddNewInstance(
+      disks=[self.cfg.CreateDisk(dev_type=constants.DT_PLAIN)])
+    op = opcodes.OpClusterSetParams(vg_name=vg_name)
+    self.ExecOpCodeExpectOpPrereqError(op, "Cannot unset volume group")
+
+  def testUnsetVgNameWithNoLvmDiskTemplateEnabled(self):
+    vg_name = ""
+    self.cfg.SetEnabledDiskTemplates([constants.DT_DISKLESS])
+    op = opcodes.OpClusterSetParams(vg_name=vg_name)
+    self.ExecOpCode(op)
+
+    self.assertEqual(None, self.cluster.volume_group_name)
+
+  def testVgNameToOldName(self):
+    vg_name = self.cluster.volume_group_name
+    op = opcodes.OpClusterSetParams(vg_name=vg_name)
+    self.ExecOpCode(op)
+
+    self.mcpu.assertLogContainsRegex("already in desired state")
+
+  def testVgNameWithFailingNode(self):
+    vg_name = "test_vg"
+    op = opcodes.OpClusterSetParams(vg_name=vg_name)
+    self.rpc.call_vg_list.return_value = \
+      self.RpcResultsBuilder() \
+        .AddFailedNode(self.master) \
+        .Build()
+    self.ExecOpCode(op)
+
+    self.mcpu.assertLogContainsRegex("Error while gathering data on node")
+
+  def testVgNameWithValidNode(self):
+    vg_name = "test_vg"
+    op = opcodes.OpClusterSetParams(vg_name=vg_name)
+    self.rpc.call_vg_list.return_value = \
+      self.RpcResultsBuilder() \
+        .AddSuccessfulNode(self.master, {vg_name: 1024 * 1024}) \
+        .Build()
+    self.ExecOpCode(op)
+
+  def testVgNameWithTooSmallNode(self):
+    vg_name = "test_vg"
+    op = opcodes.OpClusterSetParams(vg_name=vg_name)
+    self.rpc.call_vg_list.return_value = \
+      self.RpcResultsBuilder() \
+        .AddSuccessfulNode(self.master, {vg_name: 1}) \
+        .Build()
+    self.ExecOpCodeExpectOpPrereqError(op, "too small")
+
+  def testMiscParameters(self):
+    op = opcodes.OpClusterSetParams(candidate_pool_size=123,
+                                    maintain_node_health=True,
+                                    modify_etc_hosts=True,
+                                    prealloc_wipe_disks=True,
+                                    reserved_lvs=["/dev/mock_lv"],
+                                    use_external_mip_script=True)
+    self.ExecOpCode(op)
+
+    self.mcpu.assertLogIsEmpty()
+    self.assertEqual(123, self.cluster.candidate_pool_size)
+    self.assertEqual(True, self.cluster.maintain_node_health)
+    self.assertEqual(True, self.cluster.modify_etc_hosts)
+    self.assertEqual(True, self.cluster.prealloc_wipe_disks)
+    self.assertEqual(["/dev/mock_lv"], self.cluster.reserved_lvs)
+    self.assertEqual(True, self.cluster.use_external_mip_script)
+
+  def testAddHiddenOs(self):
+    self.cluster.hidden_os = ["hidden1", "hidden2"]
+    op = opcodes.OpClusterSetParams(hidden_os=[(constants.DDM_ADD, "hidden2"),
+                                               (constants.DDM_ADD, "hidden3")])
+    self.ExecOpCode(op)
+
+    self.assertEqual(["hidden1", "hidden2", "hidden3"], self.cluster.hidden_os)
+    self.mcpu.assertLogContainsRegex("OS hidden2 already")
+
+  def testRemoveBlacklistedOs(self):
+    self.cluster.blacklisted_os = ["blisted1", "blisted2"]
+    op = opcodes.OpClusterSetParams(blacklisted_os=[
+                                      (constants.DDM_REMOVE, "blisted2"),
+                                      (constants.DDM_REMOVE, "blisted3")])
+    self.ExecOpCode(op)
+
+    self.assertEqual(["blisted1"], self.cluster.blacklisted_os)
+    self.mcpu.assertLogContainsRegex("OS blisted3 not found")
+
+  def testMasterNetdev(self):
+    master_netdev = "test_dev"
+    op = opcodes.OpClusterSetParams(master_netdev=master_netdev)
+    self.ExecOpCode(op)
+
+    self.assertEqual(master_netdev, self.cluster.master_netdev)
+
+  def testMasterNetdevFailNoForce(self):
+    master_netdev = "test_dev"
+    op = opcodes.OpClusterSetParams(master_netdev=master_netdev)
+    self.rpc.call_node_deactivate_master_ip.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateFailedNodeResult(self.master)
+    self.ExecOpCodeExpectOpExecError(op, "Could not disable the master ip")
+
+  def testMasterNetdevFailForce(self):
+    master_netdev = "test_dev"
+    op = opcodes.OpClusterSetParams(master_netdev=master_netdev,
+                                    force=True)
+    self.rpc.call_node_deactivate_master_ip.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateFailedNodeResult(self.master)
+    self.ExecOpCode(op)
+
+    self.mcpu.assertLogContainsRegex("Could not disable the master ip")
+
+
+class TestLUClusterVerify(CmdlibTestCase):
+  def testVerifyAllGroups(self):
+    op = opcodes.OpClusterVerify()
+    result = self.ExecOpCode(op)
+
+    self.assertEqual(2, len(result["jobs"]))
+
+  def testVerifyDefaultGroups(self):
+    op = opcodes.OpClusterVerify(group_name="default")
+    result = self.ExecOpCode(op)
+
+    self.assertEqual(1, len(result["jobs"]))
+
+
+class TestLUClusterVerifyConfig(CmdlibTestCase):
+
+  def setUp(self):
+    super(TestLUClusterVerifyConfig, self).setUp()
+
+    self._load_cert_patcher = testutils \
+      .patch_object(OpenSSL.crypto, "load_certificate")
+    self._load_cert_mock = self._load_cert_patcher.start()
+    self._verify_cert_patcher = testutils \
+      .patch_object(utils, "VerifyX509Certificate")
+    self._verify_cert_mock = self._verify_cert_patcher.start()
+    self._read_file_patcher = testutils.patch_object(utils, "ReadFile")
+    self._read_file_mock = self._read_file_patcher.start()
+    self._can_read_patcher = testutils.patch_object(utils, "CanRead")
+    self._can_read_mock = self._can_read_patcher.start()
+
+    self._can_read_mock.return_value = True
+    self._read_file_mock.return_value = True
+    self._verify_cert_mock.return_value = (None, "")
+    self._load_cert_mock.return_value = True
+
+  def tearDown(self):
+    super(TestLUClusterVerifyConfig, self).tearDown()
+
+    self._can_read_patcher.stop()
+    self._read_file_patcher.stop()
+    self._verify_cert_patcher.stop()
+    self._load_cert_patcher.stop()
+
+  def testSuccessfulRun(self):
+    self.cfg.AddNewInstance()
+    op = opcodes.OpClusterVerifyConfig()
+    result = self.ExecOpCode(op)
+
+    self.assertTrue(result)
+
+  def testDanglingNode(self):
+    node = self.cfg.AddNewNode()
+    self.cfg.AddNewInstance(primary_node=node)
+    node.group = "invalid"
+    op = opcodes.OpClusterVerifyConfig()
+    result = self.ExecOpCode(op)
+
+    self.mcpu.assertLogContainsRegex(
+      "following nodes \(and their instances\) belong to a non existing group")
+    self.assertFalse(result)
+
+  def testDanglingInstance(self):
+    inst = self.cfg.AddNewInstance()
+    inst.primary_node = "invalid"
+    op = opcodes.OpClusterVerifyConfig()
+    result = self.ExecOpCode(op)
+
+    self.mcpu.assertLogContainsRegex(
+      "following instances have a non-existing primary-node")
+    self.assertFalse(result)
+
+
+class TestLUClusterVerifyGroup(CmdlibTestCase):
+  def testEmptyNodeGroup(self):
+    group = self.cfg.AddNewNodeGroup()
+    op = opcodes.OpClusterVerifyGroup(group_name=group.name, verbose=True)
+
+    result = self.ExecOpCode(op)
+
+    self.assertTrue(result)
+    self.mcpu.assertLogContainsRegex("Empty node group, skipping verification")
+
+  def testSimpleInvocation(self):
+    op = opcodes.OpClusterVerifyGroup(group_name="default", verbose=True)
+
+    self.ExecOpCode(op)
+
+  def testSimpleInvocationWithInstance(self):
+    self.cfg.AddNewInstance(disks=[])
+    op = opcodes.OpClusterVerifyGroup(group_name="default", verbose=True)
+
+    self.ExecOpCode(op)
+
+  def testGhostNode(self):
+    group = self.cfg.AddNewNodeGroup()
+    node = self.cfg.AddNewNode(group=group.uuid, offline=True)
+    self.master.offline = True
+    self.cfg.AddNewInstance(disk_template=constants.DT_DRBD8,
+                            primary_node=self.master,
+                            secondary_node=node)
+
+    self.rpc.call_blockdev_getmirrorstatus_multi.return_value = \
+      RpcResultsBuilder() \
+        .AddOfflineNode(self.master) \
+        .Build()
+
+    op = opcodes.OpClusterVerifyGroup(group_name="default", verbose=True)
+
+    self.ExecOpCode(op)
+
+  def testValidRpcResult(self):
+    self.cfg.AddNewInstance(disks=[])
+
+    self.rpc.call_node_verify.return_value = \
+      RpcResultsBuilder() \
+        .AddSuccessfulNode(self.master, {}) \
+        .Build()
+
+    op = opcodes.OpClusterVerifyGroup(group_name="default", verbose=True)
+
+    self.ExecOpCode(op)
+
+
+class TestLUClusterVerifyGroupMethods(CmdlibTestCase):
+  """Base class for testing individual methods in LUClusterVerifyGroup.
+
+  """
+  def setUp(self):
+    super(TestLUClusterVerifyGroupMethods, self).setUp()
+    self.op = opcodes.OpClusterVerifyGroup(group_name="default")
+
+  def PrepareLU(self, lu):
+    lu._exclusive_storage = False
+    lu.master_node = self.master_uuid
+    lu.group_info = self.group
+    cluster.LUClusterVerifyGroup.all_node_info = \
+      property(fget=lambda _: self.cfg.GetAllNodesInfo())
+
+
+class TestLUClusterVerifyGroupVerifyNode(TestLUClusterVerifyGroupMethods):
+  @withLockedLU
+  def testInvalidNodeResult(self, lu):
+    self.assertFalse(lu._VerifyNode(self.master, None))
+    self.assertFalse(lu._VerifyNode(self.master, ""))
+
+  @withLockedLU
+  def testInvalidVersion(self, lu):
+    self.assertFalse(lu._VerifyNode(self.master, {"version": None}))
+    self.assertFalse(lu._VerifyNode(self.master, {"version": ""}))
+    self.assertFalse(lu._VerifyNode(self.master, {
+      "version": (constants.PROTOCOL_VERSION - 1, constants.RELEASE_VERSION)
+    }))
+
+    self.mcpu.ClearLogMessages()
+    self.assertTrue(lu._VerifyNode(self.master, {
+      "version": (constants.PROTOCOL_VERSION, constants.RELEASE_VERSION + "x")
+    }))
+    self.mcpu.assertLogContainsRegex("software version mismatch")
+
+  def _GetValidNodeResult(self, additional_fields):
+    ret = {
+      "version": (constants.PROTOCOL_VERSION, constants.RELEASE_VERSION),
+      constants.NV_NODESETUP: []
+    }
+    ret.update(additional_fields)
+    return ret
+
+  @withLockedLU
+  def testHypervisor(self, lu):
+    lu._VerifyNode(self.master, self._GetValidNodeResult({
+      constants.NV_HYPERVISOR: {
+        constants.HT_XEN_PVM: None,
+        constants.HT_XEN_HVM: "mock error"
+      }
+    }))
+    self.mcpu.assertLogContainsRegex(constants.HT_XEN_HVM)
+    self.mcpu.assertLogContainsRegex("mock error")
+
+  @withLockedLU
+  def testHvParams(self, lu):
+    lu._VerifyNode(self.master, self._GetValidNodeResult({
+      constants.NV_HVPARAMS: [("mock item", constants.HT_XEN_HVM, "mock error")]
+    }))
+    self.mcpu.assertLogContainsRegex(constants.HT_XEN_HVM)
+    self.mcpu.assertLogContainsRegex("mock item")
+    self.mcpu.assertLogContainsRegex("mock error")
+
+  @withLockedLU
+  def testSuccessfulResult(self, lu):
+    self.assertTrue(lu._VerifyNode(self.master, self._GetValidNodeResult({})))
+    self.mcpu.assertLogIsEmpty()
+
+
+class TestLUClusterVerifyGroupVerifyNodeTime(TestLUClusterVerifyGroupMethods):
+  @withLockedLU
+  def testInvalidNodeResult(self, lu):
+    for ndata in [{}, {constants.NV_TIME: "invalid"}]:
+      self.mcpu.ClearLogMessages()
+      lu._VerifyNodeTime(self.master, ndata, None, None)
+
+      self.mcpu.assertLogContainsRegex("Node returned invalid time")
+
+  @withLockedLU
+  def testNodeDiverges(self, lu):
+    for ntime in [(0, 0), (2000, 0)]:
+      self.mcpu.ClearLogMessages()
+      lu._VerifyNodeTime(self.master, {constants.NV_TIME: ntime}, 1000, 1005)
+
+      self.mcpu.assertLogContainsRegex("Node time diverges")
+
+  @withLockedLU
+  def testSuccessfulResult(self, lu):
+    lu._VerifyNodeTime(self.master, {constants.NV_TIME: (0, 0)}, 0, 5)
+    self.mcpu.assertLogIsEmpty()
+
+
+class TestLUClusterVerifyGroupUpdateVerifyNodeLVM(
+        TestLUClusterVerifyGroupMethods):
+  def setUp(self):
+    super(TestLUClusterVerifyGroupUpdateVerifyNodeLVM, self).setUp()
+    self.VALID_NRESULT = {
+      constants.NV_VGLIST: {"mock_vg": 30000},
+      constants.NV_PVLIST: [
+        {
+          "name": "mock_pv",
+          "vg_name": "mock_vg",
+          "size": 5000,
+          "free": 2500,
+          "attributes": [],
+          "lv_list": []
+        }
+      ]
+    }
+
+  @withLockedLU
+  def testNoVgName(self, lu):
+    lu._UpdateVerifyNodeLVM(self.master, {}, None, None)
+    self.mcpu.assertLogIsEmpty()
+
+  @withLockedLU
+  def testEmptyNodeResult(self, lu):
+    lu._UpdateVerifyNodeLVM(self.master, {}, "mock_vg", None)
+    self.mcpu.assertLogContainsRegex("unable to check volume groups")
+    self.mcpu.assertLogContainsRegex("Can't get PV list from node")
+
+  @withLockedLU
+  def testValidNodeResult(self, lu):
+    lu._UpdateVerifyNodeLVM(self.master, self.VALID_NRESULT, "mock_vg", None)
+    self.mcpu.assertLogIsEmpty()
+
+  @withLockedLU
+  def testValidNodeResultExclusiveStorage(self, lu):
+    lu._exclusive_storage = True
+    lu._UpdateVerifyNodeLVM(self.master, self.VALID_NRESULT, "mock_vg",
+                            cluster.LUClusterVerifyGroup.NodeImage())
+    self.mcpu.assertLogIsEmpty()
+
+
+class TestLUClusterVerifyGroupVerifyGroupDRBDVersion(
+        TestLUClusterVerifyGroupMethods):
+  @withLockedLU
+  def testEmptyNodeResult(self, lu):
+    lu._VerifyGroupDRBDVersion({})
+    self.mcpu.assertLogIsEmpty()
+
+  @withLockedLU
+  def testValidNodeResult(self, lu):
+    lu._VerifyGroupDRBDVersion(
+      RpcResultsBuilder()
+        .AddSuccessfulNode(self.master, {
+          constants.NV_DRBDVERSION: "8.3.0"
+        })
+        .Build())
+    self.mcpu.assertLogIsEmpty()
+
+  @withLockedLU
+  def testDifferentVersions(self, lu):
+    node1 = self.cfg.AddNewNode()
+    lu._VerifyGroupDRBDVersion(
+      RpcResultsBuilder()
+        .AddSuccessfulNode(self.master, {
+          constants.NV_DRBDVERSION: "8.3.0"
+        })
+        .AddSuccessfulNode(node1, {
+          constants.NV_DRBDVERSION: "8.4.0"
+        })
+        .Build())
+    self.mcpu.assertLogContainsRegex("DRBD version mismatch: 8.3.0")
+    self.mcpu.assertLogContainsRegex("DRBD version mismatch: 8.4.0")
+
+
+class TestLUClusterVerifyGroupVerifyGroupLVM(TestLUClusterVerifyGroupMethods):
+  @withLockedLU
+  def testNoVgName(self, lu):
+    lu._VerifyGroupLVM(None, None)
+    self.mcpu.assertLogIsEmpty()
+
+  @withLockedLU
+  def testNoExclusiveStorage(self, lu):
+    lu._VerifyGroupLVM(None, "mock_vg")
+    self.mcpu.assertLogIsEmpty()
+
+  @withLockedLU
+  def testNoPvInfo(self, lu):
+    lu._exclusive_storage = True
+    nimg = cluster.LUClusterVerifyGroup.NodeImage()
+    lu._VerifyGroupLVM({self.master.uuid: nimg}, "mock_vg")
+    self.mcpu.assertLogIsEmpty()
+
+  @withLockedLU
+  def testValidPvInfos(self, lu):
+    lu._exclusive_storage = True
+    node2 = self.cfg.AddNewNode()
+    nimg1 = cluster.LUClusterVerifyGroup.NodeImage(uuid=self.master.uuid)
+    nimg1.pv_min = 10000
+    nimg1.pv_max = 10010
+    nimg2 = cluster.LUClusterVerifyGroup.NodeImage(uuid=node2.uuid)
+    nimg2.pv_min = 9998
+    nimg2.pv_max = 10005
+    lu._VerifyGroupLVM({self.master.uuid: nimg1, node2.uuid: nimg2}, "mock_vg")
+    self.mcpu.assertLogIsEmpty()
+
+
+class TestLUClusterVerifyGroupVerifyNodeBridges(
+        TestLUClusterVerifyGroupMethods):
+  @withLockedLU
+  def testNoBridges(self, lu):
+    lu._VerifyNodeBridges(None, None, None)
+    self.mcpu.assertLogIsEmpty()
+
+  @withLockedLU
+  def testInvalidBridges(self, lu):
+    for ndata in [{}, {constants.NV_BRIDGES: ""}]:
+      self.mcpu.ClearLogMessages()
+      lu._VerifyNodeBridges(self.master, ndata, ["mock_bridge"])
+      self.mcpu.assertLogContainsRegex("not return valid bridge information")
+
+    self.mcpu.ClearLogMessages()
+    lu._VerifyNodeBridges(self.master, {constants.NV_BRIDGES: ["mock_bridge"]},
+                          ["mock_bridge"])
+    self.mcpu.assertLogContainsRegex("missing bridge")
+
+
+class TestLUClusterVerifyGroupVerifyNodeUserScripts(
+        TestLUClusterVerifyGroupMethods):
+  @withLockedLU
+  def testNoUserScripts(self, lu):
+    lu._VerifyNodeUserScripts(self.master, {})
+    self.mcpu.assertLogContainsRegex("did not return user scripts information")
+
+  @withLockedLU
+  def testBrokenUserScripts(self, lu):
+    lu._VerifyNodeUserScripts(self.master,
+                              {constants.NV_USERSCRIPTS: ["script"]})
+    self.mcpu.assertLogContainsRegex("scripts not present or not executable")
+
+
+class TestLUClusterVerifyGroupVerifyNodeNetwork(
+        TestLUClusterVerifyGroupMethods):
+
+  def setUp(self):
+    super(TestLUClusterVerifyGroupVerifyNodeNetwork, self).setUp()
+    self.VALID_NRESULT = {
+      constants.NV_NODELIST: {},
+      constants.NV_NODENETTEST: {},
+      constants.NV_MASTERIP: True
+    }
+
+  @withLockedLU
+  def testEmptyNodeResult(self, lu):
+    lu._VerifyNodeNetwork(self.master, {})
+    self.mcpu.assertLogContainsRegex(
+      "node hasn't returned node ssh connectivity data")
+    self.mcpu.assertLogContainsRegex(
+      "node hasn't returned node tcp connectivity data")
+    self.mcpu.assertLogContainsRegex(
+      "node hasn't returned node master IP reachability data")
+
+  @withLockedLU
+  def testValidResult(self, lu):
+    lu._VerifyNodeNetwork(self.master, self.VALID_NRESULT)
+    self.mcpu.assertLogIsEmpty()
+
+  @withLockedLU
+  def testSshProblem(self, lu):
+    self.VALID_NRESULT.update({
+      constants.NV_NODELIST: {
+        "mock_node": "mock_error"
+      }
+    })
+    lu._VerifyNodeNetwork(self.master, self.VALID_NRESULT)
+    self.mcpu.assertLogContainsRegex("ssh communication with node 'mock_node'")
+
+  @withLockedLU
+  def testTcpProblem(self, lu):
+    self.VALID_NRESULT.update({
+      constants.NV_NODENETTEST: {
+        "mock_node": "mock_error"
+      }
+    })
+    lu._VerifyNodeNetwork(self.master, self.VALID_NRESULT)
+    self.mcpu.assertLogContainsRegex("tcp communication with node 'mock_node'")
+
+  @withLockedLU
+  def testMasterIpNotReachable(self, lu):
+    self.VALID_NRESULT.update({
+      constants.NV_MASTERIP: False
+    })
+    node1 = self.cfg.AddNewNode()
+    lu._VerifyNodeNetwork(self.master, self.VALID_NRESULT)
+    self.mcpu.assertLogContainsRegex(
+      "the master node cannot reach the master IP")
+
+    self.mcpu.ClearLogMessages()
+    lu._VerifyNodeNetwork(node1, self.VALID_NRESULT)
+    self.mcpu.assertLogContainsRegex("cannot reach the master IP")
+
+
+class TestLUClusterVerifyGroupVerifyInstance(TestLUClusterVerifyGroupMethods):
+  def setUp(self):
+    super(TestLUClusterVerifyGroupVerifyInstance, self).setUp()
+
+    self.node1 = self.cfg.AddNewNode()
+    self.drbd_inst = self.cfg.AddNewInstance(
+      disks=[self.cfg.CreateDisk(dev_type=constants.DT_DRBD8,
+                                 primary_node=self.master,
+                                 secondary_node=self.node1)])
+    self.running_inst = self.cfg.AddNewInstance(
+      admin_state=constants.ADMINST_UP, disks_active=True)
+    self.diskless_inst = self.cfg.AddNewInstance(disks=[])
+
+    self.master_img = \
+      cluster.LUClusterVerifyGroup.NodeImage(uuid=self.master_uuid)
+    self.master_img.volumes = ["/".join(disk.logical_id)
+                               for inst in [self.running_inst,
+                                            self.diskless_inst]
+                               for disk in inst.disks]
+    self.master_img.volumes.extend(
+      ["/".join(disk.logical_id) for disk in self.drbd_inst.disks[0].children])
+    self.master_img.instances = [self.running_inst.uuid]
+    self.node1_img = \
+      cluster.LUClusterVerifyGroup.NodeImage(uuid=self.node1.uuid)
+    self.node1_img.volumes = \
+      ["/".join(disk.logical_id) for disk in self.drbd_inst.disks[0].children]
+    self.node_imgs = {
+      self.master_uuid: self.master_img,
+      self.node1.uuid: self.node1_img
+    }
+    self.diskstatus = {
+      self.master_uuid: [
+        (True, objects.BlockDevStatus(ldisk_status=constants.LDS_OKAY))
+        for _ in self.running_inst.disks
+      ]
+    }
+
+  @withLockedLU
+  def testDisklessInst(self, lu):
+    lu._VerifyInstance(self.diskless_inst, self.node_imgs, {})
+    self.mcpu.assertLogIsEmpty()
+
+  @withLockedLU
+  def testOfflineNode(self, lu):
+    self.master_img.offline = True
+    lu._VerifyInstance(self.drbd_inst, self.node_imgs, {})
+    self.mcpu.assertLogIsEmpty()
+
+  @withLockedLU
+  def testRunningOnOfflineNode(self, lu):
+    self.master_img.offline = True
+    lu._VerifyInstance(self.running_inst, self.node_imgs, {})
+    self.mcpu.assertLogContainsRegex(
+      "instance is marked as running and lives on offline node")
+
+  @withLockedLU
+  def testMissingVolume(self, lu):
+    self.master_img.volumes = []
+    lu._VerifyInstance(self.running_inst, self.node_imgs, {})
+    self.mcpu.assertLogContainsRegex("volume .* missing")
+
+  @withLockedLU
+  def testRunningInstanceOnWrongNode(self, lu):
+    self.master_img.instances = []
+    self.diskless_inst.admin_state = constants.ADMINST_UP
+    lu._VerifyInstance(self.running_inst, self.node_imgs, {})
+    self.mcpu.assertLogContainsRegex("instance not running on its primary node")
+
+  @withLockedLU
+  def testRunningInstanceOnRightNode(self, lu):
+    self.master_img.instances = [self.running_inst.uuid]
+    lu._VerifyInstance(self.running_inst, self.node_imgs, {})
+    self.mcpu.assertLogIsEmpty()
+
+  @withLockedLU
+  def testValidDiskStatus(self, lu):
+    lu._VerifyInstance(self.running_inst, self.node_imgs, self.diskstatus)
+    self.mcpu.assertLogIsEmpty()
+
+  @withLockedLU
+  def testDegradedDiskStatus(self, lu):
+    self.diskstatus[self.master_uuid][0][1].is_degraded = True
+    lu._VerifyInstance(self.running_inst, self.node_imgs, self.diskstatus)
+    self.mcpu.assertLogContainsRegex("instance .* is degraded")
+
+  @withLockedLU
+  def testNotOkayDiskStatus(self, lu):
+    self.diskstatus[self.master_uuid][0][1].ldisk_status = constants.LDS_FAULTY
+    lu._VerifyInstance(self.running_inst, self.node_imgs, self.diskstatus)
+    self.mcpu.assertLogContainsRegex("instance .* state is 'faulty'")
+
+  @withLockedLU
+  def testExclusiveStorageWithInvalidInstance(self, lu):
+    self.master.ndparams[constants.ND_EXCLUSIVE_STORAGE] = True
+    lu._VerifyInstance(self.drbd_inst, self.node_imgs, self.diskstatus)
+    self.mcpu.assertLogContainsRegex(
+      "instance has template drbd, which is not supported")
+
+  @withLockedLU
+  def testExclusiveStorageWithValidInstance(self, lu):
+    self.master.ndparams[constants.ND_EXCLUSIVE_STORAGE] = True
+    self.running_inst.disks[0].spindles = 1
+    lu._VerifyInstance(self.running_inst, self.node_imgs, self.diskstatus)
+    self.mcpu.assertLogIsEmpty()
+
+  @withLockedLU
+  def testDrbdInTwoGroups(self, lu):
+    group = self.cfg.AddNewNodeGroup()
+    self.node1.group = group.uuid
+    lu._VerifyInstance(self.drbd_inst, self.node_imgs, self.diskstatus)
+    self.mcpu.assertLogContainsRegex(
+      "instance has primary and secondary nodes in different groups")
+
+  @withLockedLU
+  def testOfflineSecondary(self, lu):
+    self.node1_img.offline = True
+    lu._VerifyInstance(self.drbd_inst, self.node_imgs, self.diskstatus)
+    self.mcpu.assertLogContainsRegex("instance has offline secondary node\(s\)")
+
+
+class TestLUClusterVerifyGroupVerifyOrphanVolumes(
+        TestLUClusterVerifyGroupMethods):
+  @withLockedLU
+  def testOrphanedVolume(self, lu):
+    master_img = cluster.LUClusterVerifyGroup.NodeImage(uuid=self.master_uuid)
+    master_img.volumes = ["mock_vg/disk_0", "mock_vg/disk_1", "mock_vg/disk_2"]
+    node_imgs = {
+      self.master_uuid: master_img
+    }
+    node_vol_should = {
+      self.master_uuid: ["mock_vg/disk_0"]
+    }
+
+    lu._VerifyOrphanVolumes(node_vol_should, node_imgs,
+                            utils.FieldSet("mock_vg/disk_2"))
+    self.mcpu.assertLogContainsRegex("volume mock_vg/disk_1 is unknown")
+    self.mcpu.assertLogDoesNotContainRegex("volume mock_vg/disk_0 is unknown")
+    self.mcpu.assertLogDoesNotContainRegex("volume mock_vg/disk_2 is unknown")
+
+
+class TestLUClusterVerifyGroupVerifyNPlusOneMemory(
+        TestLUClusterVerifyGroupMethods):
+  @withLockedLU
+  def testN1Failure(self, lu):
+    group1 = self.cfg.AddNewNodeGroup()
+
+    node1 = self.cfg.AddNewNode()
+    node2 = self.cfg.AddNewNode(group=group1)
+    node3 = self.cfg.AddNewNode()
+
+    inst1 = self.cfg.AddNewInstance()
+    inst2 = self.cfg.AddNewInstance()
+    inst3 = self.cfg.AddNewInstance()
+
+    node1_img = cluster.LUClusterVerifyGroup.NodeImage(uuid=node1.uuid)
+    node1_img.sbp = {
+      self.master_uuid: [inst1.uuid, inst2.uuid, inst3.uuid]
+    }
+
+    node2_img = cluster.LUClusterVerifyGroup.NodeImage(uuid=node2.uuid)
+
+    node3_img = cluster.LUClusterVerifyGroup.NodeImage(uuid=node3.uuid)
+    node3_img.offline = True
+
+    node_imgs = {
+      node1.uuid: node1_img,
+      node2.uuid: node2_img,
+      node3.uuid: node3_img
+    }
+
+    lu._VerifyNPlusOneMemory(node_imgs, self.cfg.GetAllInstancesInfo())
+    self.mcpu.assertLogContainsRegex(
+      "not enough memory to accomodate instance failovers")
+
+    self.mcpu.ClearLogMessages()
+    node1_img.mfree = 1000
+    lu._VerifyNPlusOneMemory(node_imgs, self.cfg.GetAllInstancesInfo())
+    self.mcpu.assertLogIsEmpty()
+
+
+class TestLUClusterVerifyGroupVerifyFiles(TestLUClusterVerifyGroupMethods):
+  @withLockedLU
+  def test(self, lu):
+    node1 = self.cfg.AddNewNode(master_candidate=False, offline=False,
+                                vm_capable=True)
+    node2 = self.cfg.AddNewNode(master_candidate=True, vm_capable=False)
+    node3 = self.cfg.AddNewNode(master_candidate=False, offline=False,
+                                vm_capable=True)
+    node4 = self.cfg.AddNewNode(master_candidate=False, offline=False,
+                                vm_capable=True)
+    node5 = self.cfg.AddNewNode(master_candidate=False, offline=True)
+
+    nodeinfo = [self.master, node1, node2, node3, node4, node5]
+    files_all = set([
+      pathutils.CLUSTER_DOMAIN_SECRET_FILE,
+      pathutils.RAPI_CERT_FILE,
+      pathutils.RAPI_USERS_FILE,
+      ])
+    files_opt = set([
+      pathutils.RAPI_USERS_FILE,
+      hv_xen.XL_CONFIG_FILE,
+      pathutils.VNC_PASSWORD_FILE,
+      ])
+    files_mc = set([
+      pathutils.CLUSTER_CONF_FILE,
+      ])
+    files_vm = set([
+      hv_xen.XEND_CONFIG_FILE,
+      hv_xen.XL_CONFIG_FILE,
+      pathutils.VNC_PASSWORD_FILE,
+      ])
+    nvinfo = RpcResultsBuilder() \
+      .AddSuccessfulNode(self.master, {
+        constants.NV_FILELIST: {
+          pathutils.CLUSTER_CONF_FILE: "82314f897f38b35f9dab2f7c6b1593e0",
+          pathutils.RAPI_CERT_FILE: "babbce8f387bc082228e544a2146fee4",
+          pathutils.CLUSTER_DOMAIN_SECRET_FILE: "cds-47b5b3f19202936bb4",
+          hv_xen.XEND_CONFIG_FILE: "b4a8a824ab3cac3d88839a9adeadf310",
+          hv_xen.XL_CONFIG_FILE: "77935cee92afd26d162f9e525e3d49b9"
+        }}) \
+      .AddSuccessfulNode(node1, {
+        constants.NV_FILELIST: {
+          pathutils.RAPI_CERT_FILE: "97f0356500e866387f4b84233848cc4a",
+          hv_xen.XEND_CONFIG_FILE: "b4a8a824ab3cac3d88839a9adeadf310",
+          }
+        }) \
+      .AddSuccessfulNode(node2, {
+        constants.NV_FILELIST: {
+          pathutils.RAPI_CERT_FILE: "97f0356500e866387f4b84233848cc4a",
+          pathutils.CLUSTER_DOMAIN_SECRET_FILE: "cds-47b5b3f19202936bb4",
+          }
+        }) \
+      .AddSuccessfulNode(node3, {
+        constants.NV_FILELIST: {
+          pathutils.RAPI_CERT_FILE: "97f0356500e866387f4b84233848cc4a",
+          pathutils.CLUSTER_CONF_FILE: "conf-a6d4b13e407867f7a7b4f0f232a8f527",
+          pathutils.CLUSTER_DOMAIN_SECRET_FILE: "cds-47b5b3f19202936bb4",
+          pathutils.RAPI_USERS_FILE: "rapiusers-ea3271e8d810ef3",
+          hv_xen.XL_CONFIG_FILE: "77935cee92afd26d162f9e525e3d49b9"
+          }
+        }) \
+      .AddSuccessfulNode(node4, {}) \
+      .AddOfflineNode(node5) \
+      .Build()
+    assert set(nvinfo.keys()) == set(map(operator.attrgetter("uuid"), nodeinfo))
+
+    lu._VerifyFiles(nodeinfo, self.master_uuid, nvinfo,
+                    (files_all, files_opt, files_mc, files_vm))
+
+    expected_msgs = [
+      "File %s found with 2 different checksums (variant 1 on"
+        " %s, %s, %s; variant 2 on %s)" %
+        (pathutils.RAPI_CERT_FILE, node1.name, node2.name, node3.name,
+         self.master.name),
+      "File %s is missing from node(s) %s" %
+        (pathutils.CLUSTER_DOMAIN_SECRET_FILE, node1.name),
+      "File %s should not exist on node(s) %s" %
+        (pathutils.CLUSTER_CONF_FILE, node3.name),
+      "File %s is missing from node(s) %s" %
+        (hv_xen.XEND_CONFIG_FILE, node3.name),
+      "File %s is missing from node(s) %s" %
+        (pathutils.CLUSTER_CONF_FILE, node2.name),
+      "File %s found with 2 different checksums (variant 1 on"
+        " %s; variant 2 on %s)" %
+        (pathutils.CLUSTER_CONF_FILE, self.master.name, node3.name),
+      "File %s is optional, but it must exist on all or no nodes (not"
+        " found on %s, %s, %s)" %
+        (pathutils.RAPI_USERS_FILE, self.master.name, node1.name, node2.name),
+      "File %s is optional, but it must exist on all or no nodes (not"
+        " found on %s)" % (hv_xen.XL_CONFIG_FILE, node1.name),
+      "Node did not return file checksum data",
+      ]
+
+    self.assertEqual(len(self.mcpu.GetLogMessages()), len(expected_msgs))
+    for expected_msg in expected_msgs:
+      self.mcpu.assertLogContainsInLine(expected_msg)
+
+
+class TestLUClusterVerifyGroupVerifyNodeDrbd(TestLUClusterVerifyGroupMethods):
+  def setUp(self):
+    super(TestLUClusterVerifyGroupVerifyNodeDrbd, self).setUp()
+
+    self.node1 = self.cfg.AddNewNode()
+    self.node2 = self.cfg.AddNewNode()
+    self.inst = self.cfg.AddNewInstance(
+      disks=[self.cfg.CreateDisk(dev_type=constants.DT_DRBD8,
+                                 primary_node=self.node1,
+                                 secondary_node=self.node2)],
+      admin_state=constants.ADMINST_UP)
+
+  @withLockedLU
+  def testNoDrbdHelper(self, lu):
+    lu._VerifyNodeDrbd(self.master, {}, self.cfg.GetAllInstancesInfo(), None,
+                       self.cfg.ComputeDRBDMap())
+    self.mcpu.assertLogIsEmpty()
+
+  @withLockedLU
+  def testDrbdHelperInvalidNodeResult(self, lu):
+    for ndata, expected in [({}, "no drbd usermode helper returned"),
+                            ({constants.NV_DRBDHELPER: (False, "")},
+                             "drbd usermode helper check unsuccessful"),
+                            ({constants.NV_DRBDHELPER: (True, "/bin/false")},
+                             "wrong drbd usermode helper")]:
+      self.mcpu.ClearLogMessages()
+      lu._VerifyNodeDrbd(self.master, ndata, self.cfg.GetAllInstancesInfo(),
+                         "/bin/true", self.cfg.ComputeDRBDMap())
+      self.mcpu.assertLogContainsRegex(expected)
+
+  @withLockedLU
+  def testNoNodeResult(self, lu):
+    lu._VerifyNodeDrbd(self.node1, {}, self.cfg.GetAllInstancesInfo(),
+                         None, self.cfg.ComputeDRBDMap())
+    self.mcpu.assertLogContainsRegex("drbd minor 1 of .* is not active")
+
+  @withLockedLU
+  def testInvalidNodeResult(self, lu):
+    lu._VerifyNodeDrbd(self.node1, {constants.NV_DRBDLIST: ""},
+                       self.cfg.GetAllInstancesInfo(), None,
+                       self.cfg.ComputeDRBDMap())
+    self.mcpu.assertLogContainsRegex("cannot parse drbd status file")
+
+  @withLockedLU
+  def testWrongMinorInUse(self, lu):
+    lu._VerifyNodeDrbd(self.node1, {constants.NV_DRBDLIST: [2]},
+                       self.cfg.GetAllInstancesInfo(), None,
+                       self.cfg.ComputeDRBDMap())
+    self.mcpu.assertLogContainsRegex("drbd minor 1 of .* is not active")
+    self.mcpu.assertLogContainsRegex("unallocated drbd minor 2 is in use")
+
+  @withLockedLU
+  def testValidResult(self, lu):
+    lu._VerifyNodeDrbd(self.node1, {constants.NV_DRBDLIST: [1]},
+                       self.cfg.GetAllInstancesInfo(), None,
+                       self.cfg.ComputeDRBDMap())
+    self.mcpu.assertLogIsEmpty()
+
+
+class TestLUClusterVerifyGroupVerifyNodeOs(TestLUClusterVerifyGroupMethods):
+  @withLockedLU
+  def testUpdateNodeOsInvalidNodeResult(self, lu):
+    for ndata in [{}, {constants.NV_OSLIST: ""}, {constants.NV_OSLIST: [""]},
+                  {constants.NV_OSLIST: [["1", "2"]]}]:
+      self.mcpu.ClearLogMessages()
+      nimage = cluster.LUClusterVerifyGroup.NodeImage(uuid=self.master_uuid)
+      lu._UpdateNodeOS(self.master, ndata, nimage)
+      self.mcpu.assertLogContainsRegex("node hasn't returned valid OS data")
+
+  @withLockedLU
+  def testUpdateNodeOsValidNodeResult(self, lu):
+    ndata = {
+      constants.NV_OSLIST: [
+        ["mock_OS", "/mocked/path", True, "", ["default"], [],
+         [constants.OS_API_V20]],
+        ["Another_Mock", "/random", True, "", ["var1", "var2"],
+         [{"param1": "val1"}, {"param2": "val2"}], constants.OS_API_VERSIONS]
+      ]
+    }
+    nimage = cluster.LUClusterVerifyGroup.NodeImage(uuid=self.master_uuid)
+    lu._UpdateNodeOS(self.master, ndata, nimage)
+    self.mcpu.assertLogIsEmpty()
+
+  @withLockedLU
+  def testVerifyNodeOs(self, lu):
+    node = self.cfg.AddNewNode()
+    nimg_root = cluster.LUClusterVerifyGroup.NodeImage(uuid=self.master_uuid)
+    nimg = cluster.LUClusterVerifyGroup.NodeImage(uuid=node.uuid)
+
+    nimg_root.os_fail = False
+    nimg_root.oslist = {
+      "mock_os": [("/mocked/path", True, "", set(["default"]), set(),
+                   set([constants.OS_API_V20]))],
+      "broken_base_os": [("/broken", False, "", set(), set(),
+                         set([constants.OS_API_V20]))],
+      "only_on_root": [("/random", True, "", set(), set(), set())],
+      "diffing_os": [("/pinky", True, "", set(["var1", "var2"]),
+                      set([("param1", "val1"), ("param2", "val2")]),
+                      set([constants.OS_API_V20]))]
+    }
+    nimg.os_fail = False
+    nimg.oslist = {
+      "mock_os": [("/mocked/path", True, "", set(["default"]), set(),
+                   set([constants.OS_API_V20]))],
+      "only_on_test": [("/random", True, "", set(), set(), set())],
+      "diffing_os": [("/bunny", True, "", set(["var1", "var3"]),
+                      set([("param1", "val1"), ("param3", "val3")]),
+                      set([constants.OS_API_V15]))],
+      "broken_os": [("/broken", False, "", set(), set(),
+                     set([constants.OS_API_V20]))],
+      "multi_entries": [
+        ("/multi1", True, "", set(), set(), set([constants.OS_API_V20])),
+        ("/multi2", True, "", set(), set(), set([constants.OS_API_V20]))]
+    }
+
+    lu._VerifyNodeOS(node, nimg, nimg_root)
+
+    expected_msgs = [
+      "Extra OS only_on_test not present on reference node",
+      "OSes present on reference node .* but missing on this node:" +
+        " only_on_root",
+      "OS API version for diffing_os differs",
+      "OS variants list for diffing_os differs",
+      "OS parameters for diffing_os differs",
+      "Invalid OS broken_os",
+      "Extra OS broken_os not present on reference node",
+      "OS 'multi_entries' has multiple entries",
+      "Extra OS multi_entries not present on reference node"
+    ]
+
+    self.assertEqual(len(expected_msgs), len(self.mcpu.GetLogMessages()))
+    for expected_msg in expected_msgs:
+      self.mcpu.assertLogContainsRegex(expected_msg)
+
+
+class TestLUClusterVerifyGroupVerifyAcceptedFileStoragePaths(
+  TestLUClusterVerifyGroupMethods):
+  @withLockedLU
+  def testNotMaster(self, lu):
+    lu._VerifyAcceptedFileStoragePaths(self.master, {}, False)
+    self.mcpu.assertLogIsEmpty()
+
+  @withLockedLU
+  def testNotMasterButRetunedValue(self, lu):
+    lu._VerifyAcceptedFileStoragePaths(
+      self.master, {constants.NV_ACCEPTED_STORAGE_PATHS: []}, False)
+    self.mcpu.assertLogContainsRegex(
+      "Node should not have returned forbidden file storage paths")
+
+  @withLockedLU
+  def testMasterInvalidNodeResult(self, lu):
+    lu._VerifyAcceptedFileStoragePaths(self.master, {}, True)
+    self.mcpu.assertLogContainsRegex(
+      "Node did not return forbidden file storage paths")
+
+  @withLockedLU
+  def testMasterForbiddenPaths(self, lu):
+    lu._VerifyAcceptedFileStoragePaths(
+      self.master, {constants.NV_ACCEPTED_STORAGE_PATHS: ["/forbidden"]}, True)
+    self.mcpu.assertLogContainsRegex("Found forbidden file storage paths")
+
+  @withLockedLU
+  def testMasterSuccess(self, lu):
+    lu._VerifyAcceptedFileStoragePaths(
+      self.master, {constants.NV_ACCEPTED_STORAGE_PATHS: []}, True)
+    self.mcpu.assertLogIsEmpty()
+
+
+class TestLUClusterVerifyGroupVerifyStoragePaths(
+  TestLUClusterVerifyGroupMethods):
+  @withLockedLU
+  def testVerifyFileStoragePathsSuccess(self, lu):
+    lu._VerifyFileStoragePaths(self.master, {})
+    self.mcpu.assertLogIsEmpty()
+
+  @withLockedLU
+  def testVerifyFileStoragePathsFailure(self, lu):
+    lu._VerifyFileStoragePaths(self.master,
+                               {constants.NV_FILE_STORAGE_PATH: "/fail/path"})
+    self.mcpu.assertLogContainsRegex(
+      "The configured file storage path is unusable")
+
+  @withLockedLU
+  def testVerifySharedFileStoragePathsSuccess(self, lu):
+    lu._VerifySharedFileStoragePaths(self.master, {})
+    self.mcpu.assertLogIsEmpty()
+
+  @withLockedLU
+  def testVerifySharedFileStoragePathsFailure(self, lu):
+    lu._VerifySharedFileStoragePaths(
+      self.master, {constants.NV_SHARED_FILE_STORAGE_PATH: "/fail/path"})
+    self.mcpu.assertLogContainsRegex(
+      "The configured sharedfile storage path is unusable")
+
+
+class TestLUClusterVerifyGroupVerifyOob(TestLUClusterVerifyGroupMethods):
+  @withLockedLU
+  def testEmptyResult(self, lu):
+    lu._VerifyOob(self.master, {})
+    self.mcpu.assertLogIsEmpty()
+
+  @withLockedLU
+  def testErrorResults(self, lu):
+    lu._VerifyOob(self.master, {constants.NV_OOB_PATHS: ["path1", "path2"]})
+    self.mcpu.assertLogContainsRegex("path1")
+    self.mcpu.assertLogContainsRegex("path2")
+
+
+class TestLUClusterVerifyGroupUpdateNodeVolumes(
+  TestLUClusterVerifyGroupMethods):
+  def setUp(self):
+    super(TestLUClusterVerifyGroupUpdateNodeVolumes, self).setUp()
+    self.nimg = cluster.LUClusterVerifyGroup.NodeImage(uuid=self.master_uuid)
+
+  @withLockedLU
+  def testNoVgName(self, lu):
+    lu._UpdateNodeVolumes(self.master, {}, self.nimg, None)
+    self.mcpu.assertLogIsEmpty()
+    self.assertTrue(self.nimg.lvm_fail)
+
+  @withLockedLU
+  def testErrorMessage(self, lu):
+    lu._UpdateNodeVolumes(self.master, {constants.NV_LVLIST: "mock error"},
+                          self.nimg, "mock_vg")
+    self.mcpu.assertLogContainsRegex("LVM problem on node: mock error")
+    self.assertTrue(self.nimg.lvm_fail)
+
+  @withLockedLU
+  def testInvalidNodeResult(self, lu):
+    lu._UpdateNodeVolumes(self.master, {constants.NV_LVLIST: [1, 2, 3]},
+                          self.nimg, "mock_vg")
+    self.mcpu.assertLogContainsRegex("rpc call to node failed")
+    self.assertTrue(self.nimg.lvm_fail)
+
+  @withLockedLU
+  def testValidNodeResult(self, lu):
+    lu._UpdateNodeVolumes(self.master, {constants.NV_LVLIST: {}},
+                          self.nimg, "mock_vg")
+    self.mcpu.assertLogIsEmpty()
+    self.assertFalse(self.nimg.lvm_fail)
+
+
+class TestLUClusterVerifyGroupUpdateNodeInstances(
+  TestLUClusterVerifyGroupMethods):
+  def setUp(self):
+    super(TestLUClusterVerifyGroupUpdateNodeInstances, self).setUp()
+    self.nimg = cluster.LUClusterVerifyGroup.NodeImage(uuid=self.master_uuid)
+
+  @withLockedLU
+  def testInvalidNodeResult(self, lu):
+    lu._UpdateNodeInstances(self.master, {}, self.nimg)
+    self.mcpu.assertLogContainsRegex("rpc call to node failed")
+
+  @withLockedLU
+  def testValidNodeResult(self, lu):
+    inst = self.cfg.AddNewInstance()
+    lu._UpdateNodeInstances(self.master,
+                            {constants.NV_INSTANCELIST: [inst.name]},
+                            self.nimg)
+    self.mcpu.assertLogIsEmpty()
+
+
+class TestLUClusterVerifyGroupUpdateNodeInfo(TestLUClusterVerifyGroupMethods):
+  def setUp(self):
+    super(TestLUClusterVerifyGroupUpdateNodeInfo, self).setUp()
+    self.nimg = cluster.LUClusterVerifyGroup.NodeImage(uuid=self.master_uuid)
+    self.valid_hvresult = {constants.NV_HVINFO: {"memory_free": 1024}}
+
+  @withLockedLU
+  def testInvalidHvNodeResult(self, lu):
+    for ndata in [{}, {constants.NV_HVINFO: ""}]:
+      self.mcpu.ClearLogMessages()
+      lu._UpdateNodeInfo(self.master, ndata, self.nimg, None)
+      self.mcpu.assertLogContainsRegex("rpc call to node failed")
+
+  @withLockedLU
+  def testInvalidMemoryFreeHvNodeResult(self, lu):
+    lu._UpdateNodeInfo(self.master,
+                       {constants.NV_HVINFO: {"memory_free": "abc"}},
+                       self.nimg, None)
+    self.mcpu.assertLogContainsRegex(
+      "node returned invalid nodeinfo, check hypervisor")
+
+  @withLockedLU
+  def testValidHvNodeResult(self, lu):
+    lu._UpdateNodeInfo(self.master, self.valid_hvresult, self.nimg, None)
+    self.mcpu.assertLogIsEmpty()
+
+  @withLockedLU
+  def testInvalidVgNodeResult(self, lu):
+    for vgdata in [[], ""]:
+      self.mcpu.ClearLogMessages()
+      ndata = {constants.NV_VGLIST: vgdata}
+      ndata.update(self.valid_hvresult)
+      lu._UpdateNodeInfo(self.master, ndata, self.nimg, "mock_vg")
+      self.mcpu.assertLogContainsRegex(
+        "node didn't return data for the volume group 'mock_vg'")
+
+  @withLockedLU
+  def testInvalidDiskFreeVgNodeResult(self, lu):
+    self.valid_hvresult.update({
+      constants.NV_VGLIST: {"mock_vg": "abc"}
+    })
+    lu._UpdateNodeInfo(self.master, self.valid_hvresult, self.nimg, "mock_vg")
+    self.mcpu.assertLogContainsRegex(
+      "node returned invalid LVM info, check LVM status")
+
+  @withLockedLU
+  def testValidVgNodeResult(self, lu):
+    self.valid_hvresult.update({
+      constants.NV_VGLIST: {"mock_vg": 10000}
+    })
+    lu._UpdateNodeInfo(self.master, self.valid_hvresult, self.nimg, "mock_vg")
+    self.mcpu.assertLogIsEmpty()
+
+
+class TestLUClusterVerifyGroupCollectDiskInfo(TestLUClusterVerifyGroupMethods):
+  def setUp(self):
+    super(TestLUClusterVerifyGroupCollectDiskInfo, self).setUp()
+
+    self.node1 = self.cfg.AddNewNode()
+    self.node2 = self.cfg.AddNewNode()
+    self.node3 = self.cfg.AddNewNode()
+
+    self.diskless_inst = \
+      self.cfg.AddNewInstance(primary_node=self.node1,
+                              disk_template=constants.DT_DISKLESS)
+    self.plain_inst = \
+      self.cfg.AddNewInstance(primary_node=self.node2,
+                              disk_template=constants.DT_PLAIN)
+    self.drbd_inst = \
+      self.cfg.AddNewInstance(primary_node=self.node3,
+                              secondary_node=self.node2,
+                              disk_template=constants.DT_DRBD8)
+
+    self.node1_img = cluster.LUClusterVerifyGroup.NodeImage(
+                       uuid=self.node1.uuid)
+    self.node1_img.pinst = [self.diskless_inst.uuid]
+    self.node1_img.sinst = []
+    self.node2_img = cluster.LUClusterVerifyGroup.NodeImage(
+                       uuid=self.node2.uuid)
+    self.node2_img.pinst = [self.plain_inst.uuid]
+    self.node2_img.sinst = [self.drbd_inst.uuid]
+    self.node3_img = cluster.LUClusterVerifyGroup.NodeImage(
+                       uuid=self.node3.uuid)
+    self.node3_img.pinst = [self.drbd_inst.uuid]
+    self.node3_img.sinst = []
+
+    self.node_images = {
+      self.node1.uuid: self.node1_img,
+      self.node2.uuid: self.node2_img,
+      self.node3.uuid: self.node3_img
+    }
+
+    self.node_uuids = [self.node1.uuid, self.node2.uuid, self.node3.uuid]
+
+  @withLockedLU
+  def testSuccessfulRun(self, lu):
+    self.rpc.call_blockdev_getmirrorstatus_multi.return_value = \
+      RpcResultsBuilder() \
+        .AddSuccessfulNode(self.node2, [(True, ""), (True, "")]) \
+        .AddSuccessfulNode(self.node3, [(True, "")]) \
+        .Build()
+
+    lu._CollectDiskInfo(self.node_uuids, self.node_images,
+                        self.cfg.GetAllInstancesInfo())
+
+    self.mcpu.assertLogIsEmpty()
+
+  @withLockedLU
+  def testOfflineAndFailingNodes(self, lu):
+    self.rpc.call_blockdev_getmirrorstatus_multi.return_value = \
+      RpcResultsBuilder() \
+        .AddOfflineNode(self.node2) \
+        .AddFailedNode(self.node3) \
+        .Build()
+
+    lu._CollectDiskInfo(self.node_uuids, self.node_images,
+                        self.cfg.GetAllInstancesInfo())
+
+    self.mcpu.assertLogContainsRegex("while getting disk information")
+
+  @withLockedLU
+  def testInvalidNodeResult(self, lu):
+    self.rpc.call_blockdev_getmirrorstatus_multi.return_value = \
+      RpcResultsBuilder() \
+        .AddSuccessfulNode(self.node2, [(True,), (False,)]) \
+        .AddSuccessfulNode(self.node3, [""]) \
+        .Build()
+
+    lu._CollectDiskInfo(self.node_uuids, self.node_images,
+                        self.cfg.GetAllInstancesInfo())
+    # logging is not performed through mcpu
+    self.mcpu.assertLogIsEmpty()
+
+
+class TestLUClusterVerifyGroupHooksCallBack(TestLUClusterVerifyGroupMethods):
+  def setUp(self):
+    super(TestLUClusterVerifyGroupHooksCallBack, self).setUp()
+
+    self.feedback_fn = lambda _: None
+
+  def PrepareLU(self, lu):
+    super(TestLUClusterVerifyGroupHooksCallBack, self).PrepareLU(lu)
+
+    lu.my_node_uuids = list(self.cfg.GetAllNodesInfo().keys())
+
+  @withLockedLU
+  def testEmptyGroup(self, lu):
+    lu.my_node_uuids = []
+    lu.HooksCallBack(constants.HOOKS_PHASE_POST, None, self.feedback_fn, None)
+
+  @withLockedLU
+  def testFailedResult(self, lu):
+    lu.HooksCallBack(constants.HOOKS_PHASE_POST,
+                     RpcResultsBuilder(use_node_names=True)
+                       .AddFailedNode(self.master).Build(),
+                     self.feedback_fn,
+                     None)
+    self.mcpu.assertLogContainsRegex("Communication failure in hooks execution")
+
+  @withLockedLU
+  def testOfflineNode(self, lu):
+    lu.HooksCallBack(constants.HOOKS_PHASE_POST,
+                     RpcResultsBuilder(use_node_names=True)
+                       .AddOfflineNode(self.master).Build(),
+                     self.feedback_fn,
+                     None)
+
+  @withLockedLU
+  def testValidResult(self, lu):
+    lu.HooksCallBack(constants.HOOKS_PHASE_POST,
+                     RpcResultsBuilder(use_node_names=True)
+                       .AddSuccessfulNode(self.master,
+                                          [("mock_script",
+                                            constants.HKR_SUCCESS,
+                                            "mock output")])
+                       .Build(),
+                     self.feedback_fn,
+                     None)
+
+  @withLockedLU
+  def testFailedScriptResult(self, lu):
+    lu.HooksCallBack(constants.HOOKS_PHASE_POST,
+                     RpcResultsBuilder(use_node_names=True)
+                       .AddSuccessfulNode(self.master,
+                                          [("mock_script",
+                                            constants.HKR_FAIL,
+                                            "mock output")])
+                       .Build(),
+                     self.feedback_fn,
+                     None)
+    self.mcpu.assertLogContainsRegex("Script mock_script failed")
+
+
+class TestLUClusterVerifyDisks(CmdlibTestCase):
+  def testVerifyDisks(self):
+    op = opcodes.OpClusterVerifyDisks()
+    result = self.ExecOpCode(op)
+
+    self.assertEqual(1, len(result["jobs"]))
+
+
+if __name__ == "__main__":
+  testutils.GanetiTestProgram()
diff --git a/test/py/cmdlib/cmdlib_unittest.py b/test/py/cmdlib/cmdlib_unittest.py
new file mode 100755 (executable)
index 0000000..3140f25
--- /dev/null
@@ -0,0 +1,1054 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2008, 2011, 2012, 2013 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 unittesting the cmdlib module"""
+
+
+import unittest
+import operator
+import itertools
+import copy
+
+from ganeti import constants
+from ganeti import mcpu
+from ganeti import cmdlib
+from ganeti.cmdlib import cluster
+from ganeti.cmdlib import instance
+from ganeti.cmdlib import instance_storage
+from ganeti.cmdlib import instance_utils
+from ganeti.cmdlib import common
+from ganeti.cmdlib import query
+from ganeti import opcodes
+from ganeti import errors
+from ganeti import utils
+from ganeti import luxi
+from ganeti import ht
+from ganeti import objects
+from ganeti import compat
+from ganeti import rpc
+from ganeti import locking
+from ganeti.masterd import iallocator
+
+import testutils
+import mocks
+
+
+class TestOpcodeParams(testutils.GanetiTestCase):
+  def testParamsStructures(self):
+    for op in sorted(mcpu.Processor.DISPATCH_TABLE):
+      lu = mcpu.Processor.DISPATCH_TABLE[op]
+      lu_name = lu.__name__
+      self.failIf(hasattr(lu, "_OP_REQP"),
+                  msg=("LU '%s' has old-style _OP_REQP" % lu_name))
+      self.failIf(hasattr(lu, "_OP_DEFS"),
+                  msg=("LU '%s' has old-style _OP_DEFS" % lu_name))
+      self.failIf(hasattr(lu, "_OP_PARAMS"),
+                  msg=("LU '%s' has old-style _OP_PARAMS" % lu_name))
+
+
+class TestIAllocatorChecks(testutils.GanetiTestCase):
+  def testFunction(self):
+    class TestLU(object):
+      def __init__(self, opcode):
+        self.cfg = mocks.FakeConfig()
+        self.op = opcode
+
+    class OpTest(opcodes.OpCode):
+       OP_PARAMS = [
+        ("iallocator", None, ht.TAny, None),
+        ("node", None, ht.TAny, None),
+        ]
+
+    default_iallocator = mocks.FakeConfig().GetDefaultIAllocator()
+    other_iallocator = default_iallocator + "_not"
+
+    op = OpTest()
+    lu = TestLU(op)
+
+    c_i = lambda: common.CheckIAllocatorOrNode(lu, "iallocator", "node")
+
+    # Neither node nor iallocator given
+    for n in (None, []):
+      op.iallocator = None
+      op.node = n
+      c_i()
+      self.assertEqual(lu.op.iallocator, default_iallocator)
+      self.assertEqual(lu.op.node, n)
+
+    # Both, iallocator and node given
+    for a in ("test", constants.DEFAULT_IALLOCATOR_SHORTCUT):
+      op.iallocator = a
+      op.node = "test"
+      self.assertRaises(errors.OpPrereqError, c_i)
+
+    # Only iallocator given
+    for n in (None, []):
+      op.iallocator = other_iallocator
+      op.node = n
+      c_i()
+      self.assertEqual(lu.op.iallocator, other_iallocator)
+      self.assertEqual(lu.op.node, n)
+
+    # Only node given
+    op.iallocator = None
+    op.node = "node"
+    c_i()
+    self.assertEqual(lu.op.iallocator, None)
+    self.assertEqual(lu.op.node, "node")
+
+    # Asked for default iallocator, no node given
+    op.iallocator = constants.DEFAULT_IALLOCATOR_SHORTCUT
+    op.node = None
+    c_i()
+    self.assertEqual(lu.op.iallocator, default_iallocator)
+    self.assertEqual(lu.op.node, None)
+
+    # No node, iallocator or default iallocator
+    op.iallocator = None
+    op.node = None
+    lu.cfg.GetDefaultIAllocator = lambda: None
+    self.assertRaises(errors.OpPrereqError, c_i)
+
+
+class TestLUTestJqueue(unittest.TestCase):
+  def test(self):
+    self.assert_(cmdlib.LUTestJqueue._CLIENT_CONNECT_TIMEOUT <
+                 (luxi.WFJC_TIMEOUT * 0.75),
+                 msg=("Client timeout too high, might not notice bugs"
+                      " in WaitForJobChange"))
+
+
+class TestLUQuery(unittest.TestCase):
+  def test(self):
+    self.assertEqual(sorted(query._QUERY_IMPL.keys()),
+                     sorted(constants.QR_VIA_OP))
+
+    assert constants.QR_NODE in constants.QR_VIA_OP
+    assert constants.QR_INSTANCE in constants.QR_VIA_OP
+
+    for i in constants.QR_VIA_OP:
+      self.assert_(query._GetQueryImplementation(i))
+
+    self.assertRaises(errors.OpPrereqError, query._GetQueryImplementation,
+                      "")
+    self.assertRaises(errors.OpPrereqError, query._GetQueryImplementation,
+                      "xyz")
+
+
+class _FakeLU:
+  def __init__(self, cfg=NotImplemented, proc=NotImplemented,
+               rpc=NotImplemented):
+    self.warning_log = []
+    self.info_log = []
+    self.cfg = cfg
+    self.proc = proc
+    self.rpc = rpc
+
+  def LogWarning(self, text, *args):
+    self.warning_log.append((text, args))
+
+  def LogInfo(self, text, *args):
+    self.info_log.append((text, args))
+
+
+class TestLoadNodeEvacResult(unittest.TestCase):
+  def testSuccess(self):
+    for moved in [[], [
+      ("inst20153.example.com", "grp2", ["nodeA4509", "nodeB2912"]),
+      ]]:
+      for early_release in [False, True]:
+        for use_nodes in [False, True]:
+          jobs = [
+            [opcodes.OpInstanceReplaceDisks().__getstate__()],
+            [opcodes.OpInstanceMigrate().__getstate__()],
+            ]
+
+          alloc_result = (moved, [], jobs)
+          assert iallocator._NEVAC_RESULT(alloc_result)
+
+          lu = _FakeLU()
+          result = common.LoadNodeEvacResult(lu, alloc_result,
+                                             early_release, use_nodes)
+
+          if moved:
+            (_, (info_args, )) = lu.info_log.pop(0)
+            for (instname, instgroup, instnodes) in moved:
+              self.assertTrue(instname in info_args)
+              if use_nodes:
+                for i in instnodes:
+                  self.assertTrue(i in info_args)
+              else:
+                self.assertTrue(instgroup in info_args)
+
+          self.assertFalse(lu.info_log)
+          self.assertFalse(lu.warning_log)
+
+          for op in itertools.chain(*result):
+            if hasattr(op.__class__, "early_release"):
+              self.assertEqual(op.early_release, early_release)
+            else:
+              self.assertFalse(hasattr(op, "early_release"))
+
+  def testFailed(self):
+    alloc_result = ([], [
+      ("inst5191.example.com", "errormsg21178"),
+      ], [])
+    assert iallocator._NEVAC_RESULT(alloc_result)
+
+    lu = _FakeLU()
+    self.assertRaises(errors.OpExecError, common.LoadNodeEvacResult,
+                      lu, alloc_result, False, False)
+    self.assertFalse(lu.info_log)
+    (_, (args, )) = lu.warning_log.pop(0)
+    self.assertTrue("inst5191.example.com" in args)
+    self.assertTrue("errormsg21178" in args)
+    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 = common._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,
+                      common._UpdateAndVerifySubDict, {}, test,
+                      self.type_check)
+
+
+class TestHvStateHelper(unittest.TestCase):
+  def testWithoutOpData(self):
+    self.assertEqual(common.MergeAndVerifyHvState(None, NotImplemented),
+                     None)
+
+  def testWithoutOldData(self):
+    new = {
+      constants.HT_XEN_PVM: {
+        constants.HVST_MEMORY_TOTAL: 4096,
+        },
+      }
+    self.assertEqual(common.MergeAndVerifyHvState(new, None), new)
+
+  def testWithWrongHv(self):
+    new = {
+      "i-dont-exist": {
+        constants.HVST_MEMORY_TOTAL: 4096,
+        },
+      }
+    self.assertRaises(errors.OpPrereqError, common.MergeAndVerifyHvState,
+                      new, None)
+
+class TestDiskStateHelper(unittest.TestCase):
+  def testWithoutOpData(self):
+    self.assertEqual(common.MergeAndVerifyDiskState(None, NotImplemented),
+                     None)
+
+  def testWithoutOldData(self):
+    new = {
+      constants.DT_PLAIN: {
+        "xenvg": {
+          constants.DS_DISK_RESERVED: 1024,
+          },
+        },
+      }
+    self.assertEqual(common.MergeAndVerifyDiskState(new, None), new)
+
+  def testWithWrongStorageType(self):
+    new = {
+      "i-dont-exist": {
+        "xenvg": {
+          constants.DS_DISK_RESERVED: 1024,
+          },
+        },
+      }
+    self.assertRaises(errors.OpPrereqError, common.MergeAndVerifyDiskState,
+                      new, None)
+
+
+class TestComputeMinMaxSpec(unittest.TestCase):
+  def setUp(self):
+    self.ispecs = {
+      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(common._ComputeMinMaxSpec(constants.ISPEC_MEM_SIZE, None,
+                                              self.ispecs, None) is None)
+
+  def testAutoValue(self):
+    self.assertTrue(common._ComputeMinMaxSpec(constants.ISPEC_MEM_SIZE, None,
+                                              self.ispecs,
+                                              constants.VALUE_AUTO) is None)
+
+  def testNotDefined(self):
+    self.assertTrue(common._ComputeMinMaxSpec(constants.ISPEC_NIC_COUNT, None,
+                                              self.ispecs, 3) is None)
+
+  def testNoMinDefined(self):
+    self.assertTrue(common._ComputeMinMaxSpec(constants.ISPEC_DISK_SIZE, None,
+                                              self.ispecs, 128) is None)
+
+  def testNoMaxDefined(self):
+    self.assertTrue(common._ComputeMinMaxSpec(constants.ISPEC_DISK_COUNT,
+                                              None, self.ispecs, 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.ispecs[constants.ISPECS_MIN].get(name, val)
+      max_v = self.ispecs[constants.ISPECS_MAX].get(name, val)
+      self.assertEqual(common._ComputeMinMaxSpec(name, None,
+                                                 self.ispecs, val),
+                       "%s value %s is not in range [%s, %s]" %
+                       (name, val,min_v, max_v))
+      self.assertEqual(common._ComputeMinMaxSpec(name, "1",
+                                                 self.ispecs, 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(common._ComputeMinMaxSpec(name, None, self.ispecs, val)
+                      is None)
+
+
+def _ValidateComputeMinMaxSpec(name, *_):
+  assert name in constants.ISPECS_PARAMETERS
+  return None
+
+
+def _NoDiskComputeMinMaxSpec(name, *_):
+  if name == constants.ISPEC_DISK_COUNT:
+    return name
+  else:
+    return None
+
+
+class _SpecWrapper:
+  def __init__(self, spec):
+    self.spec = spec
+
+  def ComputeMinMaxSpec(self, *args):
+    return self.spec.pop(0)
+
+
+class TestComputeIPolicySpecViolation(unittest.TestCase):
+  # Minimal policy accepted by _ComputeIPolicySpecViolation()
+  _MICRO_IPOL = {
+    constants.IPOLICY_DTS: [constants.DT_PLAIN, constants.DT_DISKLESS],
+    constants.ISPECS_MINMAX: [NotImplemented],
+    }
+
+  def test(self):
+    compute_fn = _ValidateComputeMinMaxSpec
+    ret = common.ComputeIPolicySpecViolation(self._MICRO_IPOL, 1024, 1, 1, 1,
+                                             [1024], 1, constants.DT_PLAIN,
+                                             _compute_fn=compute_fn)
+    self.assertEqual(ret, [])
+
+  def testDiskFull(self):
+    compute_fn = _NoDiskComputeMinMaxSpec
+    ret = common.ComputeIPolicySpecViolation(self._MICRO_IPOL, 1024, 1, 1, 1,
+                                             [1024], 1, constants.DT_PLAIN,
+                                             _compute_fn=compute_fn)
+    self.assertEqual(ret, [constants.ISPEC_DISK_COUNT])
+
+  def testDiskLess(self):
+    compute_fn = _NoDiskComputeMinMaxSpec
+    ret = common.ComputeIPolicySpecViolation(self._MICRO_IPOL, 1024, 1, 1, 1,
+                                             [1024], 1, constants.DT_DISKLESS,
+                                             _compute_fn=compute_fn)
+    self.assertEqual(ret, [])
+
+  def testWrongTemplates(self):
+    compute_fn = _ValidateComputeMinMaxSpec
+    ret = common.ComputeIPolicySpecViolation(self._MICRO_IPOL, 1024, 1, 1, 1,
+                                             [1024], 1, constants.DT_DRBD8,
+                                             _compute_fn=compute_fn)
+    self.assertEqual(len(ret), 1)
+    self.assertTrue("Disk template" in ret[0])
+
+  def testInvalidArguments(self):
+    self.assertRaises(AssertionError, common.ComputeIPolicySpecViolation,
+                      self._MICRO_IPOL, 1024, 1, 1, 1, [], 1,
+                      constants.DT_PLAIN,)
+
+  def testInvalidSpec(self):
+    spec = _SpecWrapper([None, False, "foo", None, "bar", None])
+    compute_fn = spec.ComputeMinMaxSpec
+    ret = common.ComputeIPolicySpecViolation(self._MICRO_IPOL, 1024, 1, 1, 1,
+                                             [1024], 1, constants.DT_PLAIN,
+                                             _compute_fn=compute_fn)
+    self.assertEqual(ret, ["foo", "bar"])
+    self.assertFalse(spec.spec)
+
+  def testWithIPolicy(self):
+    mem_size = 2048
+    cpu_count = 2
+    disk_count = 1
+    disk_sizes = [512]
+    nic_count = 1
+    spindle_use = 4
+    disk_template = "mytemplate"
+    ispec = {
+      constants.ISPEC_MEM_SIZE: mem_size,
+      constants.ISPEC_CPU_COUNT: cpu_count,
+      constants.ISPEC_DISK_COUNT: disk_count,
+      constants.ISPEC_DISK_SIZE: disk_sizes[0],
+      constants.ISPEC_NIC_COUNT: nic_count,
+      constants.ISPEC_SPINDLE_USE: spindle_use,
+      }
+    ipolicy1 = {
+      constants.ISPECS_MINMAX: [{
+        constants.ISPECS_MIN: ispec,
+        constants.ISPECS_MAX: ispec,
+        }],
+      constants.IPOLICY_DTS: [disk_template],
+      }
+    ispec_copy = copy.deepcopy(ispec)
+    ipolicy2 = {
+      constants.ISPECS_MINMAX: [
+        {
+          constants.ISPECS_MIN: ispec_copy,
+          constants.ISPECS_MAX: ispec_copy,
+          },
+        {
+          constants.ISPECS_MIN: ispec,
+          constants.ISPECS_MAX: ispec,
+          },
+        ],
+      constants.IPOLICY_DTS: [disk_template],
+      }
+    ipolicy3 = {
+      constants.ISPECS_MINMAX: [
+        {
+          constants.ISPECS_MIN: ispec,
+          constants.ISPECS_MAX: ispec,
+          },
+        {
+          constants.ISPECS_MIN: ispec_copy,
+          constants.ISPECS_MAX: ispec_copy,
+          },
+        ],
+      constants.IPOLICY_DTS: [disk_template],
+      }
+    def AssertComputeViolation(ipolicy, violations):
+      ret = common.ComputeIPolicySpecViolation(ipolicy, mem_size, cpu_count,
+                                               disk_count, nic_count,
+                                               disk_sizes, spindle_use,
+                                               disk_template)
+      self.assertEqual(len(ret), violations)
+
+    AssertComputeViolation(ipolicy1, 0)
+    AssertComputeViolation(ipolicy2, 0)
+    AssertComputeViolation(ipolicy3, 0)
+    for par in constants.ISPECS_PARAMETERS:
+      ispec[par] += 1
+      AssertComputeViolation(ipolicy1, 1)
+      AssertComputeViolation(ipolicy2, 0)
+      AssertComputeViolation(ipolicy3, 0)
+      ispec[par] -= 2
+      AssertComputeViolation(ipolicy1, 1)
+      AssertComputeViolation(ipolicy2, 0)
+      AssertComputeViolation(ipolicy3, 0)
+      ispec[par] += 1 # Restore
+    ipolicy1[constants.IPOLICY_DTS] = ["another_template"]
+    AssertComputeViolation(ipolicy1, 1)
+
+
+class _StubComputeIPolicySpecViolation:
+  def __init__(self, mem_size, cpu_count, disk_count, nic_count, disk_sizes,
+               spindle_use, disk_template):
+    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
+    self.disk_template = disk_template
+
+  def __call__(self, _, mem_size, cpu_count, disk_count, nic_count, disk_sizes,
+               spindle_use, disk_template):
+    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
+    assert self.disk_template == disk_template
+
+    return []
+
+
+class _FakeConfigForComputeIPolicyInstanceViolation:
+  def __init__(self, be, excl_stor):
+    self.cluster = objects.Cluster(beparams={"default": be})
+    self.excl_stor = excl_stor
+
+  def GetClusterInfo(self):
+    return self.cluster
+
+  def GetNodeInfo(self, _):
+    return {}
+
+  def GetNdParams(self, _):
+    return {
+      constants.ND_EXCLUSIVE_STORAGE: self.excl_stor,
+      }
+
+
+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, spindles=13)]
+    cfg = _FakeConfigForComputeIPolicyInstanceViolation(beparams, False)
+    instance = objects.Instance(beparams=beparams, disks=disks, nics=[],
+                                disk_template=constants.DT_PLAIN)
+    stub = _StubComputeIPolicySpecViolation(2048, 2, 1, 0, [512], 4,
+                                            constants.DT_PLAIN)
+    ret = common.ComputeIPolicyInstanceViolation(NotImplemented, instance,
+                                                 cfg, _compute_fn=stub)
+    self.assertEqual(ret, [])
+    instance2 = objects.Instance(beparams={}, disks=disks, nics=[],
+                                 disk_template=constants.DT_PLAIN)
+    ret = common.ComputeIPolicyInstanceViolation(NotImplemented, instance2,
+                                                 cfg, _compute_fn=stub)
+    self.assertEqual(ret, [])
+    cfg_es = _FakeConfigForComputeIPolicyInstanceViolation(beparams, True)
+    stub_es = _StubComputeIPolicySpecViolation(2048, 2, 1, 0, [512], 13,
+                                               constants.DT_PLAIN)
+    ret = common.ComputeIPolicyInstanceViolation(NotImplemented, instance,
+                                                 cfg_es, _compute_fn=stub_es)
+    self.assertEqual(ret, [])
+    ret = common.ComputeIPolicyInstanceViolation(NotImplemented, instance2,
+                                                 cfg_es, _compute_fn=stub_es)
+    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 = instance_utils._ComputeIPolicyNodeViolation(
+      NotImplemented,
+      NotImplemented,
+      "foo", "foo", NotImplemented,
+      _compute_fn=self.recorder)
+    self.assertFalse(self.recorder.called)
+    self.assertEqual(ret, [])
+
+  def testDifferentGroup(self):
+    ret = instance_utils._ComputeIPolicyNodeViolation(
+      NotImplemented,
+      NotImplemented,
+      "foo", "bar", NotImplemented,
+      _compute_fn=self.recorder)
+    self.assertTrue(self.recorder.called)
+    self.assertEqual(ret, [])
+
+
+class TestDiskSizeInBytesToMebibytes(unittest.TestCase):
+  def testLessThanOneMebibyte(self):
+    for i in [1, 2, 7, 512, 1000, 1023]:
+      lu = _FakeLU()
+      result = instance_storage._DiskSizeInBytesToMebibytes(lu, i)
+      self.assertEqual(result, 1)
+      self.assertEqual(len(lu.warning_log), 1)
+      self.assertEqual(len(lu.warning_log[0]), 2)
+      (_, (warnsize, )) = lu.warning_log[0]
+      self.assertEqual(warnsize, (1024 * 1024) - i)
+
+  def testEven(self):
+    for i in [1, 2, 7, 512, 1000, 1023]:
+      lu = _FakeLU()
+      result = instance_storage._DiskSizeInBytesToMebibytes(lu,
+                                                            i * 1024 * 1024)
+      self.assertEqual(result, i)
+      self.assertFalse(lu.warning_log)
+
+  def testLargeNumber(self):
+    for i in [1, 2, 7, 512, 1000, 1023, 2724, 12420]:
+      for j in [1, 2, 486, 326, 986, 1023]:
+        lu = _FakeLU()
+        size = (1024 * 1024 * i) + j
+        result = instance_storage._DiskSizeInBytesToMebibytes(lu, size)
+        self.assertEqual(result, i + 1, msg="Amount was not rounded up")
+        self.assertEqual(len(lu.warning_log), 1)
+        self.assertEqual(len(lu.warning_log[0]), 2)
+        (_, (warnsize, )) = lu.warning_log[0]
+        self.assertEqual(warnsize, (1024 * 1024) - j)
+
+
+class _OpTestVerifyErrors(opcodes.OpCode):
+  OP_PARAMS = [
+    ("debug_simulate_errors", False, ht.TBool, ""),
+    ("error_codes", False, ht.TBool, ""),
+    ("ignore_errors",
+     [],
+     ht.TListOf(ht.TElemOf(constants.CV_ALL_ECODES_STRINGS)),
+     "")
+    ]
+
+
+class _LuTestVerifyErrors(cluster._VerifyErrors):
+  def __init__(self, **kwargs):
+    cluster._VerifyErrors.__init__(self)
+    self.op = _OpTestVerifyErrors(**kwargs)
+    self.op.Validate(True)
+    self.msglist = []
+    self._feedback_fn = self.msglist.append
+    self.bad = False
+
+  def DispatchCallError(self, which, *args, **kwargs):
+    if which:
+      self._Error(*args, **kwargs)
+    else:
+      self._ErrorIf(True, *args, **kwargs)
+
+  def CallErrorIf(self, c, *args, **kwargs):
+    self._ErrorIf(c, *args, **kwargs)
+
+
+class TestVerifyErrors(unittest.TestCase):
+  # Fake cluster-verify error code structures; we use two arbitary real error
+  # codes to pass validation of ignore_errors
+  (_, _ERR1ID, _) = constants.CV_ECLUSTERCFG
+  _NODESTR = "node"
+  _NODENAME = "mynode"
+  _ERR1CODE = (_NODESTR, _ERR1ID, "Error one")
+  (_, _ERR2ID, _) = constants.CV_ECLUSTERCERT
+  _INSTSTR = "instance"
+  _INSTNAME = "myinstance"
+  _ERR2CODE = (_INSTSTR, _ERR2ID, "Error two")
+  # Arguments used to call _Error() or _ErrorIf()
+  _ERR1ARGS = (_ERR1CODE, _NODENAME, "Error1 is %s", "an error")
+  _ERR2ARGS = (_ERR2CODE, _INSTNAME, "Error2 has no argument")
+  # Expected error messages
+  _ERR1MSG = _ERR1ARGS[2] % _ERR1ARGS[3]
+  _ERR2MSG = _ERR2ARGS[2]
+
+  def testNoError(self):
+    lu = _LuTestVerifyErrors()
+    lu.CallErrorIf(False, self._ERR1CODE, *self._ERR1ARGS)
+    self.assertFalse(lu.bad)
+    self.assertFalse(lu.msglist)
+
+  def _InitTest(self, **kwargs):
+    self.lu1 = _LuTestVerifyErrors(**kwargs)
+    self.lu2 = _LuTestVerifyErrors(**kwargs)
+
+  def _CallError(self, *args, **kwargs):
+    # Check that _Error() and _ErrorIf() produce the same results
+    self.lu1.DispatchCallError(True, *args, **kwargs)
+    self.lu2.DispatchCallError(False, *args, **kwargs)
+    self.assertEqual(self.lu1.bad, self.lu2.bad)
+    self.assertEqual(self.lu1.msglist, self.lu2.msglist)
+    # Test-specific checks are made on one LU
+    return self.lu1
+
+  def _checkMsgCommon(self, logstr, errmsg, itype, item, warning):
+    self.assertTrue(errmsg in logstr)
+    if warning:
+      self.assertTrue("WARNING" in logstr)
+    else:
+      self.assertTrue("ERROR" in logstr)
+    self.assertTrue(itype in logstr)
+    self.assertTrue(item in logstr)
+
+  def _checkMsg1(self, logstr, warning=False):
+    self._checkMsgCommon(logstr, self._ERR1MSG, self._NODESTR,
+                         self._NODENAME, warning)
+
+  def _checkMsg2(self, logstr, warning=False):
+    self._checkMsgCommon(logstr, self._ERR2MSG, self._INSTSTR,
+                         self._INSTNAME, warning)
+
+  def testPlain(self):
+    self._InitTest()
+    lu = self._CallError(*self._ERR1ARGS)
+    self.assertTrue(lu.bad)
+    self.assertEqual(len(lu.msglist), 1)
+    self._checkMsg1(lu.msglist[0])
+
+  def testMultiple(self):
+    self._InitTest()
+    self._CallError(*self._ERR1ARGS)
+    lu = self._CallError(*self._ERR2ARGS)
+    self.assertTrue(lu.bad)
+    self.assertEqual(len(lu.msglist), 2)
+    self._checkMsg1(lu.msglist[0])
+    self._checkMsg2(lu.msglist[1])
+
+  def testIgnore(self):
+    self._InitTest(ignore_errors=[self._ERR1ID])
+    lu = self._CallError(*self._ERR1ARGS)
+    self.assertFalse(lu.bad)
+    self.assertEqual(len(lu.msglist), 1)
+    self._checkMsg1(lu.msglist[0], warning=True)
+
+  def testWarning(self):
+    self._InitTest()
+    lu = self._CallError(*self._ERR1ARGS,
+                         code=_LuTestVerifyErrors.ETYPE_WARNING)
+    self.assertFalse(lu.bad)
+    self.assertEqual(len(lu.msglist), 1)
+    self._checkMsg1(lu.msglist[0], warning=True)
+
+  def testWarning2(self):
+    self._InitTest()
+    self._CallError(*self._ERR1ARGS)
+    lu = self._CallError(*self._ERR2ARGS,
+                         code=_LuTestVerifyErrors.ETYPE_WARNING)
+    self.assertTrue(lu.bad)
+    self.assertEqual(len(lu.msglist), 2)
+    self._checkMsg1(lu.msglist[0])
+    self._checkMsg2(lu.msglist[1], warning=True)
+
+  def testDebugSimulate(self):
+    lu = _LuTestVerifyErrors(debug_simulate_errors=True)
+    lu.CallErrorIf(False, *self._ERR1ARGS)
+    self.assertTrue(lu.bad)
+    self.assertEqual(len(lu.msglist), 1)
+    self._checkMsg1(lu.msglist[0])
+
+  def testErrCodes(self):
+    self._InitTest(error_codes=True)
+    lu = self._CallError(*self._ERR1ARGS)
+    self.assertTrue(lu.bad)
+    self.assertEqual(len(lu.msglist), 1)
+    self._checkMsg1(lu.msglist[0])
+    self.assertTrue(self._ERR1ID in lu.msglist[0])
+
+
+class TestGetUpdatedIPolicy(unittest.TestCase):
+  """Tests for cmdlib._GetUpdatedIPolicy()"""
+  _OLD_CLUSTER_POLICY = {
+    constants.IPOLICY_VCPU_RATIO: 1.5,
+    constants.ISPECS_MINMAX: [
+      {
+        constants.ISPECS_MIN: {
+          constants.ISPEC_MEM_SIZE: 32768,
+          constants.ISPEC_CPU_COUNT: 8,
+          constants.ISPEC_DISK_COUNT: 1,
+          constants.ISPEC_DISK_SIZE: 1024,
+          constants.ISPEC_NIC_COUNT: 1,
+          constants.ISPEC_SPINDLE_USE: 1,
+          },
+        constants.ISPECS_MAX: {
+          constants.ISPEC_MEM_SIZE: 65536,
+          constants.ISPEC_CPU_COUNT: 10,
+          constants.ISPEC_DISK_COUNT: 5,
+          constants.ISPEC_DISK_SIZE: 1024 * 1024,
+          constants.ISPEC_NIC_COUNT: 3,
+          constants.ISPEC_SPINDLE_USE: 12,
+          },
+        },
+      constants.ISPECS_MINMAX_DEFAULTS,
+      ],
+    constants.ISPECS_STD: constants.IPOLICY_DEFAULTS[constants.ISPECS_STD],
+    }
+  _OLD_GROUP_POLICY = {
+    constants.IPOLICY_SPINDLE_RATIO: 2.5,
+    constants.ISPECS_MINMAX: [{
+      constants.ISPECS_MIN: {
+        constants.ISPEC_MEM_SIZE: 128,
+        constants.ISPEC_CPU_COUNT: 1,
+        constants.ISPEC_DISK_COUNT: 1,
+        constants.ISPEC_DISK_SIZE: 1024,
+        constants.ISPEC_NIC_COUNT: 1,
+        constants.ISPEC_SPINDLE_USE: 1,
+        },
+      constants.ISPECS_MAX: {
+        constants.ISPEC_MEM_SIZE: 32768,
+        constants.ISPEC_CPU_COUNT: 8,
+        constants.ISPEC_DISK_COUNT: 5,
+        constants.ISPEC_DISK_SIZE: 1024 * 1024,
+        constants.ISPEC_NIC_COUNT: 3,
+        constants.ISPEC_SPINDLE_USE: 12,
+        },
+      }],
+    }
+
+  def _TestSetSpecs(self, old_policy, isgroup):
+    diff_minmax = [{
+      constants.ISPECS_MIN: {
+        constants.ISPEC_MEM_SIZE: 64,
+        constants.ISPEC_CPU_COUNT: 1,
+        constants.ISPEC_DISK_COUNT: 2,
+        constants.ISPEC_DISK_SIZE: 64,
+        constants.ISPEC_NIC_COUNT: 1,
+        constants.ISPEC_SPINDLE_USE: 1,
+        },
+      constants.ISPECS_MAX: {
+        constants.ISPEC_MEM_SIZE: 16384,
+        constants.ISPEC_CPU_COUNT: 10,
+        constants.ISPEC_DISK_COUNT: 12,
+        constants.ISPEC_DISK_SIZE: 1024,
+        constants.ISPEC_NIC_COUNT: 9,
+        constants.ISPEC_SPINDLE_USE: 18,
+        },
+      }]
+    diff_std = {
+        constants.ISPEC_DISK_COUNT: 10,
+        constants.ISPEC_DISK_SIZE: 512,
+        }
+    diff_policy = {
+      constants.ISPECS_MINMAX: diff_minmax
+      }
+    if not isgroup:
+      diff_policy[constants.ISPECS_STD] = diff_std
+    new_policy = common.GetUpdatedIPolicy(old_policy, diff_policy,
+                                          group_policy=isgroup)
+
+    self.assertTrue(constants.ISPECS_MINMAX in new_policy)
+    self.assertEqual(new_policy[constants.ISPECS_MINMAX], diff_minmax)
+    for key in old_policy:
+      if not key in diff_policy:
+        self.assertTrue(key in new_policy)
+        self.assertEqual(new_policy[key], old_policy[key])
+
+    if not isgroup:
+      new_std = new_policy[constants.ISPECS_STD]
+      for key in diff_std:
+        self.assertTrue(key in new_std)
+        self.assertEqual(new_std[key], diff_std[key])
+      old_std = old_policy.get(constants.ISPECS_STD, {})
+      for key in old_std:
+        self.assertTrue(key in new_std)
+        if key not in diff_std:
+          self.assertEqual(new_std[key], old_std[key])
+
+  def _TestSet(self, old_policy, diff_policy, isgroup):
+    new_policy = common.GetUpdatedIPolicy(old_policy, diff_policy,
+                                           group_policy=isgroup)
+    for key in diff_policy:
+      self.assertTrue(key in new_policy)
+      self.assertEqual(new_policy[key], diff_policy[key])
+    for key in old_policy:
+      if not key in diff_policy:
+        self.assertTrue(key in new_policy)
+        self.assertEqual(new_policy[key], old_policy[key])
+
+  def testSet(self):
+    diff_policy = {
+      constants.IPOLICY_VCPU_RATIO: 3,
+      constants.IPOLICY_DTS: [constants.DT_FILE],
+      }
+    self._TestSet(self._OLD_GROUP_POLICY, diff_policy, True)
+    self._TestSetSpecs(self._OLD_GROUP_POLICY, True)
+    self._TestSet({}, diff_policy, True)
+    self._TestSetSpecs({}, True)
+    self._TestSet(self._OLD_CLUSTER_POLICY, diff_policy, False)
+    self._TestSetSpecs(self._OLD_CLUSTER_POLICY, False)
+
+  def testUnset(self):
+    old_policy = self._OLD_GROUP_POLICY
+    diff_policy = {
+      constants.IPOLICY_SPINDLE_RATIO: constants.VALUE_DEFAULT,
+      }
+    new_policy = common.GetUpdatedIPolicy(old_policy, diff_policy,
+                                          group_policy=True)
+    for key in diff_policy:
+      self.assertFalse(key in new_policy)
+    for key in old_policy:
+      if not key in diff_policy:
+        self.assertTrue(key in new_policy)
+        self.assertEqual(new_policy[key], old_policy[key])
+
+    self.assertRaises(errors.OpPrereqError, common.GetUpdatedIPolicy,
+                      old_policy, diff_policy, group_policy=False)
+
+  def testUnsetEmpty(self):
+    old_policy = {}
+    for key in constants.IPOLICY_ALL_KEYS:
+      diff_policy = {
+        key: constants.VALUE_DEFAULT,
+        }
+    new_policy = common.GetUpdatedIPolicy(old_policy, diff_policy,
+                                          group_policy=True)
+    self.assertEqual(new_policy, old_policy)
+
+  def _TestInvalidKeys(self, old_policy, isgroup):
+    INVALID_KEY = "this_key_shouldnt_be_allowed"
+    INVALID_DICT = {
+      INVALID_KEY: 3,
+      }
+    invalid_policy = INVALID_DICT
+    self.assertRaises(errors.OpPrereqError, common.GetUpdatedIPolicy,
+                      old_policy, invalid_policy, group_policy=isgroup)
+    invalid_ispecs = {
+      constants.ISPECS_MINMAX: [INVALID_DICT],
+      }
+    self.assertRaises(errors.TypeEnforcementError, common.GetUpdatedIPolicy,
+                      old_policy, invalid_ispecs, group_policy=isgroup)
+    if isgroup:
+      invalid_for_group = {
+        constants.ISPECS_STD: constants.IPOLICY_DEFAULTS[constants.ISPECS_STD],
+        }
+      self.assertRaises(errors.OpPrereqError, common.GetUpdatedIPolicy,
+                        old_policy, invalid_for_group, group_policy=isgroup)
+    good_ispecs = self._OLD_CLUSTER_POLICY[constants.ISPECS_MINMAX]
+    invalid_ispecs = copy.deepcopy(good_ispecs)
+    invalid_policy = {
+      constants.ISPECS_MINMAX: invalid_ispecs,
+      }
+    for minmax in invalid_ispecs:
+      for key in constants.ISPECS_MINMAX_KEYS:
+        ispec = minmax[key]
+        ispec[INVALID_KEY] = None
+        self.assertRaises(errors.TypeEnforcementError,
+                          common.GetUpdatedIPolicy, old_policy,
+                          invalid_policy, group_policy=isgroup)
+        del ispec[INVALID_KEY]
+        for par in constants.ISPECS_PARAMETERS:
+          oldv = ispec[par]
+          ispec[par] = "this_is_not_good"
+          self.assertRaises(errors.TypeEnforcementError,
+                            common.GetUpdatedIPolicy,
+                            old_policy, invalid_policy, group_policy=isgroup)
+          ispec[par] = oldv
+    # This is to make sure that no two errors were present during the tests
+    common.GetUpdatedIPolicy(old_policy, invalid_policy,
+                             group_policy=isgroup)
+
+  def testInvalidKeys(self):
+    self._TestInvalidKeys(self._OLD_GROUP_POLICY, True)
+    self._TestInvalidKeys(self._OLD_CLUSTER_POLICY, False)
+
+  def testInvalidValues(self):
+    for par in (constants.IPOLICY_PARAMETERS |
+                frozenset([constants.IPOLICY_DTS])):
+      bad_policy = {
+        par: "invalid_value",
+        }
+      self.assertRaises(errors.OpPrereqError, common.GetUpdatedIPolicy, {},
+                        bad_policy, group_policy=True)
+
+
+class TestCopyLockList(unittest.TestCase):
+  def test(self):
+    self.assertEqual(instance_utils.CopyLockList([]), [])
+    self.assertEqual(instance_utils.CopyLockList(None), None)
+    self.assertEqual(instance_utils.CopyLockList(locking.ALL_SET),
+                     locking.ALL_SET)
+
+    names = ["foo", "bar"]
+    output = instance_utils.CopyLockList(names)
+    self.assertEqual(names, output)
+    self.assertNotEqual(id(names), id(output), msg="List was not copied")
+
+
+if __name__ == "__main__":
+  testutils.GanetiTestProgram()
diff --git a/test/py/cmdlib/group_unittest.py b/test/py/cmdlib/group_unittest.py
new file mode 100644 (file)
index 0000000..d654ca1
--- /dev/null
@@ -0,0 +1,457 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2008, 2011, 2012, 2013 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.
+
+
+"""Tests for LUGroup*
+
+"""
+
+import itertools
+
+from ganeti import constants
+from ganeti import opcodes
+from ganeti import query
+
+from testsupport import *
+
+import testutils
+
+
+class TestLUGroupAdd(CmdlibTestCase):
+  def testAddExistingGroup(self):
+    self.cfg.AddNewNodeGroup(name="existing_group")
+
+    op = opcodes.OpGroupAdd(group_name="existing_group")
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Desired group name 'existing_group' already exists")
+
+  def testAddNewGroup(self):
+    op = opcodes.OpGroupAdd(group_name="new_group")
+
+    self.ExecOpCode(op)
+
+    self.mcpu.assertLogIsEmpty()
+
+  def testAddNewGroupParams(self):
+    ndparams = {constants.ND_EXCLUSIVE_STORAGE: True}
+    hv_state = {constants.HT_FAKE: {constants.HVST_CPU_TOTAL: 8}}
+    disk_state = {
+      constants.DT_PLAIN: {
+        "mock_vg": {constants.DS_DISK_TOTAL: 10}
+      }
+    }
+    diskparams = {constants.DT_RBD: {constants.RBD_POOL: "mock_pool"}}
+    ipolicy = constants.IPOLICY_DEFAULTS
+    op = opcodes.OpGroupAdd(group_name="new_group",
+                            ndparams=ndparams,
+                            hv_state=hv_state,
+                            disk_state=disk_state,
+                            diskparams=diskparams,
+                            ipolicy=ipolicy)
+
+    self.ExecOpCode(op)
+
+    self.mcpu.assertLogIsEmpty()
+
+  def testAddNewGroupInvalidDiskparams(self):
+    diskparams = {constants.DT_RBD: {constants.LV_STRIPES: 1}}
+    op = opcodes.OpGroupAdd(group_name="new_group",
+                            diskparams=diskparams)
+
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Provided option keys not supported")
+
+  def testAddNewGroupInvalidIPolic(self):
+    ipolicy = {"invalid_key": "value"}
+    op = opcodes.OpGroupAdd(group_name="new_group",
+                            ipolicy=ipolicy)
+
+    self.ExecOpCodeExpectOpPrereqError(op, "Invalid keys in ipolicy")
+
+
+class TestLUGroupAssignNodes(CmdlibTestCase):
+  def __init__(self, methodName='runTest'):
+    super(TestLUGroupAssignNodes, self).__init__(methodName)
+
+    self.op = opcodes.OpGroupAssignNodes(group_name="default",
+                                         nodes=[])
+
+  def testAssignSingleNode(self):
+    node = self.cfg.AddNewNode()
+    op = self.CopyOpCode(self.op, nodes=[node.name])
+
+    self.ExecOpCode(op)
+
+    self.mcpu.assertLogIsEmpty()
+
+  def _BuildSplitInstanceSituation(self):
+    node = self.cfg.AddNewNode()
+    self.cfg.AddNewInstance(disk_template=constants.DT_DRBD8,
+                            primary_node=self.master,
+                            secondary_node=node)
+    group = self.cfg.AddNewNodeGroup()
+
+    return (node, group)
+
+  def testSplitInstanceNoForce(self):
+    (node, group) = self._BuildSplitInstanceSituation()
+    op = opcodes.OpGroupAssignNodes(group_name=group.name,
+                                    nodes=[node.name])
+
+    self.ExecOpCodeExpectOpExecError(
+      op, "instances get split by this change and --force was not given")
+
+  def testSplitInstanceForce(self):
+    (node, group) = self._BuildSplitInstanceSituation()
+
+    node2 = self.cfg.AddNewNode(group=group)
+    self.cfg.AddNewInstance(disk_template=constants.DT_DRBD8,
+                            primary_node=self.master,
+                            secondary_node=node2)
+
+    op = opcodes.OpGroupAssignNodes(group_name=group.name,
+                                    nodes=[node.name],
+                                    force=True)
+
+    self.ExecOpCode(op)
+
+    self.mcpu.assertLogContainsRegex("will split the following instances")
+    self.mcpu.assertLogContainsRegex(
+      "instances continue to be split across groups")
+
+
+  @withLockedLU
+  def testCheckAssignmentForSplitInstances(self, lu):
+    g1 = self.cfg.AddNewNodeGroup()
+    g2 = self.cfg.AddNewNodeGroup()
+    g3 = self.cfg.AddNewNodeGroup()
+
+    for (n, g) in [("n1a", g1), ("n1b", g1), ("n2a", g2), ("n2b", g2),
+                   ("n3a", g3), ("n3b", g3), ("n3c", g3)]:
+      self.cfg.AddNewNode(uuid=n, group=g.uuid)
+
+    for uuid, pnode, snode in [("inst1a", "n1a", "n1b"),
+                               ("inst1b", "n1b", "n1a"),
+                               ("inst2a", "n2a", "n2b"),
+                               ("inst3a", "n3a", None),
+                               ("inst3b", "n3b", "n1b"),
+                               ("inst3c", "n3b", "n2b")]:
+      dt = constants.DT_DISKLESS if snode is None else constants.DT_DRBD8
+      self.cfg.AddNewInstance(uuid=uuid,
+                              disk_template=dt,
+                              primary_node=pnode,
+                              secondary_node=snode)
+
+    # Test first with the existing state.
+    (new, prev) = lu.CheckAssignmentForSplitInstances(
+      [], self.cfg.GetAllNodesInfo(), self.cfg.GetAllInstancesInfo())
+
+    self.assertEqual([], new)
+    self.assertEqual(set(["inst3b", "inst3c"]), set(prev))
+
+    # And now some changes.
+    (new, prev) = lu.CheckAssignmentForSplitInstances(
+      [("n1b", g3.uuid)],
+      self.cfg.GetAllNodesInfo(),
+      self.cfg.GetAllInstancesInfo())
+
+    self.assertEqual(set(["inst1a", "inst1b"]), set(new))
+    self.assertEqual(set(["inst3c"]), set(prev))
+
+
+class TestLUGroupQuery(CmdlibTestCase):
+  def setUp(self):
+    super(TestLUGroupQuery, self).setUp()
+    self.fields = query._BuildGroupFields().keys()
+
+  def testInvalidGroupName(self):
+    op = opcodes.OpGroupQuery(names=["does_not_exist"],
+                              output_fields=self.fields)
+
+    self.ExecOpCodeExpectOpPrereqError(op, "Some groups do not exist")
+
+  def testQueryAllGroups(self):
+    op = opcodes.OpGroupQuery(output_fields=self.fields)
+
+    self.ExecOpCode(op)
+
+    self.mcpu.assertLogIsEmpty()
+
+  def testQueryGroupsByNameAndUuid(self):
+    group1 = self.cfg.AddNewNodeGroup()
+    group2 = self.cfg.AddNewNodeGroup()
+
+    node1 = self.cfg.AddNewNode(group=group1)
+    node2 = self.cfg.AddNewNode(group=group1)
+    self.cfg.AddNewInstance(disk_template=constants.DT_DRBD8,
+                            primary_node=node1,
+                            secondary_node=node2)
+    self.cfg.AddNewInstance(primary_node=node2)
+
+    op = opcodes.OpGroupQuery(names=[group1.name, group2.uuid],
+                              output_fields=self.fields)
+
+    self.ExecOpCode(op)
+
+    self.mcpu.assertLogIsEmpty()
+
+
+class TestLUGroupSetParams(CmdlibTestCase):
+  def testNoModifications(self):
+    op = opcodes.OpGroupSetParams(group_name=self.group.name)
+
+    self.ExecOpCodeExpectOpPrereqError(op,
+                                       "Please pass at least one modification")
+
+  def testModifyingAll(self):
+    ndparams = {constants.ND_EXCLUSIVE_STORAGE: True}
+    hv_state = {constants.HT_FAKE: {constants.HVST_CPU_TOTAL: 8}}
+    disk_state = {
+      constants.DT_PLAIN: {
+        "mock_vg": {constants.DS_DISK_TOTAL: 10}
+      }
+    }
+    diskparams = {constants.DT_RBD: {constants.RBD_POOL: "mock_pool"}}
+    ipolicy = {constants.IPOLICY_DTS: [constants.DT_DRBD8]}
+    op = opcodes.OpGroupSetParams(group_name=self.group.name,
+                                  ndparams=ndparams,
+                                  hv_state=hv_state,
+                                  disk_state=disk_state,
+                                  diskparams=diskparams,
+                                  ipolicy=ipolicy)
+
+    self.ExecOpCode(op)
+
+    self.mcpu.assertLogIsEmpty()
+
+  def testInvalidDiskparams(self):
+    diskparams = {constants.DT_RBD: {constants.LV_STRIPES: 1}}
+    op = opcodes.OpGroupSetParams(group_name=self.group.name,
+                                  diskparams=diskparams)
+
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Provided option keys not supported")
+
+  def testIPolicyNewViolations(self):
+    self.cfg.AddNewInstance(beparams={constants.BE_VCPUS: 8})
+
+    min_max = dict(constants.ISPECS_MINMAX_DEFAULTS)
+    min_max[constants.ISPECS_MAX].update({constants.ISPEC_CPU_COUNT: 2})
+    ipolicy = {constants.ISPECS_MINMAX: [min_max]}
+    op = opcodes.OpGroupSetParams(group_name=self.group.name,
+                                  ipolicy=ipolicy)
+
+    self.ExecOpCode(op)
+
+    self.assertLogContainsRegex(
+      "After the ipolicy change the following instances violate them")
+
+
+class TestLUGroupRemove(CmdlibTestCase):
+  def testNonEmptyGroup(self):
+    group = self.cfg.AddNewNodeGroup()
+    self.cfg.AddNewNode(group=group)
+    op = opcodes.OpGroupRemove(group_name=group.name)
+
+    self.ExecOpCodeExpectOpPrereqError(op, "Group .* not empty")
+
+  def testRemoveLastGroup(self):
+    self.master.group = "invalid_group"
+    op = opcodes.OpGroupRemove(group_name=self.group.name)
+
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Group .* is the only group, cannot be removed")
+
+  def testRemoveGroup(self):
+    group = self.cfg.AddNewNodeGroup()
+    op = opcodes.OpGroupRemove(group_name=group.name)
+
+    self.ExecOpCode(op)
+
+    self.mcpu.assertLogIsEmpty()
+
+
+class TestLUGroupRename(CmdlibTestCase):
+  def testRenameToExistingName(self):
+    group = self.cfg.AddNewNodeGroup()
+    op = opcodes.OpGroupRename(group_name=group.name,
+                               new_name=self.group.name)
+
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Desired new name .* clashes with existing node group")
+
+  def testRename(self):
+    group = self.cfg.AddNewNodeGroup()
+    op = opcodes.OpGroupRename(group_name=group.name,
+                               new_name="new_group_name")
+
+    self.ExecOpCode(op)
+
+    self.mcpu.assertLogIsEmpty()
+
+
+class TestLUGroupEvacuate(CmdlibTestCase):
+  def testEvacuateEmptyGroup(self):
+    group = self.cfg.AddNewNodeGroup()
+    op = opcodes.OpGroupEvacuate(group_name=group.name)
+
+    self.iallocator_cls.return_value.result = ([], [], [])
+
+    self.ExecOpCode(op)
+
+  def testEvacuateOnlyGroup(self):
+    op = opcodes.OpGroupEvacuate(group_name=self.group.name)
+
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "There are no possible target groups")
+
+  def testEvacuateWithTargetGroups(self):
+    group = self.cfg.AddNewNodeGroup()
+    self.cfg.AddNewNode(group=group)
+    self.cfg.AddNewNode(group=group)
+
+    target_group1 = self.cfg.AddNewNodeGroup()
+    target_group2 = self.cfg.AddNewNodeGroup()
+    op = opcodes.OpGroupEvacuate(group_name=group.name,
+                                 target_groups=[target_group1.name,
+                                                target_group2.name])
+
+    self.iallocator_cls.return_value.result = ([], [], [])
+
+    self.ExecOpCode(op)
+
+  def testFailingIAllocator(self):
+    group = self.cfg.AddNewNodeGroup()
+    op = opcodes.OpGroupEvacuate(group_name=group.name)
+
+    self.iallocator_cls.return_value.success = False
+
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Can't compute group evacuation using iallocator")
+
+
+class TestLUGroupVerifyDisks(CmdlibTestCase):
+  def testNoInstances(self):
+    op = opcodes.OpGroupVerifyDisks(group_name=self.group.name)
+
+    self.ExecOpCode(op)
+
+    self.mcpu.assertLogIsEmpty()
+
+  def testOfflineAndFailingNode(self):
+    node = self.cfg.AddNewNode(offline=True)
+    self.cfg.AddNewInstance(primary_node=node,
+                            admin_state=constants.ADMINST_UP)
+    self.cfg.AddNewInstance(admin_state=constants.ADMINST_UP)
+    self.rpc.call_lv_list.return_value = \
+      self.RpcResultsBuilder() \
+        .AddFailedNode(self.master) \
+        .AddOfflineNode(node) \
+        .Build()
+
+    op = opcodes.OpGroupVerifyDisks(group_name=self.group.name)
+
+    (nerrors, offline, missing) = self.ExecOpCode(op)
+
+    self.assertEqual(1, len(nerrors))
+    self.assertEqual(0, len(offline))
+    self.assertEqual(2, len(missing))
+
+  def testValidNodeResult(self):
+    self.cfg.AddNewInstance(
+      disks=[self.cfg.CreateDisk(dev_type=constants.DT_PLAIN),
+             self.cfg.CreateDisk(dev_type=constants.DT_PLAIN)
+             ],
+      admin_state=constants.ADMINST_UP)
+    self.rpc.call_lv_list.return_value = \
+      self.RpcResultsBuilder() \
+        .AddSuccessfulNode(self.master, {
+          "mockvg/mock_disk_1": (None, None, True),
+          "mockvg/mock_disk_2": (None, None, False)
+        }) \
+        .Build()
+
+    op = opcodes.OpGroupVerifyDisks(group_name=self.group.name)
+
+    (nerrors, offline, missing) = self.ExecOpCode(op)
+
+    self.assertEqual(0, len(nerrors))
+    self.assertEqual(1, len(offline))
+    self.assertEqual(0, len(missing))
+
+  def testDrbdDisk(self):
+    node1 = self.cfg.AddNewNode()
+    node2 = self.cfg.AddNewNode()
+    node3 = self.cfg.AddNewNode()
+    node4 = self.cfg.AddNewNode()
+
+    valid_disk = self.cfg.CreateDisk(dev_type=constants.DT_DRBD8,
+                                     primary_node=node1,
+                                     secondary_node=node2)
+    broken_disk = self.cfg.CreateDisk(dev_type=constants.DT_DRBD8,
+                                      primary_node=node1,
+                                      secondary_node=node2)
+    failing_node_disk = self.cfg.CreateDisk(dev_type=constants.DT_DRBD8,
+                                            primary_node=node3,
+                                            secondary_node=node4)
+
+    self.cfg.AddNewInstance(disks=[valid_disk, broken_disk],
+                            primary_node=node1,
+                            admin_state=constants.ADMINST_UP)
+    self.cfg.AddNewInstance(disks=[failing_node_disk],
+                            primary_node=node3,
+                            admin_state=constants.ADMINST_UP)
+
+    lv_list_result = dict(("/".join(disk.logical_id), (None, None, True))
+                          for disk in itertools.chain(valid_disk.children,
+                                                      broken_disk.children))
+    self.rpc.call_lv_list.return_value = \
+      self.RpcResultsBuilder() \
+        .AddSuccessfulNode(node1, lv_list_result) \
+        .AddSuccessfulNode(node2, lv_list_result) \
+        .AddFailedNode(node3) \
+        .AddFailedNode(node4) \
+        .Build()
+
+    def GetDrbdNeedsActivationResult(node_uuid, *_):
+      if node_uuid == node1.uuid:
+        return self.RpcResultsBuilder() \
+                 .CreateSuccessfulNodeResult(node1, [])
+      elif node_uuid == node2.uuid:
+        return self.RpcResultsBuilder() \
+                 .CreateSuccessfulNodeResult(node2, [broken_disk.uuid])
+      elif node_uuid == node3.uuid or node_uuid == node4.uuid:
+        return self.RpcResultsBuilder() \
+                 .CreateFailedNodeResult(node_uuid)
+
+    self.rpc.call_drbd_needs_activation.side_effect = \
+      GetDrbdNeedsActivationResult
+
+    op = opcodes.OpGroupVerifyDisks(group_name=self.group.name)
+
+    (nerrors, offline, missing) = self.ExecOpCode(op)
+
+    self.assertEqual(2, len(nerrors))
+    self.assertEqual(1, len(offline))
+    self.assertEqual(1, len(missing))
+
+
+if __name__ == "__main__":
+  testutils.GanetiTestProgram()
diff --git a/test/py/cmdlib/instance_migration_unittest.py b/test/py/cmdlib/instance_migration_unittest.py
new file mode 100644 (file)
index 0000000..4de9ae1
--- /dev/null
@@ -0,0 +1,162 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2013 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.
+
+
+"""Tests for LUInstanceFailover and LUInstanceMigrate
+
+"""
+
+from ganeti import constants
+from ganeti import objects
+from ganeti import opcodes
+
+from testsupport import *
+
+import testutils
+
+
+class TestLUInstanceMigrate(CmdlibTestCase):
+  def setUp(self):
+    super(TestLUInstanceMigrate, self).setUp()
+
+    self.snode = self.cfg.AddNewNode()
+
+    hv_info = ("bootid",
+               [{
+                 "type": constants.ST_LVM_VG,
+                 "storage_free": 10000
+               }],
+               ({"memory_free": 10000}, ))
+    self.rpc.call_node_info.return_value = \
+      self.RpcResultsBuilder() \
+        .AddSuccessfulNode(self.master, hv_info) \
+        .AddSuccessfulNode(self.snode, hv_info) \
+        .Build()
+
+    self.rpc.call_blockdev_find.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, objects.BlockDevStatus())
+
+    self.rpc.call_migration_info.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, True)
+    self.rpc.call_accept_instance.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.snode, True)
+    self.rpc.call_instance_migrate.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, True)
+    self.rpc.call_instance_get_migration_status.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, objects.MigrationStatus())
+    self.rpc.call_instance_finalize_migration_dst.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.snode, True)
+    self.rpc.call_instance_finalize_migration_src.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, True)
+
+    self.inst = self.cfg.AddNewInstance(disk_template=constants.DT_DRBD8,
+                                        admin_state=constants.ADMINST_UP,
+                                        secondary_node=self.snode)
+    self.op = opcodes.OpInstanceMigrate(instance_name=self.inst.name)
+
+  def testPlainDisk(self):
+    inst = self.cfg.AddNewInstance(disk_template=constants.DT_PLAIN)
+    op = self.CopyOpCode(self.op,
+                         instance_name=inst.name)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Instance's disk layout 'plain' does not allow migrations")
+
+  def testMigrationToWrongNode(self):
+    node = self.cfg.AddNewNode()
+    op = self.CopyOpCode(self.op,
+                         target_node=node.name)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Instances with disk template drbd cannot be migrated to"
+          " arbitrary nodes")
+
+  def testMigration(self):
+    op = self.CopyOpCode(self.op)
+    self.ExecOpCode(op)
+
+
+class TestLUInstanceFailover(CmdlibTestCase):
+  def setUp(self):
+    super(TestLUInstanceFailover, self).setUp()
+
+    self.snode = self.cfg.AddNewNode()
+
+    hv_info = ("bootid",
+               [{
+                 "type": constants.ST_LVM_VG,
+                 "storage_free": 10000
+               }],
+               ({"memory_free": 10000}, ))
+    self.rpc.call_node_info.return_value = \
+      self.RpcResultsBuilder() \
+        .AddSuccessfulNode(self.master, hv_info) \
+        .AddSuccessfulNode(self.snode, hv_info) \
+        .Build()
+
+    self.rpc.call_blockdev_find.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, objects.BlockDevStatus())
+
+    self.rpc.call_instance_shutdown.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, True)
+    self.rpc.call_blockdev_shutdown.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, True)
+    self.rpc.call_blockdev_assemble.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.snode, ("/dev/mock", "/var/mock"))
+    self.rpc.call_instance_start.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.snode, True)
+
+    self.inst = self.cfg.AddNewInstance(disk_template=constants.DT_DRBD8,
+                                        admin_state=constants.ADMINST_UP,
+                                        secondary_node=self.snode)
+    self.op = opcodes.OpInstanceFailover(instance_name=self.inst.name)
+
+  def testPlainDisk(self):
+    inst = self.cfg.AddNewInstance(disk_template=constants.DT_PLAIN)
+    op = self.CopyOpCode(self.op,
+                         instance_name=inst.name)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Instance's disk layout 'plain' does not allow failovers")
+
+  def testMigrationToWrongNode(self):
+    node = self.cfg.AddNewNode()
+    op = self.CopyOpCode(self.op,
+                         target_node=node.name)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Instances with disk template drbd cannot be failed over to"
+          " arbitrary nodes")
+
+  def testMigration(self):
+    op = self.CopyOpCode(self.op)
+    self.ExecOpCode(op)
+
+
+if __name__ == "__main__":
+  testutils.GanetiTestProgram()
diff --git a/test/py/cmdlib/instance_query_unittest.py b/test/py/cmdlib/instance_query_unittest.py
new file mode 100644 (file)
index 0000000..68b651d
--- /dev/null
@@ -0,0 +1,68 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2013 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.
+
+
+"""Tests for LUInstanceFailover and LUInstanceMigrate
+
+"""
+
+from ganeti import opcodes
+from ganeti import query
+
+from testsupport import *
+
+import testutils
+
+
+class TestLUInstanceQuery(CmdlibTestCase):
+  def setUp(self):
+    super(TestLUInstanceQuery, self).setUp()
+
+    self.inst = self.cfg.AddNewInstance()
+    self.fields = query._BuildInstanceFields().keys()
+
+  def testInvalidInstance(self):
+    op = opcodes.OpInstanceQuery(names=["does_not_exist"],
+                                 output_fields=self.fields)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Instance 'does_not_exist' not known")
+
+  def testQueryInstance(self):
+    op = opcodes.OpInstanceQuery(output_fields=self.fields)
+    self.ExecOpCode(op)
+
+
+class TestLUInstanceQueryData(CmdlibTestCase):
+  def setUp(self):
+    super(TestLUInstanceQueryData, self).setUp()
+
+    self.inst = self.cfg.AddNewInstance()
+
+  def testQueryInstanceData(self):
+    op = opcodes.OpInstanceQueryData()
+    self.ExecOpCode(op)
+
+  def testQueryStaticInstanceData(self):
+    op = opcodes.OpInstanceQueryData(static=True)
+    self.ExecOpCode(op)
+
+
+if __name__ == "__main__":
+  testutils.GanetiTestProgram()
similarity index 97%
rename from test/py/ganeti.cmdlib.instance_storage_unittest.py
rename to test/py/cmdlib/instance_storage_unittest.py
index 7874b5d..5e1db45 100755 (executable)
@@ -73,7 +73,8 @@ class TestCheckNodesFreeDiskOnVG(unittest.TestCase):
   def testPerformNodeInfoCall(self):
     expected_hv_arg = [(self.hvname, self.hvparams)]
     expected_storage_arg = {self.node_uuid:
-        [(constants.ST_LVM_VG, self.vg, [self.es])]}
+        [(constants.ST_LVM_VG, self.vg, [self.es]),
+         (constants.ST_LVM_PV, self.vg, [self.es])]}
     instance_storage._PerformNodeInfoCall(self.lu, self.node_uuids, self.vg)
     self.lu.rpc.call_node_info.assert_called_with(
         self.node_uuids, expected_storage_arg, expected_hv_arg)
diff --git a/test/py/cmdlib/instance_unittest.py b/test/py/cmdlib/instance_unittest.py
new file mode 100644 (file)
index 0000000..57371fd
--- /dev/null
@@ -0,0 +1,2327 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2008, 2011, 2012, 2013 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.
+
+
+"""Tests for LUInstance*
+
+"""
+
+import copy
+import itertools
+import unittest
+import mock
+import operator
+
+from ganeti import compat
+from ganeti import constants
+from ganeti import errors
+from ganeti import ht
+from ganeti import opcodes
+from ganeti import objects
+from ganeti import rpc
+from ganeti import utils
+from ganeti.cmdlib import instance
+
+from cmdlib.cmdlib_unittest import _StubComputeIPolicySpecViolation, _FakeLU
+
+from testsupport import *
+
+import testutils
+
+
+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,
+                                            constants.DT_PLAIN)
+    ret = instance._ComputeIPolicyInstanceSpecViolation(NotImplemented, ispec,
+                                                        constants.DT_PLAIN,
+                                                        _compute_fn=stub)
+    self.assertEqual(ret, [])
+
+
+class TestLUInstanceCreate(CmdlibTestCase):
+  def setUp(self):
+    super(TestLUInstanceCreate, self).setUp()
+
+    self.net = self.cfg.AddNewNetwork()
+    self.cfg.ConnectNetworkToGroup(self.net, self.group)
+
+    self.node1 = self.cfg.AddNewNode()
+    self.node2 = self.cfg.AddNewNode()
+
+    self.rpc.call_os_get.side_effect = \
+      lambda node, _: self.RpcResultsBuilder() \
+                        .CreateSuccessfulNodeResult(node, self.os)
+
+    hv_info = ("bootid",
+               [{
+                 "type": constants.ST_LVM_VG,
+                 "storage_free": 10000
+               }],
+               ({"memory_free": 10000}, ))
+    self.rpc.call_node_info.return_value = \
+      self.RpcResultsBuilder() \
+        .AddSuccessfulNode(self.master, hv_info) \
+        .AddSuccessfulNode(self.node1, hv_info) \
+        .AddSuccessfulNode(self.node2, hv_info) \
+        .Build()
+
+    self.rpc.call_blockdev_getmirrorstatus.side_effect = \
+      lambda node, _: self.RpcResultsBuilder() \
+                        .CreateSuccessfulNodeResult(node, [])
+
+    self.iallocator_cls.return_value.result = [self.node1.name, self.node2.name]
+
+    self.diskless_op = opcodes.OpInstanceCreate(
+      instance_name="diskless.test.com",
+      pnode=self.master.name,
+      disk_template=constants.DT_DISKLESS,
+      mode=constants.INSTANCE_CREATE,
+      nics=[{}],
+      disks=[],
+      os_type=self.os_name_variant)
+
+    self.plain_op = opcodes.OpInstanceCreate(
+      instance_name="plain.test.com",
+      pnode=self.master.name,
+      disk_template=constants.DT_PLAIN,
+      mode=constants.INSTANCE_CREATE,
+      nics=[{}],
+      disks=[{
+        constants.IDISK_SIZE: 1024
+      }],
+      os_type=self.os_name_variant)
+
+    self.block_op = opcodes.OpInstanceCreate(
+      instance_name="block.test.com",
+      pnode=self.master.name,
+      disk_template=constants.DT_BLOCK,
+      mode=constants.INSTANCE_CREATE,
+      nics=[{}],
+      disks=[{
+        constants.IDISK_SIZE: 1024,
+        constants.IDISK_ADOPT: "/dev/disk/block0"
+      }],
+      os_type=self.os_name_variant)
+
+    self.drbd_op = opcodes.OpInstanceCreate(
+      instance_name="drbd.test.com",
+      pnode=self.node1.name,
+      snode=self.node2.name,
+      disk_template=constants.DT_DRBD8,
+      mode=constants.INSTANCE_CREATE,
+      nics=[{}],
+      disks=[{
+        constants.IDISK_SIZE: 1024
+      }],
+      os_type=self.os_name_variant)
+
+    self.file_op = opcodes.OpInstanceCreate(
+      instance_name="file.test.com",
+      pnode=self.node1.name,
+      disk_template=constants.DT_FILE,
+      mode=constants.INSTANCE_CREATE,
+      nics=[{}],
+      disks=[{
+        constants.IDISK_SIZE: 1024
+      }],
+      os_type=self.os_name_variant)
+
+  def testSimpleCreate(self):
+    op = self.CopyOpCode(self.diskless_op)
+    self.ExecOpCode(op)
+
+  def testStrangeHostnameResolve(self):
+    op = self.CopyOpCode(self.diskless_op)
+    self.netutils_mod.GetHostname.return_value = \
+      HostnameMock("random.host.example.com", "203.0.113.1")
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Resolved hostname .* does not look the same as given hostname")
+
+  def testOpportunisticLockingNoIAllocator(self):
+    op = self.CopyOpCode(self.diskless_op,
+                         opportunistic_locking=True,
+                         iallocator=None)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Opportunistic locking is only available in combination with an"
+          " instance allocator")
+
+  def testNicWithNetAndMode(self):
+    op = self.CopyOpCode(self.diskless_op,
+                         nics=[{
+                           constants.INIC_NETWORK: self.net.name,
+                           constants.INIC_MODE: constants.NIC_MODE_BRIDGED
+                         }])
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "If network is given, no mode or link is allowed to be passed")
+
+  def testVlanWithWrongMode(self):
+    op = self.CopyOpCode(self.diskless_op,
+                         nics=[{
+                           constants.INIC_VLAN: ":1",
+                           constants.INIC_MODE: constants.NIC_MODE_BRIDGED
+                         }])
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "VLAN is given, but network mode is not openvswitch")
+
+  def testAutoIpNoNameCheck(self):
+    op = self.CopyOpCode(self.diskless_op,
+                         nics=[{
+                           constants.INIC_IP: constants.VALUE_AUTO
+                         }],
+                         ip_check=False,
+                         name_check=False)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "IP address set to auto but name checks have been skipped")
+
+  def testAutoIp(self):
+    op = self.CopyOpCode(self.diskless_op,
+                         nics=[{
+                           constants.INIC_IP: constants.VALUE_AUTO
+                         }])
+    self.ExecOpCode(op)
+
+  def testPoolIpNoNetwork(self):
+    op = self.CopyOpCode(self.diskless_op,
+                         nics=[{
+                           constants.INIC_IP: constants.NIC_IP_POOL
+                         }])
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "if ip=pool, parameter network must be passed too")
+
+  def testValidIp(self):
+    op = self.CopyOpCode(self.diskless_op,
+                         nics=[{
+                           constants.INIC_IP: "203.0.113.1"
+                         }])
+    self.ExecOpCode(op)
+
+  def testRoutedNoIp(self):
+    op = self.CopyOpCode(self.diskless_op,
+                         nics=[{
+                           constants.INIC_MODE: constants.NIC_MODE_ROUTED
+                         }])
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Routed nic mode requires an ip address")
+
+  def testValicMac(self):
+    op = self.CopyOpCode(self.diskless_op,
+                         nics=[{
+                           constants.INIC_MAC: "f0:df:f4:a3:d1:cf"
+                         }])
+    self.ExecOpCode(op)
+
+  def testValidNicParams(self):
+    op = self.CopyOpCode(self.diskless_op,
+                         nics=[{
+                           constants.INIC_MODE: constants.NIC_MODE_BRIDGED,
+                           constants.INIC_LINK: "br_mock"
+                         }])
+    self.ExecOpCode(op)
+
+  def testValidNicParamsOpenVSwitch(self):
+    op = self.CopyOpCode(self.diskless_op,
+                         nics=[{
+                           constants.INIC_MODE: constants.NIC_MODE_OVS,
+                           constants.INIC_VLAN: "1"
+                         }])
+    self.ExecOpCode(op)
+
+  def testNicNoneName(self):
+    op = self.CopyOpCode(self.diskless_op,
+                         nics=[{
+                           constants.INIC_NAME: constants.VALUE_NONE
+                         }])
+    self.ExecOpCode(op)
+
+  def testConflictingIP(self):
+    op = self.CopyOpCode(self.diskless_op,
+                         nics=[{
+                           constants.INIC_IP: self.net.gateway[:-1] + "2"
+                         }])
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "The requested IP address .* belongs to network .*, but the target"
+          " NIC does not.")
+
+  def testVLanFormat(self):
+    for vlan in [".pinky", ":bunny", ":1:pinky", "bunny"]:
+      self.ResetMocks()
+      op = self.CopyOpCode(self.diskless_op,
+                           nics=[{
+                             constants.INIC_VLAN: vlan
+                           }])
+      self.ExecOpCodeExpectOpPrereqError(
+        op, "Specified VLAN parameter is invalid")
+
+  def testPoolIp(self):
+    op = self.CopyOpCode(self.diskless_op,
+                         nics=[{
+                           constants.INIC_IP: constants.NIC_IP_POOL,
+                           constants.INIC_NETWORK: self.net.name
+                         }])
+    self.ExecOpCode(op)
+
+  def testPoolIpUnconnectedNetwork(self):
+    net = self.cfg.AddNewNetwork()
+    op = self.CopyOpCode(self.diskless_op,
+                         nics=[{
+                           constants.INIC_IP: constants.NIC_IP_POOL,
+                           constants.INIC_NETWORK: net.name
+                         }])
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "No netparams found for network .*.")
+
+  def testIpNotInNetwork(self):
+    op = self.CopyOpCode(self.diskless_op,
+                         nics=[{
+                           constants.INIC_IP: "203.0.113.1",
+                           constants.INIC_NETWORK: self.net.name
+                         }])
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "IP address .* already in use or does not belong to network .*")
+
+  def testMixAdoptAndNotAdopt(self):
+    op = self.CopyOpCode(self.diskless_op,
+                         disk_template=constants.DT_PLAIN,
+                         disks=[{
+                           constants.IDISK_ADOPT: "lv1"
+                         }, {}])
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Either all disks are adopted or none is")
+
+  def testMustAdoptWithoutAdopt(self):
+    op = self.CopyOpCode(self.diskless_op,
+                         disk_template=constants.DT_BLOCK,
+                         disks=[{}])
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Disk template blockdev requires disk adoption, but no 'adopt'"
+          " parameter given")
+
+  def testDontAdoptWithAdopt(self):
+    op = self.CopyOpCode(self.diskless_op,
+                         disk_template=constants.DT_DRBD8,
+                         disks=[{
+                           constants.IDISK_ADOPT: "lv1"
+                         }])
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Disk adoption is not supported for the 'drbd' disk template")
+
+  def testAdoptWithIAllocator(self):
+    op = self.CopyOpCode(self.diskless_op,
+                         disk_template=constants.DT_PLAIN,
+                         disks=[{
+                           constants.IDISK_ADOPT: "lv1"
+                         }],
+                         iallocator="mock")
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Disk adoption not allowed with an iallocator script")
+
+  def testAdoptWithImport(self):
+    op = self.CopyOpCode(self.diskless_op,
+                         disk_template=constants.DT_PLAIN,
+                         disks=[{
+                           constants.IDISK_ADOPT: "lv1"
+                         }],
+                         mode=constants.INSTANCE_IMPORT)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Disk adoption not allowed for instance import")
+
+  def testArgumentCombinations(self):
+    op = self.CopyOpCode(self.diskless_op,
+                         # start flag will be flipped
+                         no_install=True,
+                         start=True,
+                         # no allowed combination
+                         ip_check=True,
+                         name_check=False)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Cannot do IP address check without a name check")
+
+  def testInvalidFileDriver(self):
+    op = self.CopyOpCode(self.diskless_op,
+                         file_driver="invalid_file_driver")
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Parameter 'OP_INSTANCE_CREATE.file_driver' fails validation")
+
+  def testMissingSecondaryNode(self):
+    op = self.CopyOpCode(self.diskless_op,
+                         pnode=self.master.name,
+                         disk_template=constants.DT_DRBD8)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "The networked disk templates need a mirror node")
+
+  def testIgnoredSecondaryNode(self):
+    op = self.CopyOpCode(self.diskless_op,
+                         pnode=self.master.name,
+                         snode=self.node1.name,
+                         disk_template=constants.DT_PLAIN)
+    try:
+      self.ExecOpCode(op)
+    except Exception:
+      pass
+    self.mcpu.assertLogContainsRegex(
+      "Secondary node will be ignored on non-mirrored disk template")
+
+  def testMissingOsType(self):
+    op = self.CopyOpCode(self.diskless_op,
+                         os_type=self.REMOVE)
+    self.ExecOpCodeExpectOpPrereqError(op, "No guest OS specified")
+
+  def testBlacklistedOs(self):
+    self.cluster.blacklisted_os = [self.os_name_variant]
+    op = self.CopyOpCode(self.diskless_op)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Guest OS .* is not allowed for installation")
+
+  def testMissingDiskTemplate(self):
+    self.cluster.enabled_disk_templates = [constants.DT_DISKLESS]
+    op = self.CopyOpCode(self.diskless_op,
+                         disk_template=self.REMOVE)
+    self.ExecOpCode(op)
+
+  def testExistingInstance(self):
+    inst = self.cfg.AddNewInstance()
+    op = self.CopyOpCode(self.diskless_op,
+                         instance_name=inst.name)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Instance .* is already in the cluster")
+
+  def testPlainInstance(self):
+    op = self.CopyOpCode(self.plain_op)
+    self.ExecOpCode(op)
+
+  def testPlainIAllocator(self):
+    op = self.CopyOpCode(self.plain_op,
+                         pnode=self.REMOVE,
+                         iallocator="mock")
+    self.ExecOpCode(op)
+
+  def testIAllocatorOpportunisticLocking(self):
+    op = self.CopyOpCode(self.plain_op,
+                         pnode=self.REMOVE,
+                         iallocator="mock",
+                         opportunistic_locking=True)
+    self.ExecOpCode(op)
+
+  def testFailingIAllocator(self):
+    self.iallocator_cls.return_value.success = False
+    op = self.CopyOpCode(self.plain_op,
+                         pnode=self.REMOVE,
+                         iallocator="mock")
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Can't compute nodes using iallocator")
+
+  def testDrbdInstance(self):
+    op = self.CopyOpCode(self.drbd_op)
+    self.ExecOpCode(op)
+
+  def testDrbdIAllocator(self):
+    op = self.CopyOpCode(self.drbd_op,
+                         pnode=self.REMOVE,
+                         snode=self.REMOVE,
+                         iallocator="mock")
+    self.ExecOpCode(op)
+
+  def testFileInstance(self):
+    op = self.CopyOpCode(self.file_op)
+    self.ExecOpCode(op)
+
+  def testFileInstanceNoClusterStorage(self):
+    self.cluster.file_storage_dir = None
+    op = self.CopyOpCode(self.file_op)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Cluster file storage dir not defined")
+
+  def testFileInstanceAdditionalPath(self):
+    op = self.CopyOpCode(self.file_op,
+                         file_storage_dir="mock_dir")
+    self.ExecOpCode(op)
+
+  def testIdentifyDefaults(self):
+    op = self.CopyOpCode(self.plain_op,
+                         hvparams={
+                           constants.HV_BOOT_ORDER: "cd"
+                         },
+                         beparams=constants.BEC_DEFAULTS.copy(),
+                         nics=[{
+                           constants.NIC_MODE: constants.NIC_MODE_BRIDGED
+                         }],
+                         osparams={
+                           self.os_name_variant: {}
+                         },
+                         identify_defaults=True)
+    self.ExecOpCode(op)
+
+    inst = self.cfg.GetAllInstancesInfo().values()[0]
+    self.assertEqual(0, len(inst.hvparams))
+    self.assertEqual(0, len(inst.beparams))
+    assert self.os_name_variant not in inst.osparams or \
+            len(inst.osparams[self.os_name_variant]) == 0
+
+  def testOfflineNode(self):
+    self.node1.offline = True
+    op = self.CopyOpCode(self.diskless_op,
+                         pnode=self.node1.name)
+    self.ExecOpCodeExpectOpPrereqError(op, "Cannot use offline primary node")
+
+  def testDrainedNode(self):
+    self.node1.drained = True
+    op = self.CopyOpCode(self.diskless_op,
+                         pnode=self.node1.name)
+    self.ExecOpCodeExpectOpPrereqError(op, "Cannot use drained primary node")
+
+  def testNonVmCapableNode(self):
+    self.node1.vm_capable = False
+    op = self.CopyOpCode(self.diskless_op,
+                         pnode=self.node1.name)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Cannot use non-vm_capable primary node")
+
+  def testNonEnabledHypervisor(self):
+    self.cluster.enabled_hypervisors = [constants.HT_XEN_HVM]
+    op = self.CopyOpCode(self.diskless_op,
+                         hypervisor=constants.HT_FAKE)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Selected hypervisor .* not enabled in the cluster")
+
+  def testAddTag(self):
+    op = self.CopyOpCode(self.diskless_op,
+                         tags=["tag"])
+    self.ExecOpCode(op)
+
+  def testInvalidTag(self):
+    op = self.CopyOpCode(self.diskless_op,
+                         tags=["too_long" * 20])
+    self.ExecOpCodeExpectException(op, errors.TagError, "Tag too long")
+
+  def testPingableInstanceName(self):
+    self.netutils_mod.TcpPing.return_value = True
+    op = self.CopyOpCode(self.diskless_op)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "IP .* of instance diskless.test.com already in use")
+
+  def testPrimaryIsSecondaryNode(self):
+    op = self.CopyOpCode(self.drbd_op,
+                         snode=self.drbd_op.pnode)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "The secondary node cannot be the primary node")
+
+  def testPrimarySecondaryDifferentNodeGroups(self):
+    group = self.cfg.AddNewNodeGroup()
+    self.node2.group = group.uuid
+    op = self.CopyOpCode(self.drbd_op)
+    self.ExecOpCode(op)
+    self.mcpu.assertLogContainsRegex(
+      "The primary and secondary nodes are in two different node groups")
+
+  def testExclusiveStorageUnsupportedDiskTemplate(self):
+    self.node1.ndparams[constants.ND_EXCLUSIVE_STORAGE] = True
+    op = self.CopyOpCode(self.drbd_op)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Disk template drbd not supported with exclusive storage")
+
+  def testAdoptPlain(self):
+    self.rpc.call_lv_list.return_value = \
+      self.RpcResultsBuilder() \
+        .AddSuccessfulNode(self.master, {
+          "xenvg/mock_disk_1": (10000, None, False)
+        }) \
+        .Build()
+    op = self.CopyOpCode(self.plain_op)
+    op.disks[0].update({constants.IDISK_ADOPT: "mock_disk_1"})
+    self.ExecOpCode(op)
+
+  def testAdoptPlainMissingLv(self):
+    self.rpc.call_lv_list.return_value = \
+      self.RpcResultsBuilder() \
+        .AddSuccessfulNode(self.master, {}) \
+        .Build()
+    op = self.CopyOpCode(self.plain_op)
+    op.disks[0].update({constants.IDISK_ADOPT: "mock_disk_1"})
+    self.ExecOpCodeExpectOpPrereqError(op, "Missing logical volume")
+
+  def testAdoptPlainOnlineLv(self):
+    self.rpc.call_lv_list.return_value = \
+      self.RpcResultsBuilder() \
+        .AddSuccessfulNode(self.master, {
+          "xenvg/mock_disk_1": (10000, None, True)
+        }) \
+        .Build()
+    op = self.CopyOpCode(self.plain_op)
+    op.disks[0].update({constants.IDISK_ADOPT: "mock_disk_1"})
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Online logical volumes found, cannot adopt")
+
+  def testAdoptBlock(self):
+    self.rpc.call_bdev_sizes.return_value = \
+      self.RpcResultsBuilder() \
+        .AddSuccessfulNode(self.master, {
+          "/dev/disk/block0": 10000
+        }) \
+        .Build()
+    op = self.CopyOpCode(self.block_op)
+    self.ExecOpCode(op)
+
+  def testAdoptBlockDuplicateNames(self):
+    op = self.CopyOpCode(self.block_op,
+                         disks=[{
+                           constants.IDISK_SIZE: 0,
+                           constants.IDISK_ADOPT: "/dev/disk/block0"
+                         }, {
+                           constants.IDISK_SIZE: 0,
+                           constants.IDISK_ADOPT: "/dev/disk/block0"
+                         }])
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Duplicate disk names given for adoption")
+
+  def testAdoptBlockInvalidNames(self):
+    op = self.CopyOpCode(self.block_op,
+                         disks=[{
+                           constants.IDISK_SIZE: 0,
+                           constants.IDISK_ADOPT: "/invalid/block0"
+                         }])
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Device node.* lie outside .* and cannot be adopted")
+
+  def testAdoptBlockMissingDisk(self):
+    self.rpc.call_bdev_sizes.return_value = \
+      self.RpcResultsBuilder() \
+        .AddSuccessfulNode(self.master, {}) \
+        .Build()
+    op = self.CopyOpCode(self.block_op)
+    self.ExecOpCodeExpectOpPrereqError(op, "Missing block device")
+
+  def testNoWaitForSyncDrbd(self):
+    op = self.CopyOpCode(self.drbd_op,
+                         wait_for_sync=False)
+    self.ExecOpCode(op)
+
+  def testNoWaitForSyncPlain(self):
+    op = self.CopyOpCode(self.plain_op,
+                         wait_for_sync=False)
+    self.ExecOpCode(op)
+
+  def testImportPlainFromGivenSrcNode(self):
+    exp_info = """
+[export]
+version=0
+os=mock_os
+[instance]
+name=old_name.example.com
+"""
+
+    self.rpc.call_export_info.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, exp_info)
+    op = self.CopyOpCode(self.plain_op,
+                         mode=constants.INSTANCE_IMPORT,
+                         src_node=self.master.name)
+    self.ExecOpCode(op)
+
+  def testImportPlainWithoutSrcNodeNotFound(self):
+    op = self.CopyOpCode(self.plain_op,
+                         mode=constants.INSTANCE_IMPORT)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "No export found for relative path")
+
+  def testImportPlainWithoutSrcNode(self):
+    exp_info = """
+[export]
+version=0
+os=mock_os
+[instance]
+name=old_name.example.com
+"""
+
+    self.rpc.call_export_list.return_value = \
+      self.RpcResultsBuilder() \
+        .AddSuccessfulNode(self.master, {"mock_path": {}}) \
+        .Build()
+    self.rpc.call_export_info.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, exp_info)
+
+    op = self.CopyOpCode(self.plain_op,
+                         mode=constants.INSTANCE_IMPORT,
+                         src_path="mock_path")
+    self.ExecOpCode(op)
+
+  def testImportPlainCorruptExportInfo(self):
+    exp_info = ""
+    self.rpc.call_export_info.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, exp_info)
+    op = self.CopyOpCode(self.plain_op,
+                         mode=constants.INSTANCE_IMPORT,
+                         src_node=self.master.name)
+    self.ExecOpCodeExpectException(op, errors.ProgrammerError,
+                                   "Corrupted export config")
+
+  def testImportPlainWrongExportInfoVersion(self):
+    exp_info = """
+[export]
+version=1
+"""
+    self.rpc.call_export_info.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, exp_info)
+    op = self.CopyOpCode(self.plain_op,
+                         mode=constants.INSTANCE_IMPORT,
+                         src_node=self.master.name)
+    self.ExecOpCodeExpectOpPrereqError(op, "Wrong export version")
+
+  def testImportPlainWithParametersAndImport(self):
+    exp_info = """
+[export]
+version=0
+os=mock_os
+[instance]
+name=old_name.example.com
+disk0_size=1024
+disk1_size=1500
+disk1_dump=mock_path
+nic0_mode=bridged
+nic0_link=br_mock
+nic0_mac=f6:ab:f4:45:d1:af
+nic0_ip=192.0.2.1
+tags=tag1 tag2
+hypervisor=xen-hvm
+[hypervisor]
+boot_order=cd
+[backend]
+memory=1024
+vcpus=8
+[os]
+param1=val1
+"""
+
+    self.rpc.call_export_info.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, exp_info)
+    self.rpc.call_import_start.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, "daemon_name")
+    self.rpc.call_impexp_status.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master,
+                                    [
+                                      objects.ImportExportStatus(exit_status=0)
+                                    ])
+    self.rpc.call_impexp_cleanup.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, True)
+
+    op = self.CopyOpCode(self.plain_op,
+                         disks=[],
+                         nics=[],
+                         tags=[],
+                         hypervisor=None,
+                         hvparams={},
+                         mode=constants.INSTANCE_IMPORT,
+                         src_node=self.master.name)
+    self.ExecOpCode(op)
+
+
+class TestCheckOSVariant(CmdlibTestCase):
+  def testNoVariantsSupported(self):
+    os = self.cfg.CreateOs(supported_variants=[])
+    self.assertRaises(errors.OpPrereqError, instance._CheckOSVariant,
+                      os, "os+variant")
+
+  def testNoVariantGiven(self):
+    os = self.cfg.CreateOs(supported_variants=["default"])
+    self.assertRaises(errors.OpPrereqError, instance._CheckOSVariant,
+                      os, "os")
+
+  def testWrongVariantGiven(self):
+    os = self.cfg.CreateOs(supported_variants=["default"])
+    self.assertRaises(errors.OpPrereqError, instance._CheckOSVariant,
+                      os, "os+wrong_variant")
+
+  def testOkWithVariant(self):
+    os = self.cfg.CreateOs(supported_variants=["default"])
+    instance._CheckOSVariant(os, "os+default")
+
+  def testOkWithoutVariant(self):
+    os = self.cfg.CreateOs(supported_variants=[])
+    instance._CheckOSVariant(os, "os")
+
+
+class TestCheckTargetNodeIPolicy(TestLUInstanceCreate):
+  def setUp(self):
+    super(TestCheckTargetNodeIPolicy, self).setUp()
+
+    self.op = self.diskless_op
+
+    self.instance = self.cfg.AddNewInstance()
+    self.target_group = self.cfg.AddNewNodeGroup()
+    self.target_node = self.cfg.AddNewNode(group=self.target_group)
+
+  @withLockedLU
+  def testNoViolation(self, lu):
+    compute_recoder = mock.Mock(return_value=[])
+    instance.CheckTargetNodeIPolicy(lu, NotImplemented, self.instance,
+                                    self.target_node, NotImplemented,
+                                    _compute_fn=compute_recoder)
+    self.assertTrue(compute_recoder.called)
+    self.mcpu.assertLogIsEmpty()
+
+  @withLockedLU
+  def testNoIgnore(self, lu):
+    compute_recoder = mock.Mock(return_value=["mem_size not in range"])
+    self.assertRaises(errors.OpPrereqError, instance.CheckTargetNodeIPolicy,
+                      lu, NotImplemented, self.instance,
+                      self.target_node, NotImplemented,
+                      _compute_fn=compute_recoder)
+    self.assertTrue(compute_recoder.called)
+    self.mcpu.assertLogIsEmpty()
+
+  @withLockedLU
+  def testIgnoreViolation(self, lu):
+    compute_recoder = mock.Mock(return_value=["mem_size not in range"])
+    instance.CheckTargetNodeIPolicy(lu, NotImplemented, self.instance,
+                                    self.target_node, NotImplemented,
+                                    ignore=True, _compute_fn=compute_recoder)
+    self.assertTrue(compute_recoder.called)
+    msg = ("Instance does not meet target node group's .* instance policy:"
+           " mem_size not in range")
+    self.mcpu.assertLogContainsRegex(msg)
+
+
+class TestApplyContainerMods(unittest.TestCase):
+  def testEmptyContainer(self):
+    container = []
+    chgdesc = []
+    instance._ApplyContainerMods("test", container, chgdesc, [], None, None,
+                                 None)
+    self.assertEqual(container, [])
+    self.assertEqual(chgdesc, [])
+
+  def testAdd(self):
+    container = []
+    chgdesc = []
+    mods = instance._PrepareContainerMods([
+      (constants.DDM_ADD, -1, "Hello"),
+      (constants.DDM_ADD, -1, "World"),
+      (constants.DDM_ADD, 0, "Start"),
+      (constants.DDM_ADD, -1, "End"),
+      ], None)
+    instance._ApplyContainerMods("test", container, chgdesc, mods,
+                                 None, None, None)
+    self.assertEqual(container, ["Start", "Hello", "World", "End"])
+    self.assertEqual(chgdesc, [])
+
+    mods = instance._PrepareContainerMods([
+      (constants.DDM_ADD, 0, "zero"),
+      (constants.DDM_ADD, 3, "Added"),
+      (constants.DDM_ADD, 5, "four"),
+      (constants.DDM_ADD, 7, "xyz"),
+      ], None)
+    instance._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 = instance._PrepareContainerMods([
+        (constants.DDM_ADD, idx, "error"),
+        ], None)
+      self.assertRaises(IndexError, instance._ApplyContainerMods,
+                        "test", container, None, mods, None, None, None)
+
+  def testRemoveError(self):
+    for idx in [0, 1, 2, 100, -1, -4]:
+      mods = instance._PrepareContainerMods([
+        (constants.DDM_REMOVE, idx, None),
+        ], None)
+      self.assertRaises(IndexError, instance._ApplyContainerMods,
+                        "test", [], None, mods, None, None, None)
+
+    mods = instance._PrepareContainerMods([
+      (constants.DDM_REMOVE, 0, object()),
+      ], None)
+    self.assertRaises(AssertionError, instance._ApplyContainerMods,
+                      "test", [""], None, mods, None, None, None)
+
+  def testAddError(self):
+    for idx in range(-100, -1) + [100]:
+      mods = instance._PrepareContainerMods([
+        (constants.DDM_ADD, idx, None),
+        ], None)
+      self.assertRaises(IndexError, instance._ApplyContainerMods,
+                        "test", [], None, mods, None, None, None)
+
+  def testRemove(self):
+    container = ["item 1", "item 2"]
+    mods = instance._PrepareContainerMods([
+      (constants.DDM_ADD, -1, "aaa"),
+      (constants.DDM_REMOVE, -1, None),
+      (constants.DDM_ADD, -1, "bbb"),
+      ], None)
+    chgdesc = []
+    instance._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 = instance._PrepareContainerMods([
+      (constants.DDM_MODIFY, -1, "a"),
+      (constants.DDM_MODIFY, 0, "b"),
+      (constants.DDM_MODIFY, 1, "c"),
+      ], None)
+    chgdesc = []
+    instance._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 = instance._PrepareContainerMods([
+        (constants.DDM_MODIFY, idx, "error"),
+        ], None)
+      self.assertRaises(IndexError, instance._ApplyContainerMods,
+                        "test", container, None, mods, None, None, 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 = instance._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"),
+      ], mock.Mock)
+    instance._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(ConfigMock):
+  def __init__(self):
+    super(_FakeConfigForGenDiskTemplate, self).__init__()
+
+    self._unique_id = itertools.count()
+    self._drbd_minor = itertools.count(20)
+    self._port = itertools.count(constants.FIRST_DRBD_PORT)
+    self._secret = itertools.count()
+
+  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())
+
+
+class TestGenerateDiskTemplate(CmdlibTestCase):
+  def setUp(self):
+    super(TestGenerateDiskTemplate, self).setUp()
+
+    self.cfg = _FakeConfigForGenDiskTemplate()
+    self.cluster.enabled_disk_templates = list(constants.DISK_TEMPLATES)
+
+    self.nodegroup = self.cfg.AddNewNodeGroup(name="ng")
+
+    self.lu = self.GetMockLU()
+
+  @staticmethod
+  def GetDiskParams():
+    return copy.deepcopy(constants.DISK_DT_DEFAULTS)
+
+  def testWrongDiskTemplate(self):
+    gdt = instance.GenerateDiskTemplate
+    disk_template = "##unknown##"
+
+    assert disk_template not in constants.DISK_TEMPLATES
+
+    self.assertRaises(errors.OpPrereqError, gdt, self.lu, disk_template,
+                      "inst26831.example.com", "node30113.example.com", [], [],
+                      NotImplemented, NotImplemented, 0, self.lu.LogInfo,
+                      self.GetDiskParams())
+
+  def testDiskless(self):
+    gdt = instance.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):
+    gdt = instance.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())
+
+    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())
+
+    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))
+    instance._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.DT_PLAIN)
+
+    self.assertEqual(map(operator.attrgetter("logical_id"), result), [
+      ("xenvg", "ec1-uq0.disk3"),
+      ("othervg", "ec1-uq1.disk4"),
+      ])
+
+  def testFile(self):
+    # anything != DT_FILE would do here
+    self.cluster.enabled_disk_templates = [constants.DT_PLAIN]
+    self.assertRaises(errors.OpPrereqError, self._TestTrivialDisk,
+                      constants.DT_FILE, [], 0, NotImplemented)
+    self.assertRaises(errors.OpPrereqError, self._TestTrivialDisk,
+                      constants.DT_SHARED_FILE, [], 0, NotImplemented)
+
+    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,
+        }]
+
+      self.cluster.enabled_disk_templates = [disk_template]
+      result = self._TestTrivialDisk(
+        disk_template, disk_info, 2, disk_template,
+        file_storage_dir="/tmp", file_driver=constants.FD_BLKTAP)
+
+      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.DT_BLOCK)
+
+    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.DT_RBD)
+
+    self.assertEqual(map(operator.attrgetter("logical_id"), result), [
+      ("rbd", "ec1-uq0.rbd.disk0"),
+      ("rbd", "ec1-uq1.rbd.disk1"),
+      ])
+
+  def testDrbd8(self):
+    gdt = instance.GenerateDiskTemplate
+    drbd8_defaults = constants.DISK_LD_DEFAULTS[constants.DT_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(), "ec1-uq0.disk0_data"),
+        (drbd8_default_metavg, "ec1-uq0.disk0_meta"),
+      ], [
+        (self.lu.cfg.GetVGName(), "ec1-uq1.disk1_data"),
+        ("metavg", "ec1-uq1.disk1_meta"),
+      ], [
+        ("vgxyz", "ec1-uq2.disk2_data"),
+        (drbd8_default_metavg, "ec1-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.DT_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.DT_PLAIN)
+        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, constants.DRBD_META_SIZE)
+
+    self._CheckIvNames(result, 0, len(disk_info))
+    instance._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, "ec1-secret0"),
+      ("node1334.example.com", "node12272.example.com",
+       constants.FIRST_DRBD_PORT + 1, 22, 23, "ec1-secret1"),
+      ("node1334.example.com", "node12272.example.com",
+       constants.FIRST_DRBD_PORT + 2, 24, 25, "ec1-secret2"),
+      ])
+
+
+class _DiskPauseTracker:
+  def __init__(self):
+    self.history = []
+
+  def __call__(self, (disks, instance), pause):
+    assert not (set(disks) - set(instance.disks))
+
+    self.history.extend((i.logical_id, i.size, pause)
+                        for i in disks)
+
+    return (True, [True] * len(disks))
+
+
+class _ConfigForDiskWipe:
+  def __init__(self, exp_node_uuid):
+    self._exp_node_uuid = exp_node_uuid
+
+  def GetNodeName(self, node_uuid):
+    assert node_uuid == self._exp_node_uuid
+    return "name.of.expected.node"
+
+
+class _RpcForDiskWipe:
+  def __init__(self, exp_node, pause_cb, wipe_cb):
+    self._exp_node = exp_node
+    self._pause_cb = pause_cb
+    self._wipe_cb = wipe_cb
+
+  def call_blockdev_pause_resume_sync(self, node, disks, pause):
+    assert node == self._exp_node
+    return rpc.RpcResult(data=self._pause_cb(disks, pause))
+
+  def call_blockdev_wipe(self, node, bdev, offset, size):
+    assert node == self._exp_node
+    return rpc.RpcResult(data=self._wipe_cb(bdev, offset, size))
+
+
+class _DiskWipeProgressTracker:
+  def __init__(self, start_offset):
+    self._start_offset = start_offset
+    self.progress = {}
+
+  def __call__(self, (disk, _), offset, size):
+    assert isinstance(offset, (long, int))
+    assert isinstance(size, (long, int))
+
+    max_chunk_size = (disk.size / 100.0 * constants.MIN_WIPE_CHUNK_PERCENT)
+
+    assert offset >= self._start_offset
+    assert (offset + size) <= disk.size
+
+    assert size > 0
+    assert size <= constants.MAX_WIPE_CHUNK
+    assert size <= max_chunk_size
+
+    assert offset == self._start_offset or disk.logical_id in self.progress
+
+    # Keep track of progress
+    cur_progress = self.progress.setdefault(disk.logical_id, self._start_offset)
+
+    assert cur_progress == offset
+
+    # Record progress
+    self.progress[disk.logical_id] += size
+
+    return (True, None)
+
+
+class TestWipeDisks(unittest.TestCase):
+  def _FailingPauseCb(self, (disks, _), pause):
+    self.assertEqual(len(disks), 3)
+    self.assertTrue(pause)
+    # Simulate an RPC error
+    return (False, "error")
+
+  def testPauseFailure(self):
+    node_name = "node1372.example.com"
+
+    lu = _FakeLU(rpc=_RpcForDiskWipe(node_name, self._FailingPauseCb,
+                                     NotImplemented),
+                 cfg=_ConfigForDiskWipe(node_name))
+
+    disks = [
+      objects.Disk(dev_type=constants.DT_PLAIN),
+      objects.Disk(dev_type=constants.DT_PLAIN),
+      objects.Disk(dev_type=constants.DT_PLAIN),
+      ]
+
+    inst = objects.Instance(name="inst21201",
+                            primary_node=node_name,
+                            disk_template=constants.DT_PLAIN,
+                            disks=disks)
+
+    self.assertRaises(errors.OpExecError, instance.WipeDisks, lu, inst)
+
+  def _FailingWipeCb(self, (disk, _), offset, size):
+    # This should only ever be called for the first disk
+    self.assertEqual(disk.logical_id, "disk0")
+    return (False, None)
+
+  def testFailingWipe(self):
+    node_uuid = "node13445-uuid"
+    pt = _DiskPauseTracker()
+
+    lu = _FakeLU(rpc=_RpcForDiskWipe(node_uuid, pt, self._FailingWipeCb),
+                 cfg=_ConfigForDiskWipe(node_uuid))
+
+    disks = [
+      objects.Disk(dev_type=constants.DT_PLAIN, logical_id="disk0",
+                   size=100 * 1024),
+      objects.Disk(dev_type=constants.DT_PLAIN, logical_id="disk1",
+                   size=500 * 1024),
+      objects.Disk(dev_type=constants.DT_PLAIN, logical_id="disk2", size=256),
+      ]
+
+    inst = objects.Instance(name="inst562",
+                            primary_node=node_uuid,
+                            disk_template=constants.DT_PLAIN,
+                            disks=disks)
+
+    try:
+      instance.WipeDisks(lu, inst)
+    except errors.OpExecError, err:
+      self.assertTrue(str(err), "Could not wipe disk 0 at offset 0 ")
+    else:
+      self.fail("Did not raise exception")
+
+    # Check if all disks were paused and resumed
+    self.assertEqual(pt.history, [
+      ("disk0", 100 * 1024, True),
+      ("disk1", 500 * 1024, True),
+      ("disk2", 256, True),
+      ("disk0", 100 * 1024, False),
+      ("disk1", 500 * 1024, False),
+      ("disk2", 256, False),
+      ])
+
+  def _PrepareWipeTest(self, start_offset, disks):
+    node_name = "node-with-offset%s.example.com" % start_offset
+    pauset = _DiskPauseTracker()
+    progresst = _DiskWipeProgressTracker(start_offset)
+
+    lu = _FakeLU(rpc=_RpcForDiskWipe(node_name, pauset, progresst),
+                 cfg=_ConfigForDiskWipe(node_name))
+
+    instance = objects.Instance(name="inst3560",
+                                primary_node=node_name,
+                                disk_template=constants.DT_PLAIN,
+                                disks=disks)
+
+    return (lu, instance, pauset, progresst)
+
+  def testNormalWipe(self):
+    disks = [
+      objects.Disk(dev_type=constants.DT_PLAIN, logical_id="disk0", size=1024),
+      objects.Disk(dev_type=constants.DT_PLAIN, logical_id="disk1",
+                   size=500 * 1024),
+      objects.Disk(dev_type=constants.DT_PLAIN, logical_id="disk2", size=128),
+      objects.Disk(dev_type=constants.DT_PLAIN, logical_id="disk3",
+                   size=constants.MAX_WIPE_CHUNK),
+      ]
+
+    (lu, inst, pauset, progresst) = self._PrepareWipeTest(0, disks)
+
+    instance.WipeDisks(lu, inst)
+
+    self.assertEqual(pauset.history, [
+      ("disk0", 1024, True),
+      ("disk1", 500 * 1024, True),
+      ("disk2", 128, True),
+      ("disk3", constants.MAX_WIPE_CHUNK, True),
+      ("disk0", 1024, False),
+      ("disk1", 500 * 1024, False),
+      ("disk2", 128, False),
+      ("disk3", constants.MAX_WIPE_CHUNK, False),
+      ])
+
+    # Ensure the complete disk has been wiped
+    self.assertEqual(progresst.progress,
+                     dict((i.logical_id, i.size) for i in disks))
+
+  def testWipeWithStartOffset(self):
+    for start_offset in [0, 280, 8895, 1563204]:
+      disks = [
+        objects.Disk(dev_type=constants.DT_PLAIN, logical_id="disk0",
+                     size=128),
+        objects.Disk(dev_type=constants.DT_PLAIN, logical_id="disk1",
+                     size=start_offset + (100 * 1024)),
+        ]
+
+      (lu, inst, pauset, progresst) = \
+        self._PrepareWipeTest(start_offset, disks)
+
+      # Test start offset with only one disk
+      instance.WipeDisks(lu, inst,
+                         disks=[(1, disks[1], start_offset)])
+
+      # Only the second disk may have been paused and wiped
+      self.assertEqual(pauset.history, [
+        ("disk1", start_offset + (100 * 1024), True),
+        ("disk1", start_offset + (100 * 1024), False),
+        ])
+      self.assertEqual(progresst.progress, {
+        "disk1": disks[1].size,
+        })
+
+
+class TestCheckOpportunisticLocking(unittest.TestCase):
+  class OpTest(opcodes.OpCode):
+    OP_PARAMS = [
+      ("opportunistic_locking", False, ht.TBool, None),
+      ("iallocator", None, ht.TMaybe(ht.TNonEmptyString), "")
+      ]
+
+  @classmethod
+  def _MakeOp(cls, **kwargs):
+    op = cls.OpTest(**kwargs)
+    op.Validate(True)
+    return op
+
+  def testMissingAttributes(self):
+    self.assertRaises(AttributeError, instance._CheckOpportunisticLocking,
+                      object())
+
+  def testDefaults(self):
+    op = self._MakeOp()
+    instance._CheckOpportunisticLocking(op)
+
+  def test(self):
+    for iallocator in [None, "something", "other"]:
+      for opplock in [False, True]:
+        op = self._MakeOp(iallocator=iallocator,
+                          opportunistic_locking=opplock)
+        if opplock and not iallocator:
+          self.assertRaises(errors.OpPrereqError,
+                            instance._CheckOpportunisticLocking, op)
+        else:
+          instance._CheckOpportunisticLocking(op)
+
+
+class TestLUInstanceRemove(CmdlibTestCase):
+  def testRemoveMissingInstance(self):
+    op = opcodes.OpInstanceRemove(instance_name="missing.inst")
+    self.ExecOpCodeExpectOpPrereqError(op, "Instance 'missing.inst' not known")
+
+  def testRemoveInst(self):
+    inst = self.cfg.AddNewInstance(disks=[])
+    op = opcodes.OpInstanceRemove(instance_name=inst.name)
+    self.ExecOpCode(op)
+
+
+class TestLUInstanceMove(CmdlibTestCase):
+  def setUp(self):
+    super(TestLUInstanceMove, self).setUp()
+
+    self.node = self.cfg.AddNewNode()
+
+    self.rpc.call_blockdev_assemble.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.node, ("/dev/mocked_path",
+                                    "/var/run/ganeti/instance-disks/mocked_d"))
+    self.rpc.call_blockdev_export.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, "")
+    self.rpc.call_blockdev_remove.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, "")
+
+  def testMissingInstance(self):
+    op = opcodes.OpInstanceMove(instance_name="missing.inst",
+                                target_node=self.node.name)
+    self.ExecOpCodeExpectOpPrereqError(op, "Instance 'missing.inst' not known")
+
+  def testUncopyableDiskTemplate(self):
+    inst = self.cfg.AddNewInstance(disk_template=constants.DT_SHARED_FILE)
+    op = opcodes.OpInstanceMove(instance_name=inst.name,
+                                target_node=self.node.name)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Disk template sharedfile not suitable for copying")
+
+  def testAlreadyOnTargetNode(self):
+    inst = self.cfg.AddNewInstance()
+    op = opcodes.OpInstanceMove(instance_name=inst.name,
+                                target_node=self.master.name)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Instance .* is already on the node .*")
+
+  def testMoveStoppedInstance(self):
+    inst = self.cfg.AddNewInstance()
+    op = opcodes.OpInstanceMove(instance_name=inst.name,
+                                target_node=self.node.name)
+    self.ExecOpCode(op)
+
+  def testMoveRunningInstance(self):
+    self.rpc.call_node_info.return_value = \
+      self.RpcResultsBuilder() \
+        .AddSuccessfulNode(self.node,
+                           (NotImplemented, NotImplemented,
+                            ({"memory_free": 10000}, ))) \
+        .Build()
+    self.rpc.call_instance_start.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.node, "")
+
+    inst = self.cfg.AddNewInstance(admin_state=constants.ADMINST_UP)
+    op = opcodes.OpInstanceMove(instance_name=inst.name,
+                                target_node=self.node.name)
+    self.ExecOpCode(op)
+
+  def testMoveFailingStart(self):
+    self.rpc.call_node_info.return_value = \
+      self.RpcResultsBuilder() \
+        .AddSuccessfulNode(self.node,
+                           (NotImplemented, NotImplemented,
+                            ({"memory_free": 10000}, ))) \
+        .Build()
+    self.rpc.call_instance_start.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateFailedNodeResult(self.node)
+
+    inst = self.cfg.AddNewInstance(admin_state=constants.ADMINST_UP)
+    op = opcodes.OpInstanceMove(instance_name=inst.name,
+                                target_node=self.node.name)
+    self.ExecOpCodeExpectOpExecError(
+      op, "Could not start instance .* on node .*")
+
+  def testMoveFailingBlockdevAssemble(self):
+    inst = self.cfg.AddNewInstance()
+    self.rpc.call_blockdev_assemble.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateFailedNodeResult(self.node)
+    op = opcodes.OpInstanceMove(instance_name=inst.name,
+                                target_node=self.node.name)
+    self.ExecOpCodeExpectOpExecError(op, "Errors during disk copy")
+
+  def testMoveFailingBlockdevExport(self):
+    inst = self.cfg.AddNewInstance()
+    self.rpc.call_blockdev_export.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateFailedNodeResult(self.node)
+    op = opcodes.OpInstanceMove(instance_name=inst.name,
+                                target_node=self.node.name)
+    self.ExecOpCodeExpectOpExecError(op, "Errors during disk copy")
+
+
+class TestLUInstanceRename(CmdlibTestCase):
+  def setUp(self):
+    super(TestLUInstanceRename, self).setUp()
+
+    self.inst = self.cfg.AddNewInstance()
+
+    self.op = opcodes.OpInstanceRename(instance_name=self.inst.name,
+                                       new_name="new_name.example.com")
+
+  def testIpCheckWithoutNameCheck(self):
+    op = self.CopyOpCode(self.op,
+                         ip_check=True,
+                         name_check=False)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "IP address check requires a name check")
+
+  def testIpAlreadyInUse(self):
+    self.netutils_mod.TcpPing.return_value = True
+    op = self.CopyOpCode(self.op)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "IP .* of instance .* already in use")
+
+  def testExistingInstanceName(self):
+    self.cfg.AddNewInstance(name="new_name.example.com")
+    op = self.CopyOpCode(self.op)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Instance .* is already in the cluster")
+
+  def testFileInstance(self):
+    self.rpc.call_blockdev_assemble.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, (None, None))
+    self.rpc.call_blockdev_shutdown.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, None)
+
+    inst = self.cfg.AddNewInstance(disk_template=constants.DT_FILE)
+    op = self.CopyOpCode(self.op,
+                         instance_name=inst.name)
+    self.ExecOpCode(op)
+
+
+class TestLUInstanceMultiAlloc(CmdlibTestCase):
+  def setUp(self):
+    super(TestLUInstanceMultiAlloc, self).setUp()
+
+    self.inst_op = opcodes.OpInstanceCreate(instance_name="inst.example.com",
+                                            disk_template=constants.DT_DRBD8,
+                                            disks=[],
+                                            nics=[],
+                                            os_type="mock_os",
+                                            hypervisor=constants.HT_XEN_HVM,
+                                            mode=constants.INSTANCE_CREATE)
+
+  def testInstanceWithIAllocator(self):
+    inst = self.CopyOpCode(self.inst_op,
+                           iallocator="mock")
+    op = opcodes.OpInstanceMultiAlloc(instances=[inst])
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "iallocator are not allowed to be set on instance objects")
+
+  def testOnlySomeNodesGiven(self):
+    inst1 = self.CopyOpCode(self.inst_op,
+                            pnode=self.master.name)
+    inst2 = self.CopyOpCode(self.inst_op)
+    op = opcodes.OpInstanceMultiAlloc(instances=[inst1, inst2])
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "There are instance objects providing pnode/snode while others"
+          " do not")
+
+  def testMissingIAllocator(self):
+    self.cluster.default_iallocator = None
+    inst = self.CopyOpCode(self.inst_op)
+    op = opcodes.OpInstanceMultiAlloc(instances=[inst])
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "No iallocator or nodes on the instances given and no cluster-wide"
+          " default iallocator found")
+
+  def testDuplicateInstanceNames(self):
+    inst1 = self.CopyOpCode(self.inst_op)
+    inst2 = self.CopyOpCode(self.inst_op)
+    op = opcodes.OpInstanceMultiAlloc(instances=[inst1, inst2])
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "There are duplicate instance names")
+
+  def testWithGivenNodes(self):
+    snode = self.cfg.AddNewNode()
+    inst = self.CopyOpCode(self.inst_op,
+                           pnode=self.master.name,
+                           snode=snode.name)
+    op = opcodes.OpInstanceMultiAlloc(instances=[inst])
+    self.ExecOpCode(op)
+
+  def testDryRun(self):
+    snode = self.cfg.AddNewNode()
+    inst = self.CopyOpCode(self.inst_op,
+                           pnode=self.master.name,
+                           snode=snode.name)
+    op = opcodes.OpInstanceMultiAlloc(instances=[inst],
+                                      dry_run=True)
+    self.ExecOpCode(op)
+
+  def testWithIAllocator(self):
+    snode = self.cfg.AddNewNode()
+    self.iallocator_cls.return_value.result = \
+      ([("inst.example.com", [self.master.name, snode.name])], [])
+
+    inst = self.CopyOpCode(self.inst_op)
+    op = opcodes.OpInstanceMultiAlloc(instances=[inst],
+                                      iallocator="mock_ialloc")
+    self.ExecOpCode(op)
+
+  def testWithIAllocatorOpportunisticLocking(self):
+    snode = self.cfg.AddNewNode()
+    self.iallocator_cls.return_value.result = \
+      ([("inst.example.com", [self.master.name, snode.name])], [])
+
+    inst = self.CopyOpCode(self.inst_op)
+    op = opcodes.OpInstanceMultiAlloc(instances=[inst],
+                                      iallocator="mock_ialloc",
+                                      opportunistic_locking=True)
+    self.ExecOpCode(op)
+
+  def testFailingIAllocator(self):
+    self.iallocator_cls.return_value.success = False
+
+    inst = self.CopyOpCode(self.inst_op)
+    op = opcodes.OpInstanceMultiAlloc(instances=[inst],
+                                      iallocator="mock_ialloc")
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Can't compute nodes using iallocator")
+
+
+class TestLUInstanceSetParams(CmdlibTestCase):
+  def setUp(self):
+    super(TestLUInstanceSetParams, self).setUp()
+
+    self.inst = self.cfg.AddNewInstance()
+    self.op = opcodes.OpInstanceSetParams(instance_name=self.inst.name)
+
+    self.running_inst = \
+      self.cfg.AddNewInstance(admin_state=constants.ADMINST_UP)
+    self.running_op = \
+      opcodes.OpInstanceSetParams(instance_name=self.running_inst.name)
+
+    self.snode = self.cfg.AddNewNode()
+
+    self.mocked_storage_type = constants.ST_LVM_VG
+    self.mocked_storage_free = 10000
+    self.mocked_master_cpu_total = 16
+    self.mocked_master_memory_free = 2048
+    self.mocked_snode_cpu_total = 16
+    self.mocked_snode_memory_free = 512
+
+    self.mocked_running_inst_memory = 1024
+    self.mocked_running_inst_vcpus = 8
+    self.mocked_running_inst_state = "running"
+    self.mocked_running_inst_time = 10938474
+
+    bootid = "mock_bootid"
+    storage_info = [
+      {
+        "type": self.mocked_storage_type,
+        "storage_free": self.mocked_storage_free
+      }
+    ]
+    hv_info_master = {
+      "cpu_total": self.mocked_master_cpu_total,
+      "memory_free": self.mocked_master_memory_free
+    }
+    hv_info_snode = {
+      "cpu_total": self.mocked_snode_cpu_total,
+      "memory_free": self.mocked_snode_memory_free
+    }
+
+    self.rpc.call_node_info.return_value = \
+      self.RpcResultsBuilder() \
+        .AddSuccessfulNode(self.master,
+                           (bootid, storage_info, (hv_info_master, ))) \
+        .AddSuccessfulNode(self.snode,
+                           (bootid, storage_info, (hv_info_snode, ))) \
+        .Build()
+
+    def _InstanceInfo(_, instance, __, ___):
+      if instance == self.inst.name:
+        return self.RpcResultsBuilder() \
+          .CreateSuccessfulNodeResult(self.master, None)
+      elif instance == self.running_inst.name:
+        return self.RpcResultsBuilder() \
+          .CreateSuccessfulNodeResult(
+            self.master, {
+              "memory": self.mocked_running_inst_memory,
+              "vcpus": self.mocked_running_inst_vcpus,
+              "state": self.mocked_running_inst_state,
+              "time": self.mocked_running_inst_time
+            })
+      else:
+        raise AssertionError()
+    self.rpc.call_instance_info.side_effect = _InstanceInfo
+
+    self.rpc.call_bridges_exist.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, True)
+
+    self.rpc.call_blockdev_getmirrorstatus.side_effect = \
+      lambda node, _: self.RpcResultsBuilder() \
+                        .CreateSuccessfulNodeResult(node, [])
+
+    self.rpc.call_blockdev_shutdown.side_effect = \
+      lambda node, _: self.RpcResultsBuilder() \
+                        .CreateSuccessfulNodeResult(node, [])
+
+  def testNoChanges(self):
+    op = self.CopyOpCode(self.op)
+    self.ExecOpCodeExpectOpPrereqError(op, "No changes submitted")
+
+  def testGlobalHvparams(self):
+    op = self.CopyOpCode(self.op,
+                         hvparams={constants.HV_MIGRATION_PORT: 1234})
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "hypervisor parameters are global and cannot be customized")
+
+  def testHvparams(self):
+    op = self.CopyOpCode(self.op,
+                         hvparams={constants.HV_BOOT_ORDER: "cd"})
+    self.ExecOpCode(op)
+
+  def testDisksAndDiskTemplate(self):
+    op = self.CopyOpCode(self.op,
+                         disk_template=constants.DT_PLAIN,
+                         disks=[[constants.DDM_ADD, -1, {}]])
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Disk template conversion and other disk changes not supported at"
+          " the same time")
+
+  def testDiskTemplateToMirroredNoRemoteNode(self):
+    op = self.CopyOpCode(self.op,
+                         disk_template=constants.DT_DRBD8)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Changing the disk template to a mirrored one requires specifying"
+          " a secondary node")
+
+  def testPrimaryNodeToOldPrimaryNode(self):
+    op = self.CopyOpCode(self.op,
+                         pnode=self.master.name)
+    self.ExecOpCode(op)
+
+  def testPrimaryNodeChange(self):
+    node = self.cfg.AddNewNode()
+    op = self.CopyOpCode(self.op,
+                         pnode=node.name)
+    self.ExecOpCode(op)
+
+  def testPrimaryNodeChangeRunningInstance(self):
+    node = self.cfg.AddNewNode()
+    op = self.CopyOpCode(self.running_op,
+                         pnode=node.name)
+    self.ExecOpCodeExpectOpPrereqError(op, "Instance is still running")
+
+  def testOsChange(self):
+    os = self.cfg.CreateOs(supported_variants=[])
+    self.rpc.call_os_get.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, os)
+    op = self.CopyOpCode(self.op,
+                         os_name=os.name)
+    self.ExecOpCode(op)
+
+  def testVCpuChange(self):
+    op = self.CopyOpCode(self.op,
+                         beparams={
+                           constants.BE_VCPUS: 4
+                         })
+    self.ExecOpCode(op)
+
+  def testWrongCpuMask(self):
+    op = self.CopyOpCode(self.op,
+                         beparams={
+                           constants.BE_VCPUS: 4
+                         },
+                         hvparams={
+                           constants.HV_CPU_MASK: "1,2:3,4"
+                         })
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Number of vCPUs .* does not match the CPU mask .*")
+
+  def testCorrectCpuMask(self):
+    op = self.CopyOpCode(self.op,
+                         beparams={
+                           constants.BE_VCPUS: 4
+                         },
+                         hvparams={
+                           constants.HV_CPU_MASK: "1,2:3,4:all:1,4"
+                         })
+    self.ExecOpCode(op)
+
+  def testOsParams(self):
+    op = self.CopyOpCode(self.op,
+                         osparams={
+                           self.os.supported_parameters[0]: "test_param_val"
+                         })
+    self.ExecOpCode(op)
+
+  def testIncreaseMemoryTooMuch(self):
+    op = self.CopyOpCode(self.running_op,
+                         beparams={
+                           constants.BE_MAXMEM:
+                             self.mocked_master_memory_free * 2
+                         })
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "This change will prevent the instance from starting")
+
+  def testIncreaseMemory(self):
+    op = self.CopyOpCode(self.running_op,
+                         beparams={
+                           constants.BE_MAXMEM: self.mocked_master_memory_free
+                         })
+    self.ExecOpCode(op)
+
+  def testIncreaseMemoryTooMuchForSecondary(self):
+    inst = self.cfg.AddNewInstance(admin_state=constants.ADMINST_UP,
+                                   disk_template=constants.DT_DRBD8,
+                                   secondary_node=self.snode)
+    self.rpc.call_instance_info.side_effect = [
+      self.RpcResultsBuilder()
+        .CreateSuccessfulNodeResult(self.master,
+                                    {
+                                      "memory":
+                                        self.mocked_snode_memory_free * 2,
+                                      "vcpus": self.mocked_running_inst_vcpus,
+                                      "state": self.mocked_running_inst_state,
+                                      "time": self.mocked_running_inst_time
+                                    })]
+
+    op = self.CopyOpCode(self.op,
+                         instance_name=inst.name,
+                         beparams={
+                           constants.BE_MAXMEM:
+                             self.mocked_snode_memory_free * 2,
+                           constants.BE_AUTO_BALANCE: True
+                         })
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "This change will prevent the instance from failover to its"
+          " secondary node")
+
+  def testInvalidRuntimeMemory(self):
+    op = self.CopyOpCode(self.running_op,
+                         runtime_mem=self.mocked_master_memory_free * 2)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Instance .* must have memory between .* and .* of memory")
+
+  def testIncreaseRuntimeMemory(self):
+    op = self.CopyOpCode(self.running_op,
+                         runtime_mem=self.mocked_master_memory_free,
+                         beparams={
+                           constants.BE_MAXMEM: self.mocked_master_memory_free
+                         })
+    self.ExecOpCode(op)
+
+  def testAddNicWithPoolIpNoNetwork(self):
+    op = self.CopyOpCode(self.op,
+                         nics=[(constants.DDM_ADD, -1,
+                                {
+                                  constants.INIC_IP: constants.NIC_IP_POOL
+                                })])
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "If ip=pool, parameter network cannot be none")
+
+  def testAddNicWithPoolIp(self):
+    net = self.cfg.AddNewNetwork()
+    self.cfg.ConnectNetworkToGroup(net, self.group)
+    op = self.CopyOpCode(self.op,
+                         nics=[(constants.DDM_ADD, -1,
+                                {
+                                  constants.INIC_IP: constants.NIC_IP_POOL,
+                                  constants.INIC_NETWORK: net.name
+                                })])
+    self.ExecOpCode(op)
+
+  def testAddNicWithInvalidIp(self):
+    op = self.CopyOpCode(self.op,
+                         nics=[(constants.DDM_ADD, -1,
+                                {
+                                  constants.INIC_IP: "invalid"
+                                })])
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Invalid IP address")
+
+  def testAddNic(self):
+    op = self.CopyOpCode(self.op,
+                         nics=[(constants.DDM_ADD, -1, {})])
+    self.ExecOpCode(op)
+
+  def testHotAddNic(self):
+    op = self.CopyOpCode(self.op,
+                         nics=[(constants.DDM_ADD, -1, {})],
+                         hotplug=True)
+    self.ExecOpCode(op)
+    self.assertTrue(self.rpc.call_hotplug_device.called)
+
+  def testAddNicWithIp(self):
+    op = self.CopyOpCode(self.op,
+                         nics=[(constants.DDM_ADD, -1,
+                                {
+                                  constants.INIC_IP: "2.3.1.4"
+                                })])
+    self.ExecOpCode(op)
+
+  def testModifyNicRoutedWithoutIp(self):
+    op = self.CopyOpCode(self.op,
+                         nics=[(constants.DDM_MODIFY, 0,
+                                {
+                                  constants.INIC_MODE: constants.NIC_MODE_ROUTED
+                                })])
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Cannot set the NIC IP address to None on a routed NIC")
+
+  def testModifyNicSetMac(self):
+    op = self.CopyOpCode(self.op,
+                         nics=[(constants.DDM_MODIFY, 0,
+                                {
+                                  constants.INIC_MAC: "0a:12:95:15:bf:75"
+                                })])
+    self.ExecOpCode(op)
+
+  def testModifyNicWithPoolIpNoNetwork(self):
+    op = self.CopyOpCode(self.op,
+                         nics=[(constants.DDM_MODIFY, -1,
+                                {
+                                  constants.INIC_IP: constants.NIC_IP_POOL
+                                })])
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "ip=pool, but no network found")
+
+  def testModifyNicSetNet(self):
+    old_net = self.cfg.AddNewNetwork()
+    self.cfg.ConnectNetworkToGroup(old_net, self.group)
+    inst = self.cfg.AddNewInstance(nics=[
+      self.cfg.CreateNic(network=old_net,
+                         ip="198.51.100.2")])
+
+    new_net = self.cfg.AddNewNetwork(mac_prefix="be")
+    self.cfg.ConnectNetworkToGroup(new_net, self.group)
+    op = self.CopyOpCode(self.op,
+                         instance_name=inst.name,
+                         nics=[(constants.DDM_MODIFY, 0,
+                                {
+                                  constants.INIC_NETWORK: new_net.name
+                                })])
+    self.ExecOpCode(op)
+
+  def testModifyNicSetLinkWhileConnected(self):
+    old_net = self.cfg.AddNewNetwork()
+    self.cfg.ConnectNetworkToGroup(old_net, self.group)
+    inst = self.cfg.AddNewInstance(nics=[
+      self.cfg.CreateNic(network=old_net)])
+
+    op = self.CopyOpCode(self.op,
+                         instance_name=inst.name,
+                         nics=[(constants.DDM_MODIFY, 0,
+                                {
+                                  constants.INIC_LINK: "mock_link"
+                                })])
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Not allowed to change link or mode of a NIC that is connected"
+          " to a network")
+
+  def testModifyNicSetNetAndIp(self):
+    net = self.cfg.AddNewNetwork(mac_prefix="be", network="123.123.123.0/24")
+    self.cfg.ConnectNetworkToGroup(net, self.group)
+    op = self.CopyOpCode(self.op,
+                         nics=[(constants.DDM_MODIFY, 0,
+                                {
+                                  constants.INIC_NETWORK: net.name,
+                                  constants.INIC_IP: "123.123.123.1"
+                                })])
+    self.ExecOpCode(op)
+
+  def testModifyNic(self):
+    op = self.CopyOpCode(self.op,
+                         nics=[(constants.DDM_MODIFY, 0, {})])
+    self.ExecOpCode(op)
+
+  def testHotModifyNic(self):
+    op = self.CopyOpCode(self.op,
+                         nics=[(constants.DDM_MODIFY, 0, {})],
+                         hotplug=True)
+    self.ExecOpCode(op)
+    self.assertTrue(self.rpc.call_hotplug_device.called)
+
+  def testRemoveLastNic(self):
+    op = self.CopyOpCode(self.op,
+                         nics=[(constants.DDM_REMOVE, 0, {})])
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "violates policy")
+
+  def testRemoveNic(self):
+    inst = self.cfg.AddNewInstance(nics=[self.cfg.CreateNic(),
+                                         self.cfg.CreateNic()])
+    op = self.CopyOpCode(self.op,
+                         instance_name=inst.name,
+                         nics=[(constants.DDM_REMOVE, 0, {})])
+    self.ExecOpCode(op)
+
+  def testHotRemoveNic(self):
+    inst = self.cfg.AddNewInstance(nics=[self.cfg.CreateNic(),
+                                         self.cfg.CreateNic()])
+    op = self.CopyOpCode(self.op,
+                         instance_name=inst.name,
+                         nics=[(constants.DDM_REMOVE, 0, {})],
+                         hotplug=True)
+    self.ExecOpCode(op)
+    self.assertTrue(self.rpc.call_hotplug_device.called)
+
+  def testSetOffline(self):
+    op = self.CopyOpCode(self.op,
+                         offline=True)
+    self.ExecOpCode(op)
+
+  def testUnsetOffline(self):
+    op = self.CopyOpCode(self.op,
+                         offline=False)
+    self.ExecOpCode(op)
+
+  def testAddDiskInvalidMode(self):
+    op = self.CopyOpCode(self.op,
+                         disks=[[constants.DDM_ADD, -1,
+                                 {
+                                   constants.IDISK_MODE: "invalid"
+                                 }]])
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Invalid disk access mode 'invalid'")
+
+  def testAddDiskMissingSize(self):
+    op = self.CopyOpCode(self.op,
+                         disks=[[constants.DDM_ADD, -1, {}]])
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Required disk parameter 'size' missing")
+
+  def testAddDiskInvalidSize(self):
+    op = self.CopyOpCode(self.op,
+                         disks=[[constants.DDM_ADD, -1,
+                                 {
+                                   constants.IDISK_SIZE: "invalid"
+                                 }]])
+    self.ExecOpCodeExpectException(
+      op, errors.TypeEnforcementError, "is not a valid size")
+
+  def testAddDiskRunningInstanceNoWaitForSync(self):
+    op = self.CopyOpCode(self.running_op,
+                         disks=[[constants.DDM_ADD, -1,
+                                 {
+                                   constants.IDISK_SIZE: 1024
+                                 }]],
+                         wait_for_sync=False)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Can't add a disk to an instance with activated disks"
+          " and --no-wait-for-sync given.")
+
+  def testAddDiskDownInstance(self):
+    op = self.CopyOpCode(self.op,
+                         disks=[[constants.DDM_ADD, -1,
+                                 {
+                                   constants.IDISK_SIZE: 1024
+                                 }]])
+    self.ExecOpCode(op)
+
+    self.assertTrue(self.rpc.call_blockdev_shutdown.called)
+
+  def testAddDiskRunningInstance(self):
+    op = self.CopyOpCode(self.running_op,
+                         disks=[[constants.DDM_ADD, -1,
+                                 {
+                                   constants.IDISK_SIZE: 1024
+                                 }]])
+    self.ExecOpCode(op)
+
+    self.assertFalse(self.rpc.call_blockdev_shutdown.called)
+
+  def testAddDiskNoneName(self):
+    op = self.CopyOpCode(self.op,
+                         disks=[[constants.DDM_ADD, -1,
+                                 {
+                                   constants.IDISK_SIZE: 1024,
+                                   constants.IDISK_NAME: constants.VALUE_NONE
+                                 }]])
+    self.ExecOpCode(op)
+
+  def testHotAddDisk(self):
+    self.rpc.call_blockdev_assemble.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, ("/dev/mocked_path",
+                                    "/var/run/ganeti/instance-disks/mocked_d"))
+    op = self.CopyOpCode(self.op,
+                         disks=[[constants.DDM_ADD, -1,
+                                 {
+                                   constants.IDISK_SIZE: 1024,
+                                 }]],
+                         hotplug=True)
+    self.ExecOpCode(op)
+    self.assertTrue(self.rpc.call_blockdev_create.called)
+    self.assertTrue(self.rpc.call_blockdev_assemble.called)
+    self.assertTrue(self.rpc.call_hotplug_device.called)
+
+  def testHotRemoveDisk(self):
+    inst = self.cfg.AddNewInstance(disks=[self.cfg.CreateDisk(),
+                                          self.cfg.CreateDisk()])
+    op = self.CopyOpCode(self.op,
+                         instance_name=inst.name,
+                         disks=[[constants.DDM_REMOVE, -1,
+                                 {}]],
+                         hotplug=True)
+    self.ExecOpCode(op)
+    self.assertTrue(self.rpc.call_hotplug_device.called)
+    self.assertTrue(self.rpc.call_blockdev_shutdown.called)
+    self.assertTrue(self.rpc.call_blockdev_remove.called)
+
+  def testModifyDiskWithSize(self):
+    op = self.CopyOpCode(self.op,
+                         disks=[[constants.DDM_MODIFY, 0,
+                                 {
+                                   constants.IDISK_SIZE: 1024
+                                 }]])
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Disk size change not possible, use grow-disk")
+
+  def testModifyDiskWithRandomParams(self):
+    op = self.CopyOpCode(self.op,
+                         disks=[[constants.DDM_MODIFY, 0,
+                                 {
+                                   constants.IDISK_METAVG: "new_meta_vg",
+                                   constants.IDISK_MODE: "invalid",
+                                   constants.IDISK_NAME: "new_name"
+                                 }]])
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Disk modification doesn't support additional arbitrary parameters")
+
+  def testModifyDiskUnsetName(self):
+    op = self.CopyOpCode(self.op,
+                         disks=[[constants.DDM_MODIFY, 0,
+                                  {
+                                    constants.IDISK_NAME: constants.VALUE_NONE
+                                  }]])
+    self.ExecOpCode(op)
+
+  def testSetOldDiskTemplate(self):
+    op = self.CopyOpCode(self.op,
+                         disk_template=self.inst.disk_template)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Instance already has disk template")
+
+  def testSetDisabledDiskTemplate(self):
+    self.cfg.SetEnabledDiskTemplates([self.inst.disk_template])
+    op = self.CopyOpCode(self.op,
+                         disk_template=constants.DT_EXT)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Disk template .* is not enabled for this cluster")
+
+  def testInvalidDiskTemplateConversion(self):
+    op = self.CopyOpCode(self.op,
+                         disk_template=constants.DT_EXT)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Unsupported disk template conversion from .* to .*")
+
+  def testConvertToDRBDWithSecondarySameAsPrimary(self):
+    op = self.CopyOpCode(self.op,
+                         disk_template=constants.DT_DRBD8,
+                         remote_node=self.master.name)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Given new secondary node .* is the same as the primary node"
+          " of the instance")
+
+  def testConvertPlainToDRBD(self):
+    self.rpc.call_blockdev_shutdown.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, True)
+    self.rpc.call_blockdev_getmirrorstatus.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, [objects.BlockDevStatus()])
+
+    op = self.CopyOpCode(self.op,
+                         disk_template=constants.DT_DRBD8,
+                         remote_node=self.snode.name)
+    self.ExecOpCode(op)
+
+  def testConvertDRBDToPlain(self):
+    self.inst.disks = [self.cfg.CreateDisk(dev_type=constants.DT_DRBD8,
+                                           primary_node=self.master,
+                                           secondary_node=self.snode)]
+    self.inst.disk_template = constants.DT_DRBD8
+    self.rpc.call_blockdev_shutdown.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, True)
+    self.rpc.call_blockdev_remove.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master)
+    self.rpc.call_blockdev_getmirrorstatus.return_value = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.master, [objects.BlockDevStatus()])
+
+    op = self.CopyOpCode(self.op,
+                         disk_template=constants.DT_PLAIN)
+    self.ExecOpCode(op)
+
+
+class TestLUInstanceChangeGroup(CmdlibTestCase):
+  def setUp(self):
+    super(TestLUInstanceChangeGroup, self).setUp()
+
+    self.group2 = self.cfg.AddNewNodeGroup()
+    self.node2 = self.cfg.AddNewNode(group=self.group2)
+    self.inst = self.cfg.AddNewInstance()
+    self.op = opcodes.OpInstanceChangeGroup(instance_name=self.inst.name)
+
+  def testTargetGroupIsInstanceGroup(self):
+    op = self.CopyOpCode(self.op,
+                         target_groups=[self.group.name])
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Can't use group\(s\) .* as targets, they are used by the"
+          " instance .*")
+
+  def testNoTargetGroups(self):
+    inst = self.cfg.AddNewInstance(disk_template=constants.DT_DRBD8,
+                                   primary_node=self.master,
+                                   secondary_node=self.node2)
+    op = self.CopyOpCode(self.op,
+                         instance_name=inst.name)
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "There are no possible target groups")
+
+  def testFailingIAllocator(self):
+    self.iallocator_cls.return_value.success = False
+    op = self.CopyOpCode(self.op)
+
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "Can't compute solution for changing group of instance .*"
+          " using iallocator .*")
+
+  def testChangeGroup(self):
+    self.iallocator_cls.return_value.success = True
+    self.iallocator_cls.return_value.result = ([], [], [])
+    op = self.CopyOpCode(self.op)
+
+    self.ExecOpCode(op)
+
+
+if __name__ == "__main__":
+  testutils.GanetiTestProgram()
diff --git a/test/py/cmdlib/node_unittest.py b/test/py/cmdlib/node_unittest.py
new file mode 100644 (file)
index 0000000..a3a6384
--- /dev/null
@@ -0,0 +1,258 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2013 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.
+
+
+"""Tests for LUNode*
+
+"""
+
+from collections import defaultdict
+
+from ganeti import compat
+from ganeti import constants
+from ganeti import objects
+from ganeti import opcodes
+from ganeti import errors
+
+from testsupport import *
+
+import testutils
+
+# pylint: disable=W0613
+def _TcpPingFailSecondary(cfg, mock_fct, target, port, timeout=None,
+                          live_port_needed=None, source=None):
+  # This will return True if target is in 192.0.2.0/24 (primary range)
+  # and False if not.
+  return "192.0.2." in target
+
+class TestLUNodeAdd(CmdlibTestCase):
+  def setUp(self):
+    super(TestLUNodeAdd, self).setUp()
+
+    # One node for testing readding:
+    self.node_readd = self.cfg.AddNewNode()
+    self.op_readd = opcodes.OpNodeAdd(node_name=self.node_readd.name,
+                                      readd=True,
+                                      primary_ip=self.node_readd.primary_ip,
+                                      secondary_ip=self.node_readd.secondary_ip)
+
+    # One node for testing adding:
+    # don't add to configuration now!
+    self.node_add = objects.Node(name="node_add",
+                                 primary_ip="192.0.2.200",
+                                 secondary_ip="203.0.113.200")
+
+    self.op_add = opcodes.OpNodeAdd(node_name=self.node_add.name,
+                                    primary_ip=self.node_add.primary_ip,
+                                    secondary_ip=self.node_add.secondary_ip)
+
+    self.netutils_mod.TcpPing.return_value = True
+
+    self.mocked_dns_rpc = self.rpc_mod.DnsOnlyRunner.return_value
+
+    self.mocked_dns_rpc.call_version.return_value = \
+      self.RpcResultsBuilder(use_node_names=True) \
+        .AddSuccessfulNode(self.node_add, constants.CONFIG_VERSION) \
+        .AddSuccessfulNode(self.node_readd, constants.CONFIG_VERSION) \
+        .Build()
+
+    node_verify_result = \
+      self.RpcResultsBuilder() \
+        .CreateSuccessfulNodeResult(self.node_add, {constants.NV_NODELIST: []})
+    # we can't know the node's UUID in advance, so use defaultdict here
+    self.rpc.call_node_verify.return_value = \
+      defaultdict(lambda: node_verify_result, {})
+
+  def testOvsParamsButNotEnabled(self):
+    ndparams = {
+      constants.ND_OVS: False,
+      constants.ND_OVS_NAME: "testswitch",
+    }
+
+    op = self.CopyOpCode(self.op_add,
+                         ndparams=ndparams)
+
+    self.ExecOpCodeExpectOpPrereqError(op, "OpenvSwitch is not enabled")
+
+  def testOvsNoLink(self):
+    ndparams = {
+      constants.ND_OVS: True,
+      constants.ND_OVS_NAME: "testswitch",
+      constants.ND_OVS_LINK: None,
+    }
+
+    op = self.CopyOpCode(self.op_add,
+                         ndparams=ndparams)
+
+    self.ExecOpCode(op)
+    self.assertLogContainsRegex(
+      "No physical interface for OpenvSwitch was given."
+      " OpenvSwitch will not have an outside connection."
+      " This might not be what you want")
+
+    created_node = self.cfg.GetNodeInfoByName(op.node_name)
+    self.assertEqual(ndparams[constants.ND_OVS],
+                     created_node.ndparams.get(constants.ND_OVS, None))
+    self.assertEqual(ndparams[constants.ND_OVS_NAME],
+                     created_node.ndparams.get(constants.ND_OVS_NAME, None))
+    self.assertEqual(ndparams[constants.ND_OVS_LINK],
+                     created_node.ndparams.get(constants.ND_OVS_LINK, None))
+
+  def testWithoutOVS(self):
+    self.ExecOpCode(self.op_add)
+
+    created_node = self.cfg.GetNodeInfoByName(self.op_add.node_name)
+    self.assertEqual(None,
+                     created_node.ndparams.get(constants.ND_OVS, None))
+
+  def testWithOVS(self):
+    ndparams = {
+      constants.ND_OVS: True,
+      constants.ND_OVS_LINK: "eth2",
+    }
+
+    op = self.CopyOpCode(self.op_add,
+                         ndparams=ndparams)
+
+    self.ExecOpCode(op)
+
+    created_node = self.cfg.GetNodeInfoByName(op.node_name)
+    self.assertEqual(ndparams[constants.ND_OVS],
+                     created_node.ndparams.get(constants.ND_OVS, None))
+    self.assertEqual(ndparams[constants.ND_OVS_LINK],
+                     created_node.ndparams.get(constants.ND_OVS_LINK, None))
+
+  def testReaddingMaster(self):
+    op = opcodes.OpNodeAdd(node_name=self.cfg.GetMasterNodeName(),
+                           readd=True)
+
+    self.ExecOpCodeExpectOpPrereqError(op, "Cannot readd the master node")
+
+  def testReaddNotVmCapableNode(self):
+    self.cfg.AddNewInstance(primary_node=self.node_readd)
+    self.netutils_mod.GetHostname.return_value = \
+      HostnameMock(self.node_readd.name, self.node_readd.primary_ip)
+
+    op = self.CopyOpCode(self.op_readd, vm_capable=False)
+
+    self.ExecOpCodeExpectOpPrereqError(op, "Node .* being re-added with"
+                                       " vm_capable flag set to false, but it"
+                                       " already holds instances")
+
+  def testReaddAndPassNodeGroup(self):
+    op = self.CopyOpCode(self.op_readd,group="groupname")
+
+    self.ExecOpCodeExpectOpPrereqError(op, "Cannot pass a node group when a"
+                                       " node is being readded")
+
+  def testPrimaryIPv6(self):
+    self.master.secondary_ip = self.master.primary_ip
+
+    op = self.CopyOpCode(self.op_add, primary_ip="2001:DB8::1",
+                         secondary_ip=self.REMOVE)
+
+    self.ExecOpCode(op)
+
+  def testInvalidSecondaryIP(self):
+    op = self.CopyOpCode(self.op_add, secondary_ip="333.444.555.777")
+
+    self.ExecOpCodeExpectOpPrereqError(op, "Secondary IP .* needs to be a valid"
+                                       " IPv4 address")
+
+  def testNodeAlreadyInCluster(self):
+    op = self.CopyOpCode(self.op_readd, readd=False)
+
+    self.ExecOpCodeExpectOpPrereqError(op, "Node %s is already in the"
+                                       " configuration" % self.node_readd.name)
+
+  def testReaddNodeNotInConfiguration(self):
+    op = self.CopyOpCode(self.op_add, readd=True)
+
+    self.ExecOpCodeExpectOpPrereqError(op, "Node %s is not in the"
+                                       " configuration" % self.node_add.name)
+
+  def testPrimaryIpConflict(self):
+    # In LUNodeAdd, DNS will resolve the node name to an IP address, that is
+    # used to overwrite any given primary_ip value!
+    # Thus we need to mock this DNS resolver here!
+    self.netutils_mod.GetHostname.return_value = \
+      HostnameMock(self.node_add.name, self.node_readd.primary_ip)
+
+    op = self.CopyOpCode(self.op_add)
+
+    self.ExecOpCodeExpectOpPrereqError(op, "New node ip address.* conflict with"
+                                       " existing node")
+
+  def testSecondaryIpConflict(self):
+    op = self.CopyOpCode(self.op_add, secondary_ip=self.node_readd.secondary_ip)
+
+    self.ExecOpCodeExpectOpPrereqError(op, "New node ip address.* conflict with"
+                                       " existing node")
+
+  def testReaddWithDifferentIP(self):
+    op = self.CopyOpCode(self.op_readd, primary_ip="192.0.2.100",
+                         secondary_ip="230.0.113.100")
+
+    self.ExecOpCodeExpectOpPrereqError(op, "Readded node doesn't have the same"
+                                       " IP address configuration as before")
+
+
+  def testNodeHasSecondaryIpButNotMaster(self):
+    self.master.secondary_ip = self.master.primary_ip
+
+    self.ExecOpCodeExpectOpPrereqError(self.op_add, "The master has no"
+                                       " secondary ip but the new node has one")
+
+  def testMasterHasSecondaryIpButNotNode(self):
+    op = self.CopyOpCode(self.op_add, secondary_ip=None)
+
+    self.ExecOpCodeExpectOpPrereqError(op, "The master has a secondary ip but"
+                                       " the new node doesn't have one")
+
+  def testNodeNotReachableByPing(self):
+    self.netutils_mod.TcpPing.return_value = False
+
+    op = self.CopyOpCode(self.op_add)
+
+    self.ExecOpCodeExpectOpPrereqError(op, "Node not reachable by ping")
+
+  def testNodeNotReachableByPingOnSecondary(self):
+    self.netutils_mod.GetHostname.return_value = \
+      HostnameMock(self.node_add.name, self.node_add.primary_ip)
+    self.netutils_mod.TcpPing.side_effect = \
+      compat.partial(_TcpPingFailSecondary, self.cfg, self.netutils_mod.TcpPing)
+
+    op = self.CopyOpCode(self.op_add)
+
+    self.ExecOpCodeExpectOpPrereqError(op, "Node secondary ip not reachable by"
+                                       " TCP based ping to node daemon port")
+
+  def testCantGetVersion(self):
+    self.mocked_dns_rpc.call_version.return_value = \
+      self.RpcResultsBuilder(use_node_names=True) \
+        .AddErrorNode(self.node_add) \
+        .Build()
+
+    op = self.CopyOpCode(self.op_add)
+    self.ExecOpCodeExpectOpExecError(op, "Can't get version information from"
+                                     " node %s" % self.node_add.name)
+
+if __name__ == "__main__":
+  testutils.GanetiTestProgram()
diff --git a/test/py/cmdlib/test_unittest.py b/test/py/cmdlib/test_unittest.py
new file mode 100644 (file)
index 0000000..92f9d02
--- /dev/null
@@ -0,0 +1,250 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2013 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.
+
+
+"""Tests for LUTest*"""
+
+import mock
+
+from ganeti import constants
+from ganeti import opcodes
+
+from testsupport import *
+
+import testutils
+
+DELAY_DURATION = 0.01
+
+
+class TestLUTestDelay(CmdlibTestCase):
+  def testRepeatedInvocation(self):
+    op = opcodes.OpTestDelay(duration=DELAY_DURATION,
+                             repeat=3)
+    self.ExecOpCode(op)
+
+    self.assertLogContainsMessage(" - INFO: Test delay iteration 0/2")
+    self.mcpu.assertLogContainsEntry(constants.ELOG_MESSAGE,
+                                     " - INFO: Test delay iteration 1/2")
+    self.assertLogContainsRegex("2/2$")
+
+  def testInvalidDuration(self):
+    op = opcodes.OpTestDelay(duration=-1)
+
+    self.ExecOpCodeExpectOpExecError(op)
+
+  def testOnNodeUuid(self):
+    node_uuids = [self.master_uuid]
+    op = opcodes.OpTestDelay(duration=DELAY_DURATION,
+                             on_node_uuids=node_uuids)
+    self.ExecOpCode(op)
+
+    self.rpc.call_test_delay.assert_called_once_with(node_uuids, DELAY_DURATION)
+
+  def testOnNodeName(self):
+    op = opcodes.OpTestDelay(duration=DELAY_DURATION,
+                             on_nodes=[self.master.name])
+    self.ExecOpCode(op)
+
+    self.rpc.call_test_delay.assert_called_once_with([self.master_uuid],
+                                                     DELAY_DURATION)
+
+  def testSuccessfulRpc(self):
+    op = opcodes.OpTestDelay(duration=DELAY_DURATION,
+                             on_nodes=[self.master.name])
+
+    self.rpc.call_test_delay.return_value = \
+      self.RpcResultsBuilder() \
+        .AddSuccessfulNode(self.master) \
+        .Build()
+
+    self.ExecOpCode(op)
+
+    self.rpc.call_test_delay.assert_called_once()
+
+  def testFailingRpc(self):
+    op = opcodes.OpTestDelay(duration=DELAY_DURATION,
+                             on_nodes=[self.master.name])
+
+    self.rpc.call_test_delay.return_value = \
+      self.RpcResultsBuilder() \
+        .AddFailedNode(self.master) \
+        .Build()
+
+    self.ExecOpCodeExpectOpExecError(op)
+
+  def testMultipleNodes(self):
+    node1 = self.cfg.AddNewNode()
+    node2 = self.cfg.AddNewNode()
+    op = opcodes.OpTestDelay(duration=DELAY_DURATION,
+                             on_nodes=[node1.name, node2.name])
+
+    self.rpc.call_test_delay.return_value = \
+      self.RpcResultsBuilder() \
+        .AddSuccessfulNode(node1) \
+        .AddSuccessfulNode(node2) \
+        .Build()
+
+    self.ExecOpCode(op)
+
+    self.rpc.call_test_delay.assert_called_once_with([node1.uuid, node2.uuid],
+                                                     DELAY_DURATION)
+
+
+class TestLUTestAllocator(CmdlibTestCase):
+  def setUp(self):
+    super(TestLUTestAllocator, self).setUp()
+
+    self.base_op = opcodes.OpTestAllocator(
+                      name="new-instance.example.com",
+                      nics=[],
+                      disks=[],
+                      disk_template=constants.DT_DISKLESS,
+                      direction=constants.IALLOCATOR_DIR_OUT,
+                      iallocator="test")
+
+    self.valid_alloc_op = \
+      self.CopyOpCode(self.base_op,
+                      mode=constants.IALLOCATOR_MODE_ALLOC,
+                      memory=0,
+                      disk_template=constants.DT_DISKLESS,
+                      os="mock_os",
+                      vcpus=1)
+    self.valid_multi_alloc_op = \
+      self.CopyOpCode(self.base_op,
+                      mode=constants.IALLOCATOR_MODE_MULTI_ALLOC,
+                      instances=["new-instance.example.com"],
+                      memory=0,
+                      disk_template=constants.DT_DISKLESS,
+                      os="mock_os",
+                      vcpus=1)
+    self.valid_reloc_op = \
+      self.CopyOpCode(self.base_op,
+                      mode=constants.IALLOCATOR_MODE_RELOC)
+    self.valid_chg_group_op = \
+      self.CopyOpCode(self.base_op,
+                      mode=constants.IALLOCATOR_MODE_CHG_GROUP,
+                      instances=["new-instance.example.com"],
+                      target_groups=["default"])
+    self.valid_node_evac_op = \
+      self.CopyOpCode(self.base_op,
+                      mode=constants.IALLOCATOR_MODE_NODE_EVAC,
+                      instances=["new-instance.example.com"],
+                      evac_mode=constants.NODE_EVAC_PRI)
+
+    self.iallocator_cls.return_value.in_text = "mock in text"
+    self.iallocator_cls.return_value.out_text = "mock out text"
+
+  def testMissingDirection(self):
+    op = self.CopyOpCode(self.base_op,
+                         direction=self.REMOVE)
+
+    self.ExecOpCodeExpectOpPrereqError(
+      op, "'OP_TEST_ALLOCATOR.direction' fails validation")
+
+  def testAllocWrongDisks(self):
+    op = self.CopyOpCode(self.valid_alloc_op,
+                         disks=[0, "test"])
+
+    self.ExecOpCodeExpectOpPrereqError(op, "Invalid contents")
+
+  def testAllocWithExistingInstance(self):
+    inst = self.cfg.AddNewInstance()
+    op = self.CopyOpCode(self.valid_alloc_op, name=inst.name)
+
+    self.ExecOpCodeExpectOpPrereqError(op, "already in the cluster")
+
+  def testAllocMultiAllocMissingIAllocator(self):
+    for mode in [constants.IALLOCATOR_MODE_ALLOC,
+                 constants.IALLOCATOR_MODE_MULTI_ALLOC]:
+      op = self.CopyOpCode(self.base_op,
+                           mode=mode,
+                           iallocator=None)
+
+      self.ResetMocks()
+      self.ExecOpCodeExpectOpPrereqError(op, "Missing allocator name")
+
+  def testChgGroupNodeEvacMissingInstances(self):
+    for mode in [constants.IALLOCATOR_MODE_CHG_GROUP,
+                 constants.IALLOCATOR_MODE_NODE_EVAC]:
+      op = self.CopyOpCode(self.base_op,
+                           mode=mode)
+
+      self.ResetMocks()
+      self.ExecOpCodeExpectOpPrereqError(op, "Missing instances")
+
+  def testAlloc(self):
+    op = self.valid_alloc_op
+
+    self.ExecOpCode(op)
+
+    assert self.iallocator_cls.call_count == 1
+    self.iallocator_cls.return_value.Run \
+      .assert_called_once_with("test", validate=False)
+
+  def testReloc(self):
+    op = self.valid_reloc_op
+    self.cfg.AddNewInstance(name=op.name)
+
+    self.ExecOpCode(op)
+
+    assert self.iallocator_cls.call_count == 1
+    self.iallocator_cls.return_value.Run \
+      .assert_called_once_with("test", validate=False)
+
+  def testChgGroup(self):
+    op = self.valid_chg_group_op
+    for inst_name in op.instances:
+      self.cfg.AddNewInstance(name=inst_name)
+
+    self.ExecOpCode(op)
+
+    assert self.iallocator_cls.call_count == 1
+    self.iallocator_cls.return_value.Run \
+      .assert_called_once_with("test", validate=False)
+
+  def testNodeEvac(self):
+    op = self.valid_node_evac_op
+    for inst_name in op.instances:
+      self.cfg.AddNewInstance(name=inst_name)
+
+    self.ExecOpCode(op)
+
+    assert self.iallocator_cls.call_count == 1
+    self.iallocator_cls.return_value.Run \
+      .assert_called_once_with("test", validate=False)
+
+  def testMultiAlloc(self):
+    op = self.valid_multi_alloc_op
+
+    self.ExecOpCode(op)
+
+    assert self.iallocator_cls.call_count == 1
+    self.iallocator_cls.return_value.Run \
+      .assert_called_once_with("test", validate=False)
+
+  def testAllocDirectionIn(self):
+    op = self.CopyOpCode(self.valid_alloc_op,
+                         direction=constants.IALLOCATOR_DIR_IN)
+
+    self.ExecOpCode(op)
+
+
+if __name__ == "__main__":
+  testutils.GanetiTestProgram()
diff --git a/test/py/cmdlib/testsupport/__init__.py b/test/py/cmdlib/testsupport/__init__.py
new file mode 100644 (file)
index 0000000..ec9e226
--- /dev/null
@@ -0,0 +1,50 @@
+#
+#
+
+# Copyright (C) 2013 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.
+
+
+"""Support classes and functions for testing the cmdlib module.
+
+"""
+
+from cmdlib.testsupport.cmdlib_testcase import CmdlibTestCase, \
+  withLockedLU
+from cmdlib.testsupport.config_mock import ConfigMock
+from cmdlib.testsupport.iallocator_mock import patchIAllocator
+from cmdlib.testsupport.utils_mock import patchUtils
+from cmdlib.testsupport.lock_manager_mock import LockManagerMock
+from cmdlib.testsupport.netutils_mock import patchNetutils, HostnameMock
+from cmdlib.testsupport.processor_mock import ProcessorMock
+from cmdlib.testsupport.rpc_runner_mock import CreateRpcRunnerMock, \
+  RpcResultsBuilder
+from cmdlib.testsupport.ssh_mock import patchSsh
+
+__all__ = ["CmdlibTestCase",
+           "withLockedLU",
+           "ConfigMock",
+           "CreateRpcRunnerMock",
+           "HostnameMock",
+           "patchIAllocator",
+           "patchUtils",
+           "patchNetutils",
+           "patchSsh",
+           "LockManagerMock",
+           "ProcessorMock",
+           "RpcResultsBuilder",
+           ]
diff --git a/test/py/cmdlib/testsupport/cmdlib_testcase.py b/test/py/cmdlib/testsupport/cmdlib_testcase.py
new file mode 100644 (file)
index 0000000..5c91f73
--- /dev/null
@@ -0,0 +1,417 @@
+#
+#
+
+# Copyright (C) 2013 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.
+
+
+"""Main module of the cmdlib test framework"""
+
+
+import inspect
+import mock
+import re
+import traceback
+
+from cmdlib.testsupport.config_mock import ConfigMock
+from cmdlib.testsupport.iallocator_mock import patchIAllocator
+from cmdlib.testsupport.lock_manager_mock import LockManagerMock
+from cmdlib.testsupport.netutils_mock import patchNetutils, \
+  SetupDefaultNetutilsMock
+from cmdlib.testsupport.processor_mock import ProcessorMock
+from cmdlib.testsupport.rpc_runner_mock import CreateRpcRunnerMock, \
+  RpcResultsBuilder, patchRpc, SetupDefaultRpcModuleMock
+from cmdlib.testsupport.ssh_mock import patchSsh
+
+from ganeti.cmdlib.base import LogicalUnit
+from ganeti import errors
+from ganeti import locking
+from ganeti import objects
+from ganeti import opcodes
+from ganeti import runtime
+
+import testutils
+
+
+class GanetiContextMock(object):
+  # pylint: disable=W0212
+  cfg = property(fget=lambda self: self._test_case.cfg)
+  # pylint: disable=W0212
+  glm = property(fget=lambda self: self._test_case.glm)
+  # pylint: disable=W0212
+  rpc = property(fget=lambda self: self._test_case.rpc)
+
+  def __init__(self, test_case):
+    self._test_case = test_case
+
+  def AddNode(self, node, ec_id):
+    self._test_case.cfg.AddNode(node, ec_id)
+    self._test_case.glm.add(locking.LEVEL_NODE, node.uuid)
+    self._test_case.glm.add(locking.LEVEL_NODE_RES, node.uuid)
+
+  def ReaddNode(self, node):
+    pass
+
+  def RemoveNode(self, node):
+    self._test_case.cfg.RemoveNode(node.uuid)
+    self._test_case.glm.remove(locking.LEVEL_NODE, node.uuid)
+    self._test_case.glm.remove(locking.LEVEL_NODE_RES, node.uuid)
+
+
+class MockLU(LogicalUnit):
+  def BuildHooksNodes(self):
+    pass
+
+  def BuildHooksEnv(self):
+    pass
+
+
+# pylint: disable=R0904
+class CmdlibTestCase(testutils.GanetiTestCase):
+  """Base class for cmdlib tests.
+
+  This class sets up a mocked environment for the execution of
+  L{ganeti.cmdlib.base.LogicalUnit} subclasses.
+
+  The environment can be customized via the following fields:
+
+    * C{cfg}: @see L{ConfigMock}
+    * C{glm}: @see L{LockManagerMock}
+    * C{rpc}: @see L{CreateRpcRunnerMock}
+    * C{iallocator_cls}: @see L{patchIAllocator}
+    * C{mcpu}: @see L{ProcessorMock}
+    * C{netutils_mod}: @see L{patchNetutils}
+    * C{ssh_mod}: @see L{patchSsh}
+
+  """
+
+  REMOVE = object()
+
+  cluster = property(fget=lambda self: self.cfg.GetClusterInfo(),
+                     doc="Cluster configuration object")
+  master = property(fget=lambda self: self.cfg.GetMasterNodeInfo(),
+                    doc="Master node")
+  master_uuid = property(fget=lambda self: self.cfg.GetMasterNode(),
+                         doc="Master node UUID")
+  # pylint: disable=W0212
+  group = property(fget=lambda self: self._GetDefaultGroup(),
+                   doc="Default node group")
+
+  os = property(fget=lambda self: self.cfg.GetDefaultOs(),
+                doc="Default OS")
+  os_name_variant = property(
+    fget=lambda self: self.os.name + objects.OS.VARIANT_DELIM +
+      self.os.supported_variants[0],
+    doc="OS name and variant string")
+
+  def setUp(self):
+    super(CmdlibTestCase, self).setUp()
+    self._iallocator_patcher = None
+    self._netutils_patcher = None
+    self._ssh_patcher = None
+    self._rpc_patcher = None
+
+    try:
+      runtime.InitArchInfo()
+    except errors.ProgrammerError:
+      # during tests, the arch info can be initialized multiple times
+      pass
+
+    self.ResetMocks()
+
+  def _StopPatchers(self):
+    if self._iallocator_patcher is not None:
+      self._iallocator_patcher.stop()
+      self._iallocator_patcher = None
+    if self._netutils_patcher is not None:
+      self._netutils_patcher.stop()
+      self._netutils_patcher = None
+    if self._ssh_patcher is not None:
+      self._ssh_patcher.stop()
+      self._ssh_patcher = None
+    if self._rpc_patcher is not None:
+      self._rpc_patcher.stop()
+      self._rpc_patcher = None
+
+  def tearDown(self):
+    super(CmdlibTestCase, self).tearDown()
+
+    self._StopPatchers()
+
+  def _GetTestModule(self):
+    module = inspect.getsourcefile(self.__class__).split("/")[-1]
+    suffix = "_unittest.py"
+    assert module.endswith(suffix), "Naming convention for cmdlib test" \
+                                    " modules is: <module>%s (found '%s')"\
+                                    % (suffix, module)
+    return module[:-len(suffix)]
+
+  def ResetMocks(self):
+    """Resets all mocks back to their initial state.
+
+    This is useful if you want to execute more than one opcode in a single
+    test.
+
+    """
+    self.cfg = ConfigMock()
+    self.glm = LockManagerMock()
+    self.rpc = CreateRpcRunnerMock()
+    self.ctx = GanetiContextMock(self)
+    self.mcpu = ProcessorMock(self.ctx)
+
+    self._StopPatchers()
+    try:
+      self._iallocator_patcher = patchIAllocator(self._GetTestModule())
+      self.iallocator_cls = self._iallocator_patcher.start()
+    except (ImportError, AttributeError):
+      # this test module does not use iallocator, no patching performed
+      self._iallocator_patcher = None
+
+    try:
+      self._netutils_patcher = patchNetutils(self._GetTestModule())
+      self.netutils_mod = self._netutils_patcher.start()
+      SetupDefaultNetutilsMock(self.netutils_mod, self.cfg)
+    except (ImportError, AttributeError):
+      # this test module does not use netutils, no patching performed
+      self._netutils_patcher = None
+
+    try:
+      self._ssh_patcher = patchSsh(self._GetTestModule())
+      self.ssh_mod = self._ssh_patcher.start()
+    except (ImportError, AttributeError):
+      # this test module does not use ssh, no patching performed
+      self._ssh_patcher = None
+
+    try:
+      self._rpc_patcher = patchRpc(self._GetTestModule())
+      self.rpc_mod = self._rpc_patcher.start()
+      SetupDefaultRpcModuleMock(self.rpc_mod)
+    except (ImportError, AttributeError):
+      # this test module does not use rpc, no patching performed
+      self._rpc_patcher = None
+
+  def GetMockLU(self):
+    """Creates a mock L{LogialUnit} with access to the mocked config etc.
+
+    @rtype: L{LogialUnit}
+    @return: A mock LU
+
+    """
+    return MockLU(self.mcpu, mock.MagicMock(), self.ctx, self.rpc)
+
+  def RpcResultsBuilder(self, use_node_names=False):
+    """Creates a pre-configured L{RpcResultBuilder}
+
+    @type use_node_names: bool
+    @param use_node_names: @see L{RpcResultBuilder}
+    @rtype: L{RpcResultBuilder}
+    @return: a pre-configured builder for RPC results
+
+    """
+    return RpcResultsBuilder(cfg=self.cfg, use_node_names=use_node_names)
+
+  def ExecOpCode(self, opcode):
+    """Executes the given opcode.
+
+    @param opcode: the opcode to execute
+    @return: the result of the LU's C{Exec} method
+
+    """
+    self.glm.AddLocksFromConfig(self.cfg)
+
+    return self.mcpu.ExecOpCodeAndRecordOutput(opcode)
+
+  def ExecOpCodeExpectException(self, opcode,
+                                expected_exception,
+                                expected_regex=None):
+    """Executes the given opcode and expects an exception.
+
+    @param opcode: @see L{ExecOpCode}
+    @type expected_exception: class
+    @param expected_exception: the exception which must be raised
+    @type expected_regex: string
+    @param expected_regex: if not C{None}, a regular expression which must be
+          present in the string representation of the exception
+
+    """
+    try:
+      self.ExecOpCode(opcode)
+    except expected_exception, e:
+      if expected_regex is not None:
+        assert re.search(expected_regex, str(e)) is not None, \
+                "Caught exception '%s' did not match '%s'" % \
+                  (str(e), expected_regex)
+    except Exception, e:
+      tb = traceback.format_exc()
+      raise AssertionError("%s\n(See original exception above)\n"
+                           "Expected exception '%s' was not raised,"
+                           " got '%s' of class '%s' instead." %
+                           (tb, expected_exception, e, e.__class__))
+    else:
+      raise AssertionError("Expected exception '%s' was not raised" %
+                           expected_exception)
+
+  def ExecOpCodeExpectOpPrereqError(self, opcode, expected_regex=None):
+    """Executes the given opcode and expects a L{errors.OpPrereqError}
+
+    @see L{ExecOpCodeExpectException}
+
+    """
+    self.ExecOpCodeExpectException(opcode, errors.OpPrereqError, expected_regex)
+
+  def ExecOpCodeExpectOpExecError(self, opcode, expected_regex=None):
+    """Executes the given opcode and expects a L{errors.OpExecError}
+
+    @see L{ExecOpCodeExpectException}
+
+    """
+    self.ExecOpCodeExpectException(opcode, errors.OpExecError, expected_regex)
+
+  def RunWithLockedLU(self, opcode, test_func):
+    """Takes the given opcode, creates a LU and runs func on it.
+
+    The passed LU did already perform locking, but no methods which actually
+    require locking are executed on the LU.
+
+    @param opcode: the opcode to get the LU for.
+    @param test_func: the function to execute with the LU as parameter.
+    @return: the result of test_func
+
+    """
+    self.glm.AddLocksFromConfig(self.cfg)
+
+    return self.mcpu.RunWithLockedLU(opcode, test_func)
+
+  def assertLogContainsMessage(self, expected_msg):
+    """Shortcut for L{ProcessorMock.assertLogContainsMessage}
+
+    """
+    self.mcpu.assertLogContainsMessage(expected_msg)
+
+  def assertLogContainsRegex(self, expected_regex):
+    """Shortcut for L{ProcessorMock.assertLogContainsRegex}
+
+    """
+    self.mcpu.assertLogContainsRegex(expected_regex)
+
+  def assertHooksCall(self, nodes, hook_path, phase,
+                      environment=None, count=None, index=0):
+    """Asserts a call to C{rpc.call_hooks_runner}
+
+    @type nodes: list of string
+    @param nodes: node UUID's or names hooks run on
+    @type hook_path: string
+    @param hook_path: path (or name) of the hook run
+    @type phase: string
+    @param phase: phase in which the hook runs in
+    @type environment: dict
+    @param environment: the environment passed to the hooks. C{None} to skip
+            asserting it
+    @type count: int
+    @param count: the number of hook invocations. C{None} to skip asserting it
+    @type index: int
+    @param index: the index of the hook invocation to assert
+
+    """
+    if count is not None:
+      self.assertEqual(count, self.rpc.call_hooks_runner.call_count)
+
+    args = self.rpc.call_hooks_runner.call_args[index]
+
+    self.assertEqual(set(nodes), set(args[0]))
+    self.assertEqual(hook_path, args[1])
+    self.assertEqual(phase, args[2])
+    if environment is not None:
+      self.assertEqual(environment, args[3])
+
+  def assertSingleHooksCall(self, nodes, hook_path, phase,
+                            environment=None):
+    """Asserts a single call to C{rpc.call_hooks_runner}
+
+    @see L{assertHooksCall} for parameter description.
+
+    """
+    self.assertHooksCall(nodes, hook_path, phase,
+                         environment=environment, count=1)
+
+  def CopyOpCode(self, opcode, **kwargs):
+    """Creates a copy of the given opcode and applies modifications to it
+
+    @type opcode: opcode.OpCode
+    @param opcode: the opcode to copy
+    @type kwargs: dict
+    @param kwargs: dictionary of fields to overwrite in the copy. The special
+          value L{REMOVE} can be used to remove fields from the copy.
+    @return: a copy of the given opcode
+
+    """
+    state = opcode.__getstate__()
+
+    for key, value in kwargs.items():
+      if value == self.REMOVE and key in state:
+        del state[key]
+      else:
+        state[key] = value
+
+    return opcodes.OpCode.LoadOpCode(state)
+
+  def _GetDefaultGroup(self):
+    for group in self.cfg.GetAllNodeGroupsInfo().values():
+      if group.name == "default":
+        return group
+    assert False
+
+
+# pylint: disable=C0103
+def withLockedLU(func):
+  """Convenience decorator which runs the decorated method with the LU.
+
+  This uses L{CmdlibTestCase.RunWithLockedLU} to run the decorated method.
+  For this to work, the opcode to run has to be an instance field named "op",
+  "_op", "opcode" or "_opcode".
+
+  If the instance has a method called "PrepareLU", this method is invoked with
+  the LU right before the test method is called.
+
+  """
+  def wrapper(*args, **kwargs):
+    test = args[0]
+    assert isinstance(test, CmdlibTestCase)
+
+    op = None
+    for attr_name in ["op", "_op", "opcode", "_opcode"]:
+      if hasattr(test, attr_name):
+        op = getattr(test, attr_name)
+        break
+    assert op is not None
+
+    prepare_fn = None
+    if hasattr(test, "PrepareLU"):
+      prepare_fn = getattr(test, "PrepareLU")
+      assert callable(prepare_fn)
+
+    # pylint: disable=W0142
+    def callWithLU(lu):
+      if prepare_fn:
+        prepare_fn(lu)
+
+      new_args = list(args)
+      new_args.append(lu)
+      func(*new_args, **kwargs)
+
+    return test.RunWithLockedLU(op, callWithLU)
+  return wrapper
diff --git a/test/py/cmdlib/testsupport/config_mock.py b/test/py/cmdlib/testsupport/config_mock.py
new file mode 100644 (file)
index 0000000..3c8812d
--- /dev/null
@@ -0,0 +1,599 @@
+#
+#
+
+# Copyright (C) 2013 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.
+
+
+"""Support for mocking the cluster configuration"""
+
+
+import time
+import uuid as uuid_module
+
+from ganeti import config
+from ganeti import constants
+from ganeti import objects
+
+import mocks
+
+
+def _StubGetEntResolver():
+  return mocks.FakeGetentResolver()
+
+
+# pylint: disable=R0904
+class ConfigMock(config.ConfigWriter):
+  """A mocked cluster configuration with added methods for easy customization.
+
+  """
+
+  def __init__(self):
+    self._cur_group_id = 1
+    self._cur_node_id = 1
+    self._cur_inst_id = 1
+    self._cur_disk_id = 1
+    self._cur_os_id = 1
+    self._cur_nic_id = 1
+    self._cur_net_id = 1
+    self._default_os = None
+
+    super(ConfigMock, self).__init__(cfg_file="/dev/null",
+                                     _getents=_StubGetEntResolver())
+
+  def _GetUuid(self):
+    return str(uuid_module.uuid4())
+
+  def _GetObjUuid(self, obj):
+    if obj is None:
+      return None
+    elif isinstance(obj, objects.ConfigObject):
+      return obj.uuid
+    else:
+      return obj
+
+  def AddNewNodeGroup(self,
+                      uuid=None,
+                      name=None,
+                      ndparams=None,
+                      diskparams=None,
+                      ipolicy=None,
+                      hv_state_static=None,
+                      disk_state_static=None,
+                      alloc_policy=None,
+                      networks=None):
+    """Add a new L{objects.NodeGroup} to the cluster configuration
+
+    See L{objects.NodeGroup} for parameter documentation.
+
+    @rtype: L{objects.NodeGroup}
+    @return: the newly added node group
+
+    """
+    group_id = self._cur_group_id
+    self._cur_group_id += 1
+
+    if uuid is None:
+      uuid = self._GetUuid()
+    if name is None:
+      name = "mock_group_%d" % group_id
+    if networks is None:
+      networks = {}
+
+    group = objects.NodeGroup(uuid=uuid,
+                              name=name,
+                              ndparams=ndparams,
+                              diskparams=diskparams,
+                              ipolicy=ipolicy,
+                              hv_state_static=hv_state_static,
+                              disk_state_static=disk_state_static,
+                              alloc_policy=alloc_policy,
+                              networks=networks,
+                              members=[])
+
+    self.AddNodeGroup(group, None)
+    return group
+
+  # pylint: disable=R0913
+  def AddNewNode(self,
+                 uuid=None,
+                 name=None,
+                 primary_ip=None,
+                 secondary_ip=None,
+                 master_candidate=True,
+                 offline=False,
+                 drained=False,
+                 group=None,
+                 master_capable=True,
+                 vm_capable=True,
+                 ndparams=None,
+                 powered=True,
+                 hv_state=None,
+                 hv_state_static=None,
+                 disk_state=None,
+                 disk_state_static=None):
+    """Add a new L{objects.Node} to the cluster configuration
+
+    See L{objects.Node} for parameter documentation.
+
+    @rtype: L{objects.Node}
+    @return: the newly added node
+
+    """
+    node_id = self._cur_node_id
+    self._cur_node_id += 1
+
+    if uuid is None:
+      uuid = self._GetUuid()
+    if name is None:
+      name = "mock_node_%d.example.com" % node_id
+    if primary_ip is None:
+      primary_ip = "192.0.2.%d" % node_id
+    if secondary_ip is None:
+      secondary_ip = "203.0.113.%d" % node_id
+    if group is None:
+      group = self._default_group.uuid
+    group = self._GetObjUuid(group)
+    if ndparams is None:
+      ndparams = {}
+
+    node = objects.Node(uuid=uuid,
+                        name=name,
+                        primary_ip=primary_ip,
+                        secondary_ip=secondary_ip,
+                        master_candidate=master_candidate,
+                        offline=offline,
+                        drained=drained,
+                        group=group,
+                        master_capable=master_capable,
+                        vm_capable=vm_capable,
+                        ndparams=ndparams,
+                        powered=powered,
+                        hv_state=hv_state,
+                        hv_state_static=hv_state_static,
+                        disk_state=disk_state,
+                        disk_state_static=disk_state_static)
+
+    self.AddNode(node, None)
+    return node
+
+  def AddNewInstance(self,
+                     uuid=None,
+                     name=None,
+                     primary_node=None,
+                     os=None,
+                     hypervisor=None,
+                     hvparams=None,
+                     beparams=None,
+                     osparams=None,
+                     admin_state=None,
+                     nics=None,
+                     disks=None,
+                     disk_template=None,
+                     disks_active=None,
+                     network_port=None,
+                     secondary_node=None):
+    """Add a new L{objects.Instance} to the cluster configuration
+
+    See L{objects.Instance} for parameter documentation.
+
+    @rtype: L{objects.Instance}
+    @return: the newly added instance
+
+    """
+    inst_id = self._cur_inst_id
+    self._cur_inst_id += 1
+
+    if uuid is None:
+      uuid = self._GetUuid()
+    if name is None:
+      name = "mock_inst_%d.example.com" % inst_id
+    if primary_node is None:
+      primary_node = self._master_node.uuid
+    primary_node = self._GetObjUuid(primary_node)
+    if os is None:
+      os = self.GetDefaultOs().name + objects.OS.VARIANT_DELIM +\
+           self.GetDefaultOs().supported_variants[0]
+    if hypervisor is None:
+      hypervisor = self.GetClusterInfo().enabled_hypervisors[0]
+    if hvparams is None:
+      hvparams = {}
+    if beparams is None:
+      beparams = {}
+    if osparams is None:
+      osparams = {}
+    if admin_state is None:
+      admin_state = constants.ADMINST_DOWN
+    if nics is None:
+      nics = [self.CreateNic()]
+    if disk_template is None:
+      if disks is None:
+        # user chose nothing, so create a plain disk for him
+        disk_template = constants.DT_PLAIN
+      elif len(disks) == 0:
+        disk_template = constants.DT_DISKLESS
+      else:
+        disk_template = disks[0].dev_type
+    if disks is None:
+      if disk_template == constants.DT_DISKLESS:
+        disks = []
+      else:
+        disks = [self.CreateDisk(dev_type=disk_template,
+                                 primary_node=primary_node,
+                                 secondary_node=secondary_node)]
+    if disks_active is None:
+      disks_active = admin_state == constants.ADMINST_UP
+
+    inst = objects.Instance(uuid=uuid,
+                            name=name,
+                            primary_node=primary_node,
+                            os=os,
+                            hypervisor=hypervisor,
+                            hvparams=hvparams,
+                            beparams=beparams,
+                            osparams=osparams,
+                            admin_state=admin_state,
+                            nics=nics,
+                            disks=disks,
+                            disk_template=disk_template,
+                            disks_active=disks_active,
+                            network_port=network_port)
+    self.AddInstance(inst, None)
+    return inst
+
+  def AddNewNetwork(self,
+                    uuid=None,
+                    name=None,
+                    mac_prefix=None,
+                    network=None,
+                    network6=None,
+                    gateway=None,
+                    gateway6=None,
+                    reservations=None,
+                    ext_reservations=None):
+    """Add a new L{objects.Network} to the cluster configuration
+
+    See L{objects.Network} for parameter documentation.
+
+    @rtype: L{objects.Network}
+    @return: the newly added network
+
+    """
+    net_id = self._cur_net_id
+    self._cur_net_id += 1
+
+    if uuid is None:
+      uuid = self._GetUuid()
+    if name is None:
+      name = "mock_net_%d" % net_id
+    if network is None:
+      network = "198.51.100.0/24"
+    if gateway is None:
+      if network[-3:] == "/24":
+        gateway = network[:-4] + "1"
+      else:
+        gateway = "198.51.100.1"
+    if network[-3:] == "/24" and gateway == network[:-4] + "1":
+      if reservations is None:
+        reservations = "0" * 256
+      if ext_reservations:
+        ext_reservations = "11" + ("0" * 253) + "1"
+    elif reservations is None or ext_reservations is None:
+      raise AssertionError("You have to specify 'reservations' and"
+                           " 'ext_reservations'!")
+
+    net = objects.Network(uuid=uuid,
+                          name=name,
+                          mac_prefix=mac_prefix,
+                          network=network,
+                          network6=network6,
+                          gateway=gateway,
+                          gateway6=gateway6,
+                          reservations=reservations,
+                          ext_reservations=ext_reservations)
+    self.AddNetwork(net, None)
+    return net
+
+  def ConnectNetworkToGroup(self, net, group, netparams=None):
+    """Connect the given network to the group.
+
+    @type net: string or L{objects.Network}
+    @param net: network object or UUID
+    @type group: string of L{objects.NodeGroup}
+    @param group: node group object of UUID
+    @type netparams: dict
+    @param netparams: network parameters for this connection
+
+    """
+    net_obj = None
+    if isinstance(net, objects.Network):
+      net_obj = net
+    else:
+      net_obj = self.GetNetwork(net)
+
+    group_obj = None
+    if isinstance(group, objects.NodeGroup):
+      group_obj = group
+    else:
+      group_obj = self.GetNodeGroup(group)
+
+    if net_obj is None or group_obj is None:
+      raise AssertionError("Failed to get network or node group")
+
+    if netparams is None:
+      netparams = {
+        constants.NIC_MODE: constants.NIC_MODE_BRIDGED,
+        constants.NIC_LINK: "br_mock"
+      }
+
+    group_obj.networks[net_obj.uuid] = netparams
+
+  def CreateDisk(self,
+                 uuid=None,
+                 name=None,
+                 dev_type=constants.DT_PLAIN,
+                 logical_id=None,
+                 children=None,
+                 iv_name=None,
+                 size=1024,
+                 mode=constants.DISK_RDWR,
+                 params=None,
+                 spindles=None,
+                 primary_node=None,
+                 secondary_node=None,
+                 create_nodes=False,
+                 instance_disk_index=0):
+    """Create a new L{objecs.Disk} object
+
+    @rtype: L{objects.Disk}
+    @return: the newly create disk object
+
+    """
+    disk_id = self._cur_disk_id
+    self._cur_disk_id += 1
+
+    if uuid is None:
+      uuid = self._GetUuid()
+    if name is None:
+      name = "mock_disk_%d" % disk_id
+
+    if dev_type == constants.DT_DRBD8:
+      pnode_uuid = self._GetObjUuid(primary_node)
+      snode_uuid = self._GetObjUuid(secondary_node)
+      if logical_id is not None:
+        pnode_uuid = logical_id[0]
+        snode_uuid = logical_id[1]
+
+      if pnode_uuid is None and create_nodes:
+        pnode_uuid = self.AddNewNode().uuid
+      if snode_uuid is None and create_nodes:
+        snode_uuid = self.AddNewNode().uuid
+
+      if pnode_uuid is None or snode_uuid is None:
+        raise AssertionError("Trying to create DRBD disk without nodes!")
+
+      if logical_id is None:
+        logical_id = (pnode_uuid, snode_uuid,
+                      constants.FIRST_DRBD_PORT + disk_id,
+                      disk_id, disk_id, "mock_secret")
+      if children is None:
+        data_child = self.CreateDisk(dev_type=constants.DT_PLAIN,
+                                     size=size)
+        meta_child = self.CreateDisk(dev_type=constants.DT_PLAIN,
+                                     size=constants.DRBD_META_SIZE)
+        children = [data_child, meta_child]
+    elif dev_type == constants.DT_PLAIN:
+      if logical_id is None:
+        logical_id = ("mockvg", "mock_disk_%d" % disk_id)
+    elif dev_type in (constants.DT_FILE, constants.DT_SHARED_FILE):
+      if logical_id is None:
+        logical_id = (constants.FD_LOOP, "/file/storage/disk%d" % disk_id)
+    elif dev_type == constants.DT_BLOCK:
+      if logical_id is None:
+        logical_id = (constants.BLOCKDEV_DRIVER_MANUAL,
+                      "/dev/disk/disk%d" % disk_id)
+    elif logical_id is None:
+      raise NotImplementedError
+    if children is None:
+      children = []
+    if iv_name is None:
+      iv_name = "disk/%d" % instance_disk_index
+    if params is None:
+      params = {}
+
+    return objects.Disk(uuid=uuid,
+                        name=name,
+                        dev_type=dev_type,
+                        logical_id=logical_id,
+                        children=children,
+                        iv_name=iv_name,
+                        size=size,
+                        mode=mode,
+                        params=params,
+                        spindles=spindles)
+
+  def GetDefaultOs(self):
+    if self._default_os is None:
+      self._default_os = self.CreateOs(name="mocked_os")
+    return self._default_os
+
+  def CreateOs(self,
+               name=None,
+               path=None,
+               api_versions=None,
+               create_script=None,
+               export_script=None,
+               import_script=None,
+               rename_script=None,
+               verify_script=None,
+               supported_variants=None,
+               supported_parameters=None):
+    """Create a new L{objects.OS} object
+
+    @rtype: L{object.OS}
+    @return: the newly create OS objects
+
+    """
+    os_id = self._cur_os_id
+    self._cur_os_id += 1
+
+    if name is None:
+      name = "mock_os_%d" % os_id
+    if path is None:
+      path = "/mocked/path/%d" % os_id
+    if api_versions is None:
+      api_versions = [constants.OS_API_V20]
+    if create_script is None:
+      create_script = "mock_create.sh"
+    if export_script is None:
+      export_script = "mock_export.sh"
+    if import_script is None:
+      import_script = "mock_import.sh"
+    if rename_script is None:
+      rename_script = "mock_rename.sh"
+    if verify_script is None:
+      verify_script = "mock_verify.sh"
+    if supported_variants is None:
+      supported_variants = ["default"]
+    if supported_parameters is None:
+      supported_parameters = ["mock_param"]
+
+    return objects.OS(name=name,
+                      path=path,
+                      api_versions=api_versions,
+                      create_script=create_script,
+                      export_script=export_script,
+                      import_script=import_script,
+                      rename_script=rename_script,
+                      verify_script=verify_script,
+                      supported_variants=supported_variants,
+                      supported_parameters=supported_parameters)
+
+  def CreateNic(self,
+                uuid=None,
+                name=None,
+                mac=None,
+                ip=None,
+                network=None,
+                nicparams=None,
+                netinfo=None):
+    """Create a new L{objecs.NIC} object
+
+    @rtype: L{objects.NIC}
+    @return: the newly create NIC object
+
+    """
+    nic_id = self._cur_nic_id
+    self._cur_nic_id += 1
+
+    if uuid is None:
+      uuid = self._GetUuid()
+    if name is None:
+      name = "mock_nic_%d" % nic_id
+    if mac is None:
+      mac = "aa:00:00:aa:%02x:%02x" % (nic_id / 0xff, nic_id % 0xff)
+    if isinstance(network, objects.Network):
+      network = network.uuid
+    if nicparams is None:
+      nicparams = {}
+
+    return objects.NIC(uuid=uuid,
+                       name=name,
+                       mac=mac,
+                       ip=ip,
+                       network=network,
+                       nicparams=nicparams,
+                       netinfo=netinfo)
+
+  def SetEnabledDiskTemplates(self, enabled_disk_templates):
+    """Set the enabled disk templates in the cluster.
+
+    This also takes care of required IPolicy updates.
+
+    @type enabled_disk_templates: list of string
+    @param enabled_disk_templates: list of disk templates to enable
+
+    """
+    cluster = self.GetClusterInfo()
+    cluster.enabled_disk_templates = list(enabled_disk_templates)
+    cluster.ipolicy[constants.IPOLICY_DTS] = list(enabled_disk_templates)
+
+  def _OpenConfig(self, accept_foreign):
+    self._config_data = objects.ConfigData(
+      version=constants.CONFIG_VERSION,
+      cluster=None,
+      nodegroups={},
+      nodes={},
+      instances={},
+      networks={})
+
+    master_node_uuid = self._GetUuid()
+
+    self._cluster = objects.Cluster(
+      serial_no=1,
+      rsahostkeypub="",
+      highest_used_port=(constants.FIRST_DRBD_PORT - 1),
+      tcpudp_port_pool=set(),
+      mac_prefix="aa:00:00",
+      volume_group_name="xenvg",
+      reserved_lvs=None,
+      drbd_usermode_helper="/bin/true",
+      master_node=master_node_uuid,
+      master_ip="192.0.2.254",
+      master_netdev=constants.DEFAULT_BRIDGE,
+      master_netmask=None,
+      use_external_mip_script=None,
+      cluster_name="cluster.example.com",
+      file_storage_dir="/tmp",
+      shared_file_storage_dir=None,
+      enabled_hypervisors=[constants.HT_XEN_HVM, constants.HT_XEN_PVM,
+                           constants.HT_KVM],
+      hvparams=constants.HVC_DEFAULTS.copy(),
+      ipolicy=None,
+      os_hvp={self.GetDefaultOs().name: constants.HVC_DEFAULTS.copy()},
+      beparams=None,
+      osparams=None,
+      nicparams={constants.PP_DEFAULT: constants.NICC_DEFAULTS},
+      ndparams=None,
+      diskparams=None,
+      candidate_pool_size=3,
+      modify_etc_hosts=False,
+      modify_ssh_setup=False,
+      maintain_node_health=False,
+      uid_pool=None,
+      default_iallocator="mock_iallocator",
+      hidden_os=None,
+      blacklisted_os=None,
+      primary_ip_family=None,
+      prealloc_wipe_disks=None,
+      enabled_disk_templates=list(constants.DISK_TEMPLATE_PREFERENCE),
+      )
+    self._cluster.ctime = self._cluster.mtime = time.time()
+    self._cluster.UpgradeConfig()
+    self._config_data.cluster = self._cluster
+
+    self._default_group = self.AddNewNodeGroup(name="default")
+    self._master_node = self.AddNewNode(uuid=master_node_uuid)
+
+  def _WriteConfig(self, destination=None, feedback_fn=None):
+    pass
+
+  def _DistributeConfig(self, feedback_fn):
+    pass
+
+  def _GetRpc(self, address_list):
+    raise AssertionError("This should not be used during tests!")
diff --git a/test/py/cmdlib/testsupport/iallocator_mock.py b/test/py/cmdlib/testsupport/iallocator_mock.py
new file mode 100644 (file)
index 0000000..bea4939
--- /dev/null
@@ -0,0 +1,39 @@
+#
+#
+
+# Copyright (C) 2013 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.
+
+
+"""Support for mocking the IAllocator interface"""
+
+
+from cmdlib.testsupport.util import patchModule
+
+
+# pylint: disable=C0103
+def patchIAllocator(module_under_test):
+  """Patches the L{ganeti.masterd.iallocator.IAllocator} class for tests.
+
+  This function is meant to be used as a decorator for test methods.
+
+  @type module_under_test: string
+  @param module_under_test: the module within cmdlib which is tested. The
+        "ganeti.cmdlib" prefix is optional.
+
+  """
+  return patchModule(module_under_test, "iallocator.IAllocator")
diff --git a/test/py/cmdlib/testsupport/lock_manager_mock.py b/test/py/cmdlib/testsupport/lock_manager_mock.py
new file mode 100644 (file)
index 0000000..068086e
--- /dev/null
@@ -0,0 +1,57 @@
+#
+#
+
+# Copyright (C) 2013 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.
+
+
+"""Support for mocking the lock manager"""
+
+
+from ganeti import locking
+
+
+class LockManagerMock(locking.GanetiLockManager):
+  """Mocked lock manager for tests.
+
+  """
+  def __init__(self):
+    # reset singleton instance, there is a separate lock manager for every test
+    # pylint: disable=W0212
+    self.__class__._instance = None
+
+    super(LockManagerMock, self).__init__([], [], [], [])
+
+  def AddLocksFromConfig(self, cfg):
+    """Create locks for all entities in the given configuration.
+
+    @type cfg: ganeti.config.ConfigWriter
+    """
+    try:
+      self.acquire(locking.LEVEL_CLUSTER, locking.BGL)
+
+      for node_uuid in cfg.GetNodeList():
+        self.add(locking.LEVEL_NODE, node_uuid)
+        self.add(locking.LEVEL_NODE_RES, node_uuid)
+      for group_uuid in cfg.GetNodeGroupList():
+        self.add(locking.LEVEL_NODEGROUP, group_uuid)
+      for inst in cfg.GetAllInstancesInfo().values():
+        self.add(locking.LEVEL_INSTANCE, inst.name)
+      for net_uuid in cfg.GetNetworkList():
+        self.add(locking.LEVEL_NETWORK, net_uuid)
+    finally:
+      self.release(locking.LEVEL_CLUSTER, locking.BGL)
diff --git a/test/py/cmdlib/testsupport/netutils_mock.py b/test/py/cmdlib/testsupport/netutils_mock.py
new file mode 100644 (file)
index 0000000..d5b28d3
--- /dev/null
@@ -0,0 +1,116 @@
+#
+#
+
+# Copyright (C) 2013 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.
+
+
+"""Support for mocking the netutils module"""
+
+import mock
+
+from ganeti import compat
+from ganeti import netutils
+from cmdlib.testsupport.util import patchModule
+
+
+# pylint: disable=C0103
+def patchNetutils(module_under_test):
+  """Patches the L{ganeti.netutils} module for tests.
+
+  This function is meant to be used as a decorator for test methods.
+
+  @type module_under_test: string
+  @param module_under_test: the module within cmdlib which is tested. The
+        "ganeti.cmdlib" prefix is optional.
+
+  """
+  return patchModule(module_under_test, "netutils")
+
+
+class HostnameMock(object):
+  """Simple mocked version of L{netutils.Hostname}.
+
+  """
+  def __init__(self, name, ip):
+    self.name = name
+    self.ip = ip
+
+
+def _IsOverwrittenReturnValue(value):
+  return value is not None and value != mock.DEFAULT and \
+      not isinstance(value, mock.Mock)
+
+
+# pylint: disable=W0613
+def _GetHostnameMock(cfg, mock_fct, name=None, family=None):
+  if _IsOverwrittenReturnValue(mock_fct.return_value):
+    return mock.DEFAULT
+
+  if name is None:
+    name = cfg.GetMasterNodeName()
+
+  if name == cfg.GetClusterName():
+    cluster = cfg.GetClusterInfo()
+    return HostnameMock(cluster.cluster_name, cluster.master_ip)
+
+  node = cfg.GetNodeInfoByName(name)
+  if node is not None:
+    return HostnameMock(node.name, node.primary_ip)
+
+  return HostnameMock(name, "203.0.113.253")
+
+
+# pylint: disable=W0613
+def _TcpPingMock(cfg, mock_fct, target, port, timeout=None,
+                 live_port_needed=None, source=None):
+  if _IsOverwrittenReturnValue(mock_fct.return_value):
+    return mock.DEFAULT
+
+  if target == cfg.GetClusterName():
+    return True
+  if cfg.GetNodeInfoByName(target) is not None:
+    return True
+  if target in [node.primary_ip for node in cfg.GetAllNodesInfo().values()]:
+    return True
+  if target in [node.secondary_ip for node in cfg.GetAllNodesInfo().values()]:
+    return True
+  return False
+
+
+def SetupDefaultNetutilsMock(netutils_mod, cfg):
+  """Configures the given netutils_mod mock to work with the given config.
+
+  All relevant functions in netutils_mod are stubbed in such a way that they
+  are consistent with the configuration.
+
+  @param netutils_mod: the mock module to configure
+  @type cfg: cmdlib.testsupport.ConfigMock
+  @param cfg: the configuration to query for netutils request
+
+  """
+  netutils_mod.GetHostname.side_effect = \
+    compat.partial(_GetHostnameMock, cfg, netutils_mod.GetHostname)
+  netutils_mod.TcpPing.side_effect = \
+    compat.partial(_TcpPingMock, cfg, netutils_mod.TcpPing)
+  netutils_mod.GetDaemonPort.side_effect = netutils.GetDaemonPort
+  netutils_mod.FormatAddress.side_effect = netutils.FormatAddress
+  netutils_mod.Hostname.GetNormalizedName.side_effect = \
+    netutils.Hostname.GetNormalizedName
+  netutils_mod.IPAddress = netutils.IPAddress
+  netutils_mod.IP4Address = netutils.IP4Address
+  netutils_mod.IP6Address = netutils.IP6Address
diff --git a/test/py/cmdlib/testsupport/processor_mock.py b/test/py/cmdlib/testsupport/processor_mock.py
new file mode 100644 (file)
index 0000000..5d39b47
--- /dev/null
@@ -0,0 +1,228 @@
+#
+#
+
+# Copyright (C) 2013 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.
+
+
+"""Support for mocking the opcode processor"""
+
+
+import re
+
+from ganeti import constants
+from ganeti import mcpu
+
+
+class LogRecordingCallback(mcpu.OpExecCbBase):
+  """Helper class for log output recording.
+
+  """
+  def __init__(self, processor):
+    self.processor = processor
+
+  def Feedback(self, *args):
+    assert len(args) < 3
+
+    if len(args) == 1:
+      log_type = constants.ELOG_MESSAGE
+      log_msg = args[0]
+    else:
+      (log_type, log_msg) = args
+
+    self.processor.log_entries.append((log_type, log_msg))
+
+  def SubmitManyJobs(self, jobs):
+    results = []
+    for idx, _ in enumerate(jobs):
+      results.append((True, idx))
+    return results
+
+
+class ProcessorMock(mcpu.Processor):
+  """Mocked opcode processor for tests.
+
+  This class actually performs much more than a mock, as it drives the
+  execution of LU's. But it also provides access to the log output of the LU
+  the result of the execution.
+
+  See L{ExecOpCodeAndRecordOutput} for the main method of this class.
+
+  """
+
+  def __init__(self, context):
+    super(ProcessorMock, self).__init__(context, 1, True)
+    self.log_entries = []
+    self._lu_test_func = None
+
+  def ExecOpCodeAndRecordOutput(self, op):
+    """Executes the given opcode and records the output for further inspection.
+
+    @param op: the opcode to execute.
+    @return: see L{mcpu.Processor.ExecOpCode}
+
+    """
+    return self.ExecOpCode(op, LogRecordingCallback(self))
+
+  def _ExecLU(self, lu):
+    # pylint: disable=W0212
+    if not self._lu_test_func:
+      return super(ProcessorMock, self)._ExecLU(lu)
+    else:
+      # required by a lot LU's, and usually passed in Exec
+      lu._feedback_fn = self.Log
+      return self._lu_test_func(lu)
+
+  def _CheckLUResult(self, op, result):
+    # pylint: disable=W0212
+    if not self._lu_test_func:
+      return super(ProcessorMock, self)._CheckLUResult(op, result)
+    else:
+      pass
+
+  def RunWithLockedLU(self, op, func):
+    """Takes the given opcode, creates a LU and runs func with it.
+
+    @param op: the opcode to get the LU for.
+    @param func: the function to run with the created and locked LU.
+    @return: the result of func.
+
+    """
+    self._lu_test_func = func
+    try:
+      return self.ExecOpCodeAndRecordOutput(op)
+    finally:
+      self._lu_test_func = None
+
+  def GetLogEntries(self):
+    """Return the list of recorded log entries.
+
+    @rtype: list of (string, string) tuples
+    @return: the list of recorded log entries
+
+    """
+    return self.log_entries
+
+  def GetLogMessages(self):
+    """Return the list of recorded log messages.
+
+    @rtype: list of string
+    @return: the list of recorded log messages
+
+    """
+    return [msg for _, msg in self.log_entries]
+
+  def GetLogEntriesString(self):
+    """Return a string with all log entries separated by a newline.
+
+    """
+    return "\n".join("%s: %s" % (log_type, msg)
+                     for log_type, msg in self.GetLogEntries())
+
+  def GetLogMessagesString(self):
+    """Return a string with all log messages separated by a newline.
+
+    """
+    return "\n".join("%s" % msg for _, msg in self.GetLogEntries())
+
+  def assertLogContainsEntry(self, expected_type, expected_msg):
+    """Asserts that the log contains the exact given entry.
+
+    @type expected_type: string
+    @param expected_type: the expected type
+    @type expected_msg: string
+    @param expected_msg: the expected message
+
+    """
+    for log_type, msg in self.log_entries:
+      if log_type == expected_type and msg == expected_msg:
+        return
+
+    raise AssertionError(
+      "Could not find '%s' (type '%s') in LU log messages. Log is:\n%s" %
+      (expected_msg, expected_type, self.GetLogEntriesString()))
+
+  def assertLogContainsMessage(self, expected_msg):
+    """Asserts that the log contains the exact given message.
+
+    @type expected_msg: string
+    @param expected_msg: the expected message
+
+    """
+    for msg in self.GetLogMessages():
+      if msg == expected_msg:
+        return
+
+    raise AssertionError(
+      "Could not find '%s' in LU log messages. Log is:\n%s" %
+      (expected_msg, self.GetLogMessagesString()))
+
+  def assertLogContainsRegex(self, expected_regex):
+    """Asserts that the log contains a message which matches the regex.
+
+    @type expected_regex: string
+    @param expected_regex: regular expression to match messages with.
+
+    """
+    for msg in self.GetLogMessages():
+      if re.search(expected_regex, msg) is not None:
+        return
+
+    raise AssertionError(
+      "Could not find '%s' in LU log messages. Log is:\n%s" %
+      (expected_regex, self.GetLogMessagesString())
+    )
+
+  def assertLogContainsInLine(self, expected):
+    """Asserts that the log contains a message which contains a string.
+
+    @type expected: string
+    @param expected: string to search in messages.
+
+    """
+    self.assertLogContainsRegex(re.escape(expected))
+
+  def assertLogDoesNotContainRegex(self, expected_regex):
+    """Asserts that the log does not contain a message which matches the regex.
+
+    @type expected_regex: string
+    @param expected_regex: regular expression to match messages with.
+
+    """
+    for msg in self.GetLogMessages():
+      if re.search(expected_regex, msg) is not None:
+        raise AssertionError(
+          "Found '%s' in LU log messages. Log is:\n%s" %
+          (expected_regex, self.GetLogMessagesString())
+        )
+
+  def assertLogIsEmpty(self):
+    """Asserts that the log does not contain any message.
+
+    """
+    if len(self.GetLogMessages()) > 0:
+      raise AssertionError("Log is not empty. Log is:\n%s" %
+                           self.GetLogMessagesString())
+
+  def ClearLogMessages(self):
+    """Clears all recorded log messages.
+
+    This is useful if you use L{GetLockedLU} and want to test multiple calls
+    on it.
+
+    """
+    self.log_entries = []
diff --git a/test/py/cmdlib/testsupport/rpc_runner_mock.py b/test/py/cmdlib/testsupport/rpc_runner_mock.py
new file mode 100644 (file)
index 0000000..8238566
--- /dev/null
@@ -0,0 +1,208 @@
+#
+#
+
+# Copyright (C) 2013 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.
+
+
+"""Support for mocking the RPC runner"""
+
+
+import mock
+
+from ganeti import objects
+from ganeti import rpc
+
+from cmdlib.testsupport.util import patchModule
+
+
+def CreateRpcRunnerMock():
+  """Creates a new L{mock.MagicMock} tailored for L{rpc.RpcRunner}
+
+  """
+  ret = mock.MagicMock(spec=rpc.RpcRunner)
+  return ret
+
+
+class RpcResultsBuilder(object):
+  """Helper class which assists in constructing L{rpc.RpcResult} objects.
+
+  This class provides some convenience methods for constructing L{rpc.RpcResult}
+  objects. It is possible to create single results with the C{Create*} methods
+  or to create multi-node results by repeatedly calling the C{Add*} methods and
+  then obtaining the final result with C{Build}.
+
+  The C{node} parameter of all the methods can either be a L{objects.Node}
+  object, a node UUID or a node name. You have to provide the cluster config
+  in the constructor if you want to use node UUID's/names.
+
+  A typical usage of this class is as follows::
+
+    self.rpc.call_some_rpc.return_value = \
+      RpcResultsBuilder(cfg=self.cfg) \
+        .AddSuccessfulNode(node1,
+                           {
+                             "result_key": "result_data",
+                             "another_key": "other_data",
+                           }) \
+        .AddErrorNode(node2) \
+        .Build()
+
+  """
+
+  def __init__(self, cfg=None, use_node_names=False):
+    """Constructor.
+
+    @type cfg: L{ganeti.config.ConfigWriter}
+    @param cfg: used to resolve nodes if not C{None}
+    @type use_node_names: bool
+    @param use_node_names: if set to C{True}, the node field in the RPC results
+          will contain the node name instead of the node UUID.
+    """
+    self._cfg = cfg
+    self._use_node_names = use_node_names
+    self._results = []
+
+  def _GetNode(self, node_id):
+    if isinstance(node_id, objects.Node):
+      return node_id
+
+    node = None
+    if self._cfg is not None:
+      node = self._cfg.GetNodeInfo(node_id)
+      if node is None:
+        node = self._cfg.GetNodeInfoByName(node_id)
+
+    assert node is not None, "Failed to find '%s' in configuration" % node_id
+    return node
+
+  def _GetNodeId(self, node_id):
+    node = self._GetNode(node_id)
+    if self._use_node_names:
+      return node.name
+    else:
+      return node.uuid
+
+  def CreateSuccessfulNodeResult(self, node, data=None):
+    """@see L{RpcResultsBuilder}
+
+    @param node: @see L{RpcResultsBuilder}.
+    @type data: dict
+    @param data: the data as returned by the RPC
+    @rtype: L{rpc.RpcResult}
+    """
+    if data is None:
+      data = {}
+    return rpc.RpcResult(data=(True, data), node=self._GetNodeId(node))
+
+  def CreateFailedNodeResult(self, node):
+    """@see L{RpcResultsBuilder}
+
+    @param node: @see L{RpcResultsBuilder}.
+    @rtype: L{rpc.RpcResult}
+    """
+    return rpc.RpcResult(failed=True, node=self._GetNodeId(node))
+
+  def CreateOfflineNodeResult(self, node):
+    """@see L{RpcResultsBuilder}
+
+    @param node: @see L{RpcResultsBuilder}.
+    @rtype: L{rpc.RpcResult}
+    """
+    return rpc.RpcResult(failed=True, offline=True, node=self._GetNodeId(node))
+
+  def CreateErrorNodeResult(self, node, error_msg=None):
+    """@see L{RpcResultsBuilder}
+
+    @param node: @see L{RpcResultsBuilder}.
+    @type error_msg: string
+    @param error_msg: the error message as returned by the RPC
+    @rtype: L{rpc.RpcResult}
+    """
+    return rpc.RpcResult(data=(False, error_msg), node=self._GetNodeId(node))
+
+  def AddSuccessfulNode(self, node, data=None):
+    """@see L{CreateSuccessfulNode}
+
+    @rtype: L{RpcResultsBuilder}
+    @return: self for chaining
+
+    """
+    self._results.append(self.CreateSuccessfulNodeResult(node, data))
+    return self
+
+  def AddFailedNode(self, node):
+    """@see L{CreateFailedNode}
+
+    @rtype: L{RpcResultsBuilder}
+    @return: self for chaining
+
+    """
+    self._results.append(self.CreateFailedNodeResult(node))
+    return self
+
+  def AddOfflineNode(self, node):
+    """@see L{CreateOfflineNode}
+
+    @rtype: L{RpcResultsBuilder}
+    @return: self for chaining
+
+    """
+    self._results.append(self.CreateOfflineNodeResult(node))
+    return self
+
+  def AddErrorNode(self, node, error_msg=None):
+    """@see L{CreateErrorNode}
+
+    @rtype: L{RpcResultsBuilder}
+    @return: self for chaining
+
+    """
+    self._results.append(self.CreateErrorNodeResult(node, error_msg=error_msg))
+    return self
+
+  def Build(self):
+    """Creates a dictionary holding multi-node results
+
+    @rtype: dict
+    """
+    return dict((result.node, result) for result in self._results)
+
+
+# pylint: disable=C0103
+def patchRpc(module_under_test):
+  """Patches the L{ganeti.rpc} module for tests.
+
+  This function is meant to be used as a decorator for test methods.
+
+  @type module_under_test: string
+  @param module_under_test: the module within cmdlib which is tested. The
+        "ganeti.cmdlib" prefix is optional.
+
+  """
+  return patchModule(module_under_test, "rpc", wraps=rpc)
+
+
+def SetupDefaultRpcModuleMock(rpc_mod):
+  """Configures the given rpc_mod.
+
+  All relevant functions in rpc_mod are stubbed in a sensible way.
+
+  @param rpc_mod: the mock module to configure
+
+  """
+  rpc_mod.DnsOnlyRunner.return_value = CreateRpcRunnerMock()
diff --git a/test/py/cmdlib/testsupport/ssh_mock.py b/test/py/cmdlib/testsupport/ssh_mock.py
new file mode 100644 (file)
index 0000000..6ddd974
--- /dev/null
@@ -0,0 +1,39 @@
+#
+#
+
+# Copyright (C) 2013 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.
+
+
+"""Support for mocking the ssh module"""
+
+
+from cmdlib.testsupport.util import patchModule
+
+
+# pylint: disable=C0103
+def patchSsh(module_under_test):
+  """Patches the L{ganeti.ssh} module for tests.
+
+  This function is meant to be used as a decorator for test methods.
+
+  @type module_under_test: string
+  @param module_under_test: the module within cmdlib which is tested. The
+        "ganeti.cmdlib" prefix is optional.
+
+  """
+  return patchModule(module_under_test, "ssh")
diff --git a/test/py/cmdlib/testsupport/util.py b/test/py/cmdlib/testsupport/util.py
new file mode 100644 (file)
index 0000000..61305c9
--- /dev/null
@@ -0,0 +1,41 @@
+#
+#
+
+# Copyright (C) 2013 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.
+
+
+"""Utility functions or the cmdlib test framework"""
+
+
+import mock
+
+
+# pylint: disable=C0103
+def patchModule(module_under_test, mock_module, **kwargs):
+  """Computes the module prefix required to mock parts of the Ganeti code.
+
+  @type module_under_test: string
+  @param module_under_test: the module within cmdlib which is tested. The
+        "ganeti.cmdlib" prefix is optional.
+  @type mock_module
+  @param mock_module: the module which should be mocked.
+
+  """
+  if not module_under_test.startswith("ganeti.cmdlib"):
+    module_under_test = "ganeti.cmdlib." + module_under_test
+  return mock.patch("%s.%s" % (module_under_test, mock_module), **kwargs)
diff --git a/test/py/cmdlib/testsupport/utils_mock.py b/test/py/cmdlib/testsupport/utils_mock.py
new file mode 100644 (file)
index 0000000..0559a38
--- /dev/null
@@ -0,0 +1,39 @@
+#
+#
+
+# Copyright (C) 2013 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.
+
+
+"""Support for mocking the utils module"""
+
+
+from cmdlib.testsupport.util import patchModule
+
+
+# pylint: disable=C0103
+def patchUtils(module_under_test):
+  """Patches the L{ganeti.utils} module for tests.
+
+  This function is meant to be used as a decorator for test methods.
+
+  @type module_under_test: string
+  @param module_under_test: the module within cmdlib which is tested. The
+        "ganeti.cmdlib" prefix is optional.
+
+  """
+  return patchModule(module_under_test, "utils")
index ec7b50d..13009d2 100755 (executable)
@@ -28,23 +28,23 @@ err() {
   exit 1
 }
 
-if ! grep -q '^ENABLE_CONFD = ' lib/_autoconf.py; then
+if ! grep -q '^ENABLE_CONFD = ' lib/_constants.py; then
   err "Please update $0, confd enable feature is missing"
 fi
 
-if ! grep -q '^ENABLE_MOND = ' lib/_autoconf.py; then
+if ! grep -q '^ENABLE_MOND = ' lib/_constants.py; then
   err "Please update $0, mond enable feature is missing"
 fi
 
 DAEMONS_LIST="noded masterd rapi"
 STOPDAEMONS_LIST="rapi masterd noded"
 
-if grep -q '^ENABLE_CONFD = True' lib/_autoconf.py; then
+if grep -q '^ENABLE_CONFD = True' lib/_constants.py; then
   DAEMONS_LIST="$DAEMONS_LIST confd luxid"
   STOPDAEMONS_LIST="luxid confd $STOPDAEMONS_LIST"
 fi
 
-if grep -q '^ENABLE_MOND = True' lib/_autoconf.py; then
+if grep -q '^ENABLE_MOND = True' lib/_constants.py; then
   DAEMONS_LIST="$DAEMONS_LIST mond"
   STOPDAEMONS_LIST="mond $STOPDAEMONS_LIST"
 fi
index 8c9d4fe..8589701 100755 (executable)
@@ -26,7 +26,7 @@ import re
 import itertools
 import operator
 
-from ganeti import _autoconf
+from ganeti import _constants
 from ganeti import utils
 from ganeti import cmdlib
 from ganeti import build
@@ -314,7 +314,7 @@ class TestManpages(unittest.TestCase):
     return build.LoadModule("scripts/%s" % name)
 
   def test(self):
-    for script in _autoconf.GNT_SCRIPTS:
+    for script in _constants.GNT_SCRIPTS:
       self._CheckManpage(script,
                          self._ReadManFile(script),
                          self._LoadScript(script).commands.keys())
index 82c4a16..b772825 100755 (executable)
@@ -27,6 +27,7 @@ import unittest
 
 from ganeti import bootstrap
 from ganeti import constants
+from ganeti.storage import drbd
 from ganeti import errors
 from ganeti import pathutils
 
@@ -130,5 +131,47 @@ class TestRestrictIpolicyToEnabledDiskTemplates(unittest.TestCase):
     self.assertEqual(ipolicy[constants.IPOLICY_DTS], [constants.DT_PLAIN])
 
 
+class TestInitCheckDrbdHelper(unittest.TestCase):
+
+  @testutils.patch_object(drbd.DRBD8, "GetUsermodeHelper")
+  def testNoDrbd(self, drbd_mock_get_usermode_helper):
+    drbd_enabled = False
+    drbd_helper = None
+    bootstrap._InitCheckDrbdHelper(drbd_helper, drbd_enabled)
+
+  @testutils.patch_object(drbd.DRBD8, "GetUsermodeHelper")
+  def testHelperNone(self, drbd_mock_get_usermode_helper):
+    drbd_enabled = True
+    current_helper = "/bin/helper"
+    drbd_helper = None
+    drbd_mock_get_usermode_helper.return_value = current_helper
+    bootstrap._InitCheckDrbdHelper(drbd_helper, drbd_enabled)
+
+  @testutils.patch_object(drbd.DRBD8, "GetUsermodeHelper")
+  def testHelperOk(self, drbd_mock_get_usermode_helper):
+    drbd_enabled = True
+    current_helper = "/bin/helper"
+    drbd_helper = "/bin/helper"
+    drbd_mock_get_usermode_helper.return_value = current_helper
+    bootstrap._InitCheckDrbdHelper(drbd_helper, drbd_enabled)
+
+  @testutils.patch_object(drbd.DRBD8, "GetUsermodeHelper")
+  def testWrongHelper(self, drbd_mock_get_usermode_helper):
+    drbd_enabled = True
+    current_helper = "/bin/otherhelper"
+    drbd_helper = "/bin/helper"
+    drbd_mock_get_usermode_helper.return_value = current_helper
+    self.assertRaises(errors.OpPrereqError,
+        bootstrap._InitCheckDrbdHelper, drbd_helper, drbd_enabled)
+
+  @testutils.patch_object(drbd.DRBD8, "GetUsermodeHelper")
+  def testHelperCheckFails(self, drbd_mock_get_usermode_helper):
+    drbd_enabled = True
+    drbd_helper = "/bin/helper"
+    drbd_mock_get_usermode_helper.side_effect=errors.BlockDeviceError
+    self.assertRaises(errors.OpPrereqError,
+        bootstrap._InitCheckDrbdHelper, drbd_helper, drbd_enabled)
+
+
 if __name__ == "__main__":
   testutils.GanetiTestProgram()
index 1908f87..288eff5 100755 (executable)
 import unittest
 import optparse
 
+from ganeti import errors
 from ganeti.client import gnt_cluster
 from ganeti import utils
 from ganeti import compat
 from ganeti import constants
 
+import mock
 import testutils
 
 
@@ -258,5 +260,99 @@ class TestEpo(unittest.TestCase):
     self.assertEqual(result, constants.EXIT_FAILURE)
 
 
+class DrbdHelperTestCase(unittest.TestCase):
+
+  def setUp(self):
+    unittest.TestCase.setUp(self)
+    self.enabled_disk_templates = []
+
+  def enableDrbd(self):
+    self.enabled_disk_templates = [constants.DT_DRBD8]
+
+  def disableDrbd(self):
+    self.enabled_disk_templates = [constants.DT_DISKLESS]
+
+
+class InitDrbdHelper(DrbdHelperTestCase):
+
+  def testNoDrbdNoHelper(self):
+    opts = mock.Mock()
+    opts.drbd_helper = None
+    self.disableDrbd()
+    helper = gnt_cluster._InitDrbdHelper(opts, self.enabled_disk_templates)
+    self.assertEquals(None, helper)
+
+  def testNoDrbdHelper(self):
+    opts = mock.Mock()
+    self.disableDrbd()
+    opts.drbd_helper = "/bin/true"
+    helper = gnt_cluster._InitDrbdHelper(opts, self.enabled_disk_templates)
+    self.assertEquals(opts.drbd_helper, helper)
+
+  def testDrbdHelperNone(self):
+    opts = mock.Mock()
+    self.enableDrbd()
+    opts.drbd_helper = None
+    helper = gnt_cluster._InitDrbdHelper(opts, self.enabled_disk_templates)
+    self.assertEquals(constants.DEFAULT_DRBD_HELPER, helper)
+
+  def testDrbdHelperEmpty(self):
+    opts = mock.Mock()
+    self.enableDrbd()
+    opts.drbd_helper = ''
+    self.assertRaises(errors.OpPrereqError, gnt_cluster._InitDrbdHelper, opts,
+        self.enabled_disk_templates)
+
+  def testDrbdHelper(self):
+    opts = mock.Mock()
+    self.enableDrbd()
+    opts.drbd_helper = "/bin/true"
+    helper = gnt_cluster._InitDrbdHelper(opts, self.enabled_disk_templates)
+    self.assertEquals(opts.drbd_helper, helper)
+
+
+class GetDrbdHelper(DrbdHelperTestCase):
+
+  def testNoDrbdNoHelper(self):
+    opts = mock.Mock()
+    self.disableDrbd()
+    opts.drbd_helper = None
+    helper = gnt_cluster._GetDrbdHelper(opts, self.enabled_disk_templates)
+    self.assertEquals(None, helper)
+
+  def testNoTemplateInfoNoHelper(self):
+    opts = mock.Mock()
+    opts.drbd_helper = None
+    helper = gnt_cluster._GetDrbdHelper(opts, None)
+    self.assertEquals(None, helper)
+
+  def testNoTemplateInfoHelper(self):
+    opts = mock.Mock()
+    opts.drbd_helper = "/bin/true"
+    helper = gnt_cluster._GetDrbdHelper(opts, None)
+    self.assertEquals(opts.drbd_helper, helper)
+
+  def testNoDrbdHelper(self):
+    opts = mock.Mock()
+    self.disableDrbd()
+    opts.drbd_helper = "/bin/true"
+    helper = gnt_cluster._GetDrbdHelper(opts, None)
+    self.assertEquals(opts.drbd_helper, helper)
+
+  def testDrbdNoHelper(self):
+    opts = mock.Mock()
+    self.enableDrbd()
+    opts.drbd_helper = None
+    helper = gnt_cluster._GetDrbdHelper(opts, self.enabled_disk_templates)
+    self.assertEquals(None, helper)
+
+  def testDrbdHelper(self):
+    opts = mock.Mock()
+    self.enableDrbd()
+    opts.drbd_helper = "/bin/true"
+    helper = gnt_cluster._GetDrbdHelper(opts, self.enabled_disk_templates)
+    self.assertEquals(opts.drbd_helper, helper)
+
+
 if __name__ == "__main__":
   testutils.GanetiTestProgram()
diff --git a/test/py/ganeti.cmdlib.cluster_unittest.py b/test/py/ganeti.cmdlib.cluster_unittest.py
deleted file mode 100755 (executable)
index 819b401..0000000
+++ /dev/null
@@ -1,92 +0,0 @@
-#!/usr/bin/python
-#
-
-# Copyright (C) 2013 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 unittesting the cmdlib module 'cluster'"""
-
-
-import unittest
-
-from ganeti.cmdlib import cluster
-from ganeti import constants
-from ganeti import errors
-
-import testutils
-import mock
-
-
-class TestCheckFileStoragePath(unittest.TestCase):
-
-  def setUp(self):
-    unittest.TestCase.setUp(self)
-    self.log_warning = mock.Mock()
-
-  def enableFileStorage(self, file_storage_enabled):
-    if file_storage_enabled:
-      self.enabled_disk_templates = [constants.DT_FILE]
-    else:
-      # anything != 'file' would do here
-      self.enabled_disk_templates = [constants.DT_DISKLESS]
-
-  def testNone(self):
-    self.enableFileStorage(True)
-    self.assertRaises(
-        errors.ProgrammerError,
-        cluster.CheckFileStoragePathVsEnabledDiskTemplates,
-        self.log_warning, None, self.enabled_disk_templates)
-
-  def testNotEmptyAndEnabled(self):
-    self.enableFileStorage(True)
-    cluster.CheckFileStoragePathVsEnabledDiskTemplates(
-        self.log_warning, "/some/path", self.enabled_disk_templates)
-
-  def testNotEnabled(self):
-    self.enableFileStorage(False)
-    cluster.CheckFileStoragePathVsEnabledDiskTemplates(
-        self.log_warning, "/some/path", self.enabled_disk_templates)
-    self.assertTrue(self.log_warning.called)
-
-  def testEmptyAndEnabled(self):
-    self.enableFileStorage(True)
-    self.assertRaises(
-        errors.OpPrereqError,
-        cluster.CheckFileStoragePathVsEnabledDiskTemplates,
-        self.log_warning, "", self.enabled_disk_templates)
-
-  def testEmptyAndDisabled(self):
-    self.enableFileStorage(False)
-    cluster.CheckFileStoragePathVsEnabledDiskTemplates(
-        NotImplemented, "", self.enabled_disk_templates)
-
-
-class TestGetEnabledDiskTemplates(unittest.TestCase):
-
-  def testNoNew(self):
-    op_dts = [constants.DT_DISKLESS]
-    old_dts = [constants.DT_DISKLESS]
-    (enabled_dts, new_dts) =\
-        cluster.LUClusterSetParams._GetEnabledDiskTemplatesInner(
-            op_dts, old_dts)
-    self.assertEqual(enabled_dts, old_dts)
-    self.assertEqual(new_dts, [])
-
-
-if __name__ == "__main__":
-  testutils.GanetiTestProgram()
diff --git a/test/py/ganeti.cmdlib.common_unittest.py b/test/py/ganeti.cmdlib.common_unittest.py
deleted file mode 100755 (executable)
index 626afe6..0000000
+++ /dev/null
@@ -1,54 +0,0 @@
-#!/usr/bin/python
-#
-
-# Copyright (C) 2013 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 unittesting the cmdlib module 'common'"""
-
-
-import unittest
-
-from ganeti.cmdlib import common
-from ganeti import constants
-from ganeti import errors
-
-import testutils
-
-
-class TestCheckIpolicy(unittest.TestCase):
-
-  def testAllTemplatesEnabled(self):
-    allowed_disk_templates = [constants.DT_PLAIN]
-    ipolicy = {constants.IPOLICY_DTS: allowed_disk_templates}
-    enabled_disk_templates = [constants.DT_PLAIN, constants.DT_DRBD8]
-    common.CheckIpolicyVsDiskTemplates(
-        ipolicy, enabled_disk_templates)
-
-  def testSomeTemplatesUnenabled(self):
-    allowed_disk_templates = [constants.DT_PLAIN, constants.DT_DISKLESS]
-    ipolicy = {constants.IPOLICY_DTS: allowed_disk_templates}
-    enabled_disk_templates = [constants.DT_PLAIN, constants.DT_DRBD8]
-    self.assertRaises(
-        errors.OpPrereqError,
-        common.CheckIpolicyVsDiskTemplates,
-        ipolicy, enabled_disk_templates)
-
-
-if __name__ == "__main__":
-  testutils.GanetiTestProgram()
diff --git a/test/py/ganeti.cmdlib_unittest.py b/test/py/ganeti.cmdlib_unittest.py
deleted file mode 100755 (executable)
index 1165eef..0000000
+++ /dev/null
@@ -1,2075 +0,0 @@
-#!/usr/bin/python
-#
-
-# Copyright (C) 2008, 2011, 2012, 2013 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 unittesting the cmdlib module"""
-
-
-import os
-import unittest
-import tempfile
-import shutil
-import operator
-import itertools
-import copy
-
-from ganeti import constants
-from ganeti import mcpu
-from ganeti import cmdlib
-from ganeti.cmdlib import cluster
-from ganeti.cmdlib import group
-from ganeti.cmdlib import instance
-from ganeti.cmdlib import instance_storage
-from ganeti.cmdlib import instance_utils
-from ganeti.cmdlib import common
-from ganeti.cmdlib import query
-from ganeti import opcodes
-from ganeti import errors
-from ganeti import utils
-from ganeti import luxi
-from ganeti import ht
-from ganeti import objects
-from ganeti import compat
-from ganeti import rpc
-from ganeti import locking
-from ganeti import pathutils
-from ganeti.masterd import iallocator
-from ganeti.hypervisor import hv_xen
-
-import testutils
-import mocks
-
-
-class TestCertVerification(testutils.GanetiTestCase):
-  def setUp(self):
-    testutils.GanetiTestCase.setUp(self)
-
-    self.tmpdir = tempfile.mkdtemp()
-
-  def tearDown(self):
-    shutil.rmtree(self.tmpdir)
-
-  def testVerifyCertificate(self):
-    cluster._VerifyCertificate(testutils.TestDataFilename("cert1.pem"))
-
-    nonexist_filename = os.path.join(self.tmpdir, "does-not-exist")
-
-    (errcode, msg) = cluster._VerifyCertificate(nonexist_filename)
-    self.assertEqual(errcode, cluster.LUClusterVerifyConfig.ETYPE_ERROR)
-
-    # Try to load non-certificate file
-    invalid_cert = testutils.TestDataFilename("bdev-net.txt")
-    (errcode, msg) = cluster._VerifyCertificate(invalid_cert)
-    self.assertEqual(errcode, cluster.LUClusterVerifyConfig.ETYPE_ERROR)
-
-
-class TestOpcodeParams(testutils.GanetiTestCase):
-  def testParamsStructures(self):
-    for op in sorted(mcpu.Processor.DISPATCH_TABLE):
-      lu = mcpu.Processor.DISPATCH_TABLE[op]
-      lu_name = lu.__name__
-      self.failIf(hasattr(lu, "_OP_REQP"),
-                  msg=("LU '%s' has old-style _OP_REQP" % lu_name))
-      self.failIf(hasattr(lu, "_OP_DEFS"),
-                  msg=("LU '%s' has old-style _OP_DEFS" % lu_name))
-      self.failIf(hasattr(lu, "_OP_PARAMS"),
-                  msg=("LU '%s' has old-style _OP_PARAMS" % lu_name))
-
-
-class TestIAllocatorChecks(testutils.GanetiTestCase):
-  def testFunction(self):
-    class TestLU(object):
-      def __init__(self, opcode):
-        self.cfg = mocks.FakeConfig()
-        self.op = opcode
-
-    class OpTest(opcodes.OpCode):
-       OP_PARAMS = [
-        ("iallocator", None, ht.NoType, None),
-        ("node", None, ht.NoType, None),
-        ]
-
-    default_iallocator = mocks.FakeConfig().GetDefaultIAllocator()
-    other_iallocator = default_iallocator + "_not"
-
-    op = OpTest()
-    lu = TestLU(op)
-
-    c_i = lambda: common.CheckIAllocatorOrNode(lu, "iallocator", "node")
-
-    # Neither node nor iallocator given
-    for n in (None, []):
-      op.iallocator = None
-      op.node = n
-      c_i()
-      self.assertEqual(lu.op.iallocator, default_iallocator)
-      self.assertEqual(lu.op.node, n)
-
-    # Both, iallocator and node given
-    for a in ("test", constants.DEFAULT_IALLOCATOR_SHORTCUT):
-      op.iallocator = a
-      op.node = "test"
-      self.assertRaises(errors.OpPrereqError, c_i)
-
-    # Only iallocator given
-    for n in (None, []):
-      op.iallocator = other_iallocator
-      op.node = n
-      c_i()
-      self.assertEqual(lu.op.iallocator, other_iallocator)
-      self.assertEqual(lu.op.node, n)
-
-    # Only node given
-    op.iallocator = None
-    op.node = "node"
-    c_i()
-    self.assertEqual(lu.op.iallocator, None)
-    self.assertEqual(lu.op.node, "node")
-
-    # Asked for default iallocator, no node given
-    op.iallocator = constants.DEFAULT_IALLOCATOR_SHORTCUT
-    op.node = None
-    c_i()
-    self.assertEqual(lu.op.iallocator, default_iallocator)
-    self.assertEqual(lu.op.node, None)
-
-    # No node, iallocator or default iallocator
-    op.iallocator = None
-    op.node = None
-    lu.cfg.GetDefaultIAllocator = lambda: None
-    self.assertRaises(errors.OpPrereqError, c_i)
-
-
-class TestLUTestJqueue(unittest.TestCase):
-  def test(self):
-    self.assert_(cmdlib.LUTestJqueue._CLIENT_CONNECT_TIMEOUT <
-                 (luxi.WFJC_TIMEOUT * 0.75),
-                 msg=("Client timeout too high, might not notice bugs"
-                      " in WaitForJobChange"))
-
-
-class TestLUQuery(unittest.TestCase):
-  def test(self):
-    self.assertEqual(sorted(query._QUERY_IMPL.keys()),
-                     sorted(constants.QR_VIA_OP))
-
-    assert constants.QR_NODE in constants.QR_VIA_OP
-    assert constants.QR_INSTANCE in constants.QR_VIA_OP
-
-    for i in constants.QR_VIA_OP:
-      self.assert_(query._GetQueryImplementation(i))
-
-    self.assertRaises(errors.OpPrereqError, query._GetQueryImplementation,
-                      "")
-    self.assertRaises(errors.OpPrereqError, query._GetQueryImplementation,
-                      "xyz")
-
-
-class TestLUGroupAssignNodes(unittest.TestCase):
-
-  def testCheckAssignmentForSplitInstances(self):
-    node_data = dict((n, objects.Node(name=n, group=g))
-                     for (n, g) in [("n1a", "g1"), ("n1b", "g1"),
-                                    ("n2a", "g2"), ("n2b", "g2"),
-                                    ("n3a", "g3"), ("n3b", "g3"),
-                                    ("n3c", "g3"),
-                                    ])
-
-    def Instance(uuid, pnode, snode):
-      if snode is None:
-        disks = []
-        disk_template = constants.DT_DISKLESS
-      else:
-        disks = [objects.Disk(dev_type=constants.DT_DRBD8,
-                              logical_id=[pnode, snode, 1, 17, 17])]
-        disk_template = constants.DT_DRBD8
-
-      return objects.Instance(name="%s-name" % uuid, uuid="%s" % uuid,
-                              primary_node=pnode, disks=disks,
-                              disk_template=disk_template)
-
-    instance_data = dict((uuid, Instance(uuid, pnode, snode))
-                         for uuid, pnode, snode in [("inst1a", "n1a", "n1b"),
-                                                    ("inst1b", "n1b", "n1a"),
-                                                    ("inst2a", "n2a", "n2b"),
-                                                    ("inst3a", "n3a", None),
-                                                    ("inst3b", "n3b", "n1b"),
-                                                    ("inst3c", "n3b", "n2b"),
-                                                    ])
-
-    # Test first with the existing state.
-    (new, prev) = \
-      group.LUGroupAssignNodes.CheckAssignmentForSplitInstances([],
-                                                                node_data,
-                                                                instance_data)
-
-    self.assertEqual([], new)
-    self.assertEqual(set(["inst3b", "inst3c"]), set(prev))
-
-    # And now some changes.
-    (new, prev) = \
-      group.LUGroupAssignNodes.CheckAssignmentForSplitInstances([("n1b",
-                                                                  "g3")],
-                                                                node_data,
-                                                                instance_data)
-
-    self.assertEqual(set(["inst1a", "inst1b"]), set(new))
-    self.assertEqual(set(["inst3c"]), set(prev))
-
-
-class TestClusterVerifySsh(unittest.TestCase):
-  def testMultipleGroups(self):
-    fn = cluster.LUClusterVerifyGroup._SelectSshCheckNodes
-    mygroupnodes = [
-      objects.Node(name="node20", group="my", offline=False),
-      objects.Node(name="node21", group="my", offline=False),
-      objects.Node(name="node22", group="my", offline=False),
-      objects.Node(name="node23", group="my", offline=False),
-      objects.Node(name="node24", group="my", offline=False),
-      objects.Node(name="node25", group="my", offline=False),
-      objects.Node(name="node26", group="my", offline=True),
-      ]
-    nodes = [
-      objects.Node(name="node1", group="g1", offline=True),
-      objects.Node(name="node2", group="g1", offline=False),
-      objects.Node(name="node3", group="g1", offline=False),
-      objects.Node(name="node4", group="g1", offline=True),
-      objects.Node(name="node5", group="g1", offline=False),
-      objects.Node(name="node10", group="xyz", offline=False),
-      objects.Node(name="node11", group="xyz", offline=False),
-      objects.Node(name="node40", group="alloff", offline=True),
-      objects.Node(name="node41", group="alloff", offline=True),
-      objects.Node(name="node50", group="aaa", offline=False),
-      ] + mygroupnodes
-    assert not utils.FindDuplicates(map(operator.attrgetter("name"), nodes))
-
-    (online, perhost) = fn(mygroupnodes, "my", nodes)
-    self.assertEqual(online, ["node%s" % i for i in range(20, 26)])
-    self.assertEqual(set(perhost.keys()), set(online))
-
-    self.assertEqual(perhost, {
-      "node20": ["node10", "node2", "node50"],
-      "node21": ["node11", "node3", "node50"],
-      "node22": ["node10", "node5", "node50"],
-      "node23": ["node11", "node2", "node50"],
-      "node24": ["node10", "node3", "node50"],
-      "node25": ["node11", "node5", "node50"],
-      })
-
-  def testSingleGroup(self):
-    fn = cluster.LUClusterVerifyGroup._SelectSshCheckNodes
-    nodes = [
-      objects.Node(name="node1", group="default", offline=True),
-      objects.Node(name="node2", group="default", offline=False),
-      objects.Node(name="node3", group="default", offline=False),
-      objects.Node(name="node4", group="default", offline=True),
-      ]
-    assert not utils.FindDuplicates(map(operator.attrgetter("name"), nodes))
-
-    (online, perhost) = fn(nodes, "default", nodes)
-    self.assertEqual(online, ["node2", "node3"])
-    self.assertEqual(set(perhost.keys()), set(online))
-
-    self.assertEqual(perhost, {
-      "node2": [],
-      "node3": [],
-      })
-
-
-class TestClusterVerifyFiles(unittest.TestCase):
-  @staticmethod
-  def _FakeErrorIf(errors, cond, ecode, item, msg, *args, **kwargs):
-    assert ((ecode == constants.CV_ENODEFILECHECK and
-             ht.TNonEmptyString(item)) or
-            (ecode == constants.CV_ECLUSTERFILECHECK and
-             item is None))
-
-    if args:
-      msg = msg % args
-
-    if cond:
-      errors.append((item, msg))
-
-  def test(self):
-    errors = []
-    nodeinfo = [
-      objects.Node(name="master.example.com",
-                   uuid="master-uuid",
-                   offline=False,
-                   vm_capable=True),
-      objects.Node(name="node2.example.com",
-                   uuid="node2-uuid",
-                   offline=False,
-                   vm_capable=True),
-      objects.Node(name="node3.example.com",
-                   uuid="node3-uuid",
-                   master_candidate=True,
-                   vm_capable=False),
-      objects.Node(name="node4.example.com",
-                   uuid="node4-uuid",
-                   offline=False,
-                   vm_capable=True),
-      objects.Node(name="nodata.example.com",
-                   uuid="nodata-uuid",
-                   offline=False,
-                   vm_capable=True),
-      objects.Node(name="offline.example.com",
-                   uuid="offline-uuid",
-                   offline=True),
-      ]
-    files_all = set([
-      pathutils.CLUSTER_DOMAIN_SECRET_FILE,
-      pathutils.RAPI_CERT_FILE,
-      pathutils.RAPI_USERS_FILE,
-      ])
-    files_opt = set([
-      pathutils.RAPI_USERS_FILE,
-      hv_xen.XL_CONFIG_FILE,
-      pathutils.VNC_PASSWORD_FILE,
-      ])
-    files_mc = set([
-      pathutils.CLUSTER_CONF_FILE,
-      ])
-    files_vm = set([
-      hv_xen.XEND_CONFIG_FILE,
-      hv_xen.XL_CONFIG_FILE,
-      pathutils.VNC_PASSWORD_FILE,
-      ])
-    nvinfo = {
-      "master-uuid": rpc.RpcResult(data=(True, {
-        constants.NV_FILELIST: {
-          pathutils.CLUSTER_CONF_FILE: "82314f897f38b35f9dab2f7c6b1593e0",
-          pathutils.RAPI_CERT_FILE: "babbce8f387bc082228e544a2146fee4",
-          pathutils.CLUSTER_DOMAIN_SECRET_FILE: "cds-47b5b3f19202936bb4",
-          hv_xen.XEND_CONFIG_FILE: "b4a8a824ab3cac3d88839a9adeadf310",
-          hv_xen.XL_CONFIG_FILE: "77935cee92afd26d162f9e525e3d49b9"
-        }})),
-      "node2-uuid": rpc.RpcResult(data=(True, {
-        constants.NV_FILELIST: {
-          pathutils.RAPI_CERT_FILE: "97f0356500e866387f4b84233848cc4a",
-          hv_xen.XEND_CONFIG_FILE: "b4a8a824ab3cac3d88839a9adeadf310",
-          }
-        })),
-      "node3-uuid": rpc.RpcResult(data=(True, {
-        constants.NV_FILELIST: {
-          pathutils.RAPI_CERT_FILE: "97f0356500e866387f4b84233848cc4a",
-          pathutils.CLUSTER_DOMAIN_SECRET_FILE: "cds-47b5b3f19202936bb4",
-          }
-        })),
-      "node4-uuid": rpc.RpcResult(data=(True, {
-        constants.NV_FILELIST: {
-          pathutils.RAPI_CERT_FILE: "97f0356500e866387f4b84233848cc4a",
-          pathutils.CLUSTER_CONF_FILE: "conf-a6d4b13e407867f7a7b4f0f232a8f527",
-          pathutils.CLUSTER_DOMAIN_SECRET_FILE: "cds-47b5b3f19202936bb4",
-          pathutils.RAPI_USERS_FILE: "rapiusers-ea3271e8d810ef3",
-          hv_xen.XL_CONFIG_FILE: "77935cee92afd26d162f9e525e3d49b9"
-          }
-        })),
-      "nodata-uuid": rpc.RpcResult(data=(True, {})),
-      "offline-uuid": rpc.RpcResult(offline=True),
-      }
-    assert set(nvinfo.keys()) == set(map(operator.attrgetter("uuid"), nodeinfo))
-
-    verify_lu = cluster.LUClusterVerifyGroup(mocks.FakeProc(),
-                                             opcodes.OpClusterVerify(),
-                                             mocks.FakeContext(),
-                                             None)
-
-    verify_lu._ErrorIf = compat.partial(self._FakeErrorIf, errors)
-
-    # TODO: That's a bit hackish to mock only this single method. We should
-    # build a better FakeConfig which provides such a feature already.
-    def GetNodeName(node_uuid):
-      for node in nodeinfo:
-        if node.uuid == node_uuid:
-          return node.name
-      return None
-
-    verify_lu.cfg.GetNodeName = GetNodeName
-
-    verify_lu._VerifyFiles(nodeinfo, "master-uuid", nvinfo,
-                           (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;"
-              " variant 2 on master.example.com)" % pathutils.RAPI_CERT_FILE)),
-      (None, ("File %s is missing from node(s) node2.example.com" %
-              pathutils.CLUSTER_DOMAIN_SECRET_FILE)),
-      (None, ("File %s should not exist on node(s) node4.example.com" %
-              pathutils.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" %
-              pathutils.CLUSTER_CONF_FILE)),
-      (None, ("File %s found with 2 different checksums (variant 1 on"
-              " master.example.com; variant 2 on node4.example.com)" %
-              pathutils.CLUSTER_CONF_FILE)),
-      (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)" % pathutils.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, cfg=NotImplemented, proc=NotImplemented,
-               rpc=NotImplemented):
-    self.warning_log = []
-    self.info_log = []
-    self.cfg = cfg
-    self.proc = proc
-    self.rpc = rpc
-
-  def LogWarning(self, text, *args):
-    self.warning_log.append((text, args))
-
-  def LogInfo(self, text, *args):
-    self.info_log.append((text, args))
-
-
-class TestLoadNodeEvacResult(unittest.TestCase):
-  def testSuccess(self):
-    for moved in [[], [
-      ("inst20153.example.com", "grp2", ["nodeA4509", "nodeB2912"]),
-      ]]:
-      for early_release in [False, True]:
-        for use_nodes in [False, True]:
-          jobs = [
-            [opcodes.OpInstanceReplaceDisks().__getstate__()],
-            [opcodes.OpInstanceMigrate().__getstate__()],
-            ]
-
-          alloc_result = (moved, [], jobs)
-          assert iallocator._NEVAC_RESULT(alloc_result)
-
-          lu = _FakeLU()
-          result = common.LoadNodeEvacResult(lu, alloc_result,
-                                             early_release, use_nodes)
-
-          if moved:
-            (_, (info_args, )) = lu.info_log.pop(0)
-            for (instname, instgroup, instnodes) in moved:
-              self.assertTrue(instname in info_args)
-              if use_nodes:
-                for i in instnodes:
-                  self.assertTrue(i in info_args)
-              else:
-                self.assertTrue(instgroup in info_args)
-
-          self.assertFalse(lu.info_log)
-          self.assertFalse(lu.warning_log)
-
-          for op in itertools.chain(*result):
-            if hasattr(op.__class__, "early_release"):
-              self.assertEqual(op.early_release, early_release)
-            else:
-              self.assertFalse(hasattr(op, "early_release"))
-
-  def testFailed(self):
-    alloc_result = ([], [
-      ("inst5191.example.com", "errormsg21178"),
-      ], [])
-    assert iallocator._NEVAC_RESULT(alloc_result)
-
-    lu = _FakeLU()
-    self.assertRaises(errors.OpExecError, common.LoadNodeEvacResult,
-                      lu, alloc_result, False, False)
-    self.assertFalse(lu.info_log)
-    (_, (args, )) = lu.warning_log.pop(0)
-    self.assertTrue("inst5191.example.com" in args)
-    self.assertTrue("errormsg21178" in args)
-    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 = common._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,
-                      common._UpdateAndVerifySubDict, {}, test,
-                      self.type_check)
-
-
-class TestHvStateHelper(unittest.TestCase):
-  def testWithoutOpData(self):
-    self.assertEqual(common.MergeAndVerifyHvState(None, NotImplemented),
-                     None)
-
-  def testWithoutOldData(self):
-    new = {
-      constants.HT_XEN_PVM: {
-        constants.HVST_MEMORY_TOTAL: 4096,
-        },
-      }
-    self.assertEqual(common.MergeAndVerifyHvState(new, None), new)
-
-  def testWithWrongHv(self):
-    new = {
-      "i-dont-exist": {
-        constants.HVST_MEMORY_TOTAL: 4096,
-        },
-      }
-    self.assertRaises(errors.OpPrereqError, common.MergeAndVerifyHvState,
-                      new, None)
-
-class TestDiskStateHelper(unittest.TestCase):
-  def testWithoutOpData(self):
-    self.assertEqual(common.MergeAndVerifyDiskState(None, NotImplemented),
-                     None)
-
-  def testWithoutOldData(self):
-    new = {
-      constants.DT_PLAIN: {
-        "xenvg": {
-          constants.DS_DISK_RESERVED: 1024,
-          },
-        },
-      }
-    self.assertEqual(common.MergeAndVerifyDiskState(new, None), new)
-
-  def testWithWrongStorageType(self):
-    new = {
-      "i-dont-exist": {
-        "xenvg": {
-          constants.DS_DISK_RESERVED: 1024,
-          },
-        },
-      }
-    self.assertRaises(errors.OpPrereqError, common.MergeAndVerifyDiskState,
-                      new, None)
-
-
-class TestComputeMinMaxSpec(unittest.TestCase):
-  def setUp(self):
-    self.ispecs = {
-      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(common._ComputeMinMaxSpec(constants.ISPEC_MEM_SIZE, None,
-                                              self.ispecs, None) is None)
-
-  def testAutoValue(self):
-    self.assertTrue(common._ComputeMinMaxSpec(constants.ISPEC_MEM_SIZE, None,
-                                              self.ispecs,
-                                              constants.VALUE_AUTO) is None)
-
-  def testNotDefined(self):
-    self.assertTrue(common._ComputeMinMaxSpec(constants.ISPEC_NIC_COUNT, None,
-                                              self.ispecs, 3) is None)
-
-  def testNoMinDefined(self):
-    self.assertTrue(common._ComputeMinMaxSpec(constants.ISPEC_DISK_SIZE, None,
-                                              self.ispecs, 128) is None)
-
-  def testNoMaxDefined(self):
-    self.assertTrue(common._ComputeMinMaxSpec(constants.ISPEC_DISK_COUNT,
-                                              None, self.ispecs, 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.ispecs[constants.ISPECS_MIN].get(name, val)
-      max_v = self.ispecs[constants.ISPECS_MAX].get(name, val)
-      self.assertEqual(common._ComputeMinMaxSpec(name, None,
-                                                 self.ispecs, val),
-                       "%s value %s is not in range [%s, %s]" %
-                       (name, val,min_v, max_v))
-      self.assertEqual(common._ComputeMinMaxSpec(name, "1",
-                                                 self.ispecs, 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(common._ComputeMinMaxSpec(name, None, self.ispecs, val)
-                      is None)
-
-
-def _ValidateComputeMinMaxSpec(name, *_):
-  assert name in constants.ISPECS_PARAMETERS
-  return None
-
-
-def _NoDiskComputeMinMaxSpec(name, *_):
-  if name == constants.ISPEC_DISK_COUNT:
-    return name
-  else:
-    return None
-
-
-class _SpecWrapper:
-  def __init__(self, spec):
-    self.spec = spec
-
-  def ComputeMinMaxSpec(self, *args):
-    return self.spec.pop(0)
-
-
-class TestComputeIPolicySpecViolation(unittest.TestCase):
-  # Minimal policy accepted by _ComputeIPolicySpecViolation()
-  _MICRO_IPOL = {
-    constants.IPOLICY_DTS: [constants.DT_PLAIN, constants.DT_DISKLESS],
-    constants.ISPECS_MINMAX: [NotImplemented],
-    }
-
-  def test(self):
-    compute_fn = _ValidateComputeMinMaxSpec
-    ret = common.ComputeIPolicySpecViolation(self._MICRO_IPOL, 1024, 1, 1, 1,
-                                             [1024], 1, constants.DT_PLAIN,
-                                             _compute_fn=compute_fn)
-    self.assertEqual(ret, [])
-
-  def testDiskFull(self):
-    compute_fn = _NoDiskComputeMinMaxSpec
-    ret = common.ComputeIPolicySpecViolation(self._MICRO_IPOL, 1024, 1, 1, 1,
-                                             [1024], 1, constants.DT_PLAIN,
-                                             _compute_fn=compute_fn)
-    self.assertEqual(ret, [constants.ISPEC_DISK_COUNT])
-
-  def testDiskLess(self):
-    compute_fn = _NoDiskComputeMinMaxSpec
-    ret = common.ComputeIPolicySpecViolation(self._MICRO_IPOL, 1024, 1, 1, 1,
-                                             [1024], 1, constants.DT_DISKLESS,
-                                             _compute_fn=compute_fn)
-    self.assertEqual(ret, [])
-
-  def testWrongTemplates(self):
-    compute_fn = _ValidateComputeMinMaxSpec
-    ret = common.ComputeIPolicySpecViolation(self._MICRO_IPOL, 1024, 1, 1, 1,
-                                             [1024], 1, constants.DT_DRBD8,
-                                             _compute_fn=compute_fn)
-    self.assertEqual(len(ret), 1)
-    self.assertTrue("Disk template" in ret[0])
-
-  def testInvalidArguments(self):
-    self.assertRaises(AssertionError, common.ComputeIPolicySpecViolation,
-                      self._MICRO_IPOL, 1024, 1, 1, 1, [], 1,
-                      constants.DT_PLAIN,)
-
-  def testInvalidSpec(self):
-    spec = _SpecWrapper([None, False, "foo", None, "bar", None])
-    compute_fn = spec.ComputeMinMaxSpec
-    ret = common.ComputeIPolicySpecViolation(self._MICRO_IPOL, 1024, 1, 1, 1,
-                                             [1024], 1, constants.DT_PLAIN,
-                                             _compute_fn=compute_fn)
-    self.assertEqual(ret, ["foo", "bar"])
-    self.assertFalse(spec.spec)
-
-  def testWithIPolicy(self):
-    mem_size = 2048
-    cpu_count = 2
-    disk_count = 1
-    disk_sizes = [512]
-    nic_count = 1
-    spindle_use = 4
-    disk_template = "mytemplate"
-    ispec = {
-      constants.ISPEC_MEM_SIZE: mem_size,
-      constants.ISPEC_CPU_COUNT: cpu_count,
-      constants.ISPEC_DISK_COUNT: disk_count,
-      constants.ISPEC_DISK_SIZE: disk_sizes[0],
-      constants.ISPEC_NIC_COUNT: nic_count,
-      constants.ISPEC_SPINDLE_USE: spindle_use,
-      }
-    ipolicy1 = {
-      constants.ISPECS_MINMAX: [{
-        constants.ISPECS_MIN: ispec,
-        constants.ISPECS_MAX: ispec,
-        }],
-      constants.IPOLICY_DTS: [disk_template],
-      }
-    ispec_copy = copy.deepcopy(ispec)
-    ipolicy2 = {
-      constants.ISPECS_MINMAX: [
-        {
-          constants.ISPECS_MIN: ispec_copy,
-          constants.ISPECS_MAX: ispec_copy,
-          },
-        {
-          constants.ISPECS_MIN: ispec,
-          constants.ISPECS_MAX: ispec,
-          },
-        ],
-      constants.IPOLICY_DTS: [disk_template],
-      }
-    ipolicy3 = {
-      constants.ISPECS_MINMAX: [
-        {
-          constants.ISPECS_MIN: ispec,
-          constants.ISPECS_MAX: ispec,
-          },
-        {
-          constants.ISPECS_MIN: ispec_copy,
-          constants.ISPECS_MAX: ispec_copy,
-          },
-        ],
-      constants.IPOLICY_DTS: [disk_template],
-      }
-    def AssertComputeViolation(ipolicy, violations):
-      ret = common.ComputeIPolicySpecViolation(ipolicy, mem_size, cpu_count,
-                                               disk_count, nic_count,
-                                               disk_sizes, spindle_use,
-                                               disk_template)
-      self.assertEqual(len(ret), violations)
-
-    AssertComputeViolation(ipolicy1, 0)
-    AssertComputeViolation(ipolicy2, 0)
-    AssertComputeViolation(ipolicy3, 0)
-    for par in constants.ISPECS_PARAMETERS:
-      ispec[par] += 1
-      AssertComputeViolation(ipolicy1, 1)
-      AssertComputeViolation(ipolicy2, 0)
-      AssertComputeViolation(ipolicy3, 0)
-      ispec[par] -= 2
-      AssertComputeViolation(ipolicy1, 1)
-      AssertComputeViolation(ipolicy2, 0)
-      AssertComputeViolation(ipolicy3, 0)
-      ispec[par] += 1 # Restore
-    ipolicy1[constants.IPOLICY_DTS] = ["another_template"]
-    AssertComputeViolation(ipolicy1, 1)
-
-
-class _StubComputeIPolicySpecViolation:
-  def __init__(self, mem_size, cpu_count, disk_count, nic_count, disk_sizes,
-               spindle_use, disk_template):
-    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
-    self.disk_template = disk_template
-
-  def __call__(self, _, mem_size, cpu_count, disk_count, nic_count, disk_sizes,
-               spindle_use, disk_template):
-    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
-    assert self.disk_template == disk_template
-
-    return []
-
-
-class _FakeConfigForComputeIPolicyInstanceViolation:
-  def __init__(self, be, excl_stor):
-    self.cluster = objects.Cluster(beparams={"default": be})
-    self.excl_stor = excl_stor
-
-  def GetClusterInfo(self):
-    return self.cluster
-
-  def GetNodeInfo(self, _):
-    return {}
-
-  def GetNdParams(self, _):
-    return {
-      constants.ND_EXCLUSIVE_STORAGE: self.excl_stor,
-      }
-
-
-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, spindles=13)]
-    cfg = _FakeConfigForComputeIPolicyInstanceViolation(beparams, False)
-    instance = objects.Instance(beparams=beparams, disks=disks, nics=[],
-                                disk_template=constants.DT_PLAIN)
-    stub = _StubComputeIPolicySpecViolation(2048, 2, 1, 0, [512], 4,
-                                            constants.DT_PLAIN)
-    ret = common.ComputeIPolicyInstanceViolation(NotImplemented, instance,
-                                                 cfg, _compute_fn=stub)
-    self.assertEqual(ret, [])
-    instance2 = objects.Instance(beparams={}, disks=disks, nics=[],
-                                 disk_template=constants.DT_PLAIN)
-    ret = common.ComputeIPolicyInstanceViolation(NotImplemented, instance2,
-                                                 cfg, _compute_fn=stub)
-    self.assertEqual(ret, [])
-    cfg_es = _FakeConfigForComputeIPolicyInstanceViolation(beparams, True)
-    stub_es = _StubComputeIPolicySpecViolation(2048, 2, 1, 0, [512], 13,
-                                               constants.DT_PLAIN)
-    ret = common.ComputeIPolicyInstanceViolation(NotImplemented, instance,
-                                                 cfg_es, _compute_fn=stub_es)
-    self.assertEqual(ret, [])
-    ret = common.ComputeIPolicyInstanceViolation(NotImplemented, instance2,
-                                                 cfg_es, _compute_fn=stub_es)
-    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,
-                                            constants.DT_PLAIN)
-    ret = instance._ComputeIPolicyInstanceSpecViolation(NotImplemented, ispec,
-                                                        constants.DT_PLAIN,
-                                                        _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 = instance_utils._ComputeIPolicyNodeViolation(
-      NotImplemented,
-      NotImplemented,
-      "foo", "foo", NotImplemented,
-      _compute_fn=self.recorder)
-    self.assertFalse(self.recorder.called)
-    self.assertEqual(ret, [])
-
-  def testDifferentGroup(self):
-    ret = instance_utils._ComputeIPolicyNodeViolation(
-      NotImplemented,
-      NotImplemented,
-      "foo", "bar", NotImplemented,
-      _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=[])
-    instance.CheckTargetNodeIPolicy(self.lu, NotImplemented, self.instance,
-                                    self.target_node, NotImplemented,
-                                    _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, instance.CheckTargetNodeIPolicy,
-                      self.lu, NotImplemented, self.instance,
-                      self.target_node, NotImplemented,
-                      _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"])
-    instance.CheckTargetNodeIPolicy(self.lu, NotImplemented, self.instance,
-                                     self.target_node, NotImplemented,
-                                     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 = []
-    instance._ApplyContainerMods("test", container, chgdesc, [], None, None,
-                                None)
-    self.assertEqual(container, [])
-    self.assertEqual(chgdesc, [])
-
-  def testAdd(self):
-    container = []
-    chgdesc = []
-    mods = instance._PrepareContainerMods([
-      (constants.DDM_ADD, -1, "Hello"),
-      (constants.DDM_ADD, -1, "World"),
-      (constants.DDM_ADD, 0, "Start"),
-      (constants.DDM_ADD, -1, "End"),
-      ], None)
-    instance._ApplyContainerMods("test", container, chgdesc, mods,
-                                None, None, None)
-    self.assertEqual(container, ["Start", "Hello", "World", "End"])
-    self.assertEqual(chgdesc, [])
-
-    mods = instance._PrepareContainerMods([
-      (constants.DDM_ADD, 0, "zero"),
-      (constants.DDM_ADD, 3, "Added"),
-      (constants.DDM_ADD, 5, "four"),
-      (constants.DDM_ADD, 7, "xyz"),
-      ], None)
-    instance._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 = instance._PrepareContainerMods([
-        (constants.DDM_ADD, idx, "error"),
-        ], None)
-      self.assertRaises(IndexError, instance._ApplyContainerMods,
-                        "test", container, None, mods, None, None, None)
-
-  def testRemoveError(self):
-    for idx in [0, 1, 2, 100, -1, -4]:
-      mods = instance._PrepareContainerMods([
-        (constants.DDM_REMOVE, idx, None),
-        ], None)
-      self.assertRaises(IndexError, instance._ApplyContainerMods,
-                        "test", [], None, mods, None, None, None)
-
-    mods = instance._PrepareContainerMods([
-      (constants.DDM_REMOVE, 0, object()),
-      ], None)
-    self.assertRaises(AssertionError, instance._ApplyContainerMods,
-                      "test", [""], None, mods, None, None, None)
-
-  def testAddError(self):
-    for idx in range(-100, -1) + [100]:
-      mods = instance._PrepareContainerMods([
-        (constants.DDM_ADD, idx, None),
-        ], None)
-      self.assertRaises(IndexError, instance._ApplyContainerMods,
-                        "test", [], None, mods, None, None, None)
-
-  def testRemove(self):
-    container = ["item 1", "item 2"]
-    mods = instance._PrepareContainerMods([
-      (constants.DDM_ADD, -1, "aaa"),
-      (constants.DDM_REMOVE, -1, None),
-      (constants.DDM_ADD, -1, "bbb"),
-      ], None)
-    chgdesc = []
-    instance._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 = instance._PrepareContainerMods([
-      (constants.DDM_MODIFY, -1, "a"),
-      (constants.DDM_MODIFY, 0, "b"),
-      (constants.DDM_MODIFY, 1, "c"),
-      ], None)
-    chgdesc = []
-    instance._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 = instance._PrepareContainerMods([
-        (constants.DDM_MODIFY, idx, "error"),
-        ], None)
-      self.assertRaises(IndexError, instance._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 = instance._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)
-    instance._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, enabled_disk_templates):
-    self._unique_id = itertools.count()
-    self._drbd_minor = itertools.count(20)
-    self._port = itertools.count(constants.FIRST_DRBD_PORT)
-    self._secret = itertools.count()
-    self._enabled_disk_templates = enabled_disk_templates
-
-  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"
-
-  def GetClusterInfo(self):
-    cluster = objects.Cluster()
-    cluster.enabled_disk_templates = self._enabled_disk_templates
-    return cluster
-
-
-class _FakeProcForGenDiskTemplate:
-  def GetECId(self):
-    return 0
-
-
-class TestGenerateDiskTemplate(unittest.TestCase):
-
-  def _SetUpLUWithTemplates(self, enabled_disk_templates):
-    self._enabled_disk_templates = enabled_disk_templates
-    cfg = _FakeConfigForGenDiskTemplate(self._enabled_disk_templates)
-    proc = _FakeProcForGenDiskTemplate()
-
-    self.lu = _FakeLU(cfg=cfg, proc=proc)
-
-  def setUp(self):
-    nodegroup = objects.NodeGroup(name="ng")
-    nodegroup.UpgradeConfig()
-
-    self._enabled_disk_templates = list(constants.DISK_TEMPLATES)
-    self._SetUpLUWithTemplates(self._enabled_disk_templates)
-    self.nodegroup = nodegroup
-
-  @staticmethod
-  def GetDiskParams():
-    return copy.deepcopy(constants.DISK_DT_DEFAULTS)
-
-  def testWrongDiskTemplate(self):
-    gdt = instance.GenerateDiskTemplate
-    disk_template = "##unknown##"
-
-    assert disk_template not in constants.DISK_TEMPLATES
-
-    self.assertRaises(errors.OpPrereqError, gdt, self.lu, disk_template,
-                      "inst26831.example.com", "node30113.example.com", [], [],
-                      NotImplemented, NotImplemented, 0, self.lu.LogInfo,
-                      self.GetDiskParams())
-
-  def testDiskless(self):
-    gdt = instance.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):
-    gdt = instance.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())
-
-    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())
-
-    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))
-    instance._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.DT_PLAIN)
-
-    self.assertEqual(map(operator.attrgetter("logical_id"), result), [
-      ("testvg", "ec0-uq0.disk3"),
-      ("othervg", "ec0-uq1.disk4"),
-      ])
-
-  def testFile(self):
-    # anything != DT_FILE would do here
-    self._SetUpLUWithTemplates([constants.DT_PLAIN])
-    self.assertRaises(errors.OpPrereqError, self._TestTrivialDisk,
-                      constants.DT_FILE, [], 0, NotImplemented)
-    self.assertRaises(errors.OpPrereqError, self._TestTrivialDisk,
-                      constants.DT_SHARED_FILE, [], 0, NotImplemented)
-
-    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,
-        }]
-
-      self._SetUpLUWithTemplates([disk_template])
-      result = self._TestTrivialDisk(disk_template, disk_info, 2,
-        disk_template, file_storage_dir="/tmp",
-        file_driver=constants.FD_BLKTAP)
-
-      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.DT_BLOCK)
-
-    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.DT_RBD)
-
-    self.assertEqual(map(operator.attrgetter("logical_id"), result), [
-      ("rbd", "ec0-uq0.rbd.disk0"),
-      ("rbd", "ec0-uq1.rbd.disk1"),
-      ])
-
-  def testDrbd8(self):
-    gdt = instance.GenerateDiskTemplate
-    drbd8_defaults = constants.DISK_LD_DEFAULTS[constants.DT_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.DT_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.DT_PLAIN)
-        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, constants.DRBD_META_SIZE)
-
-    self._CheckIvNames(result, 0, len(disk_info))
-    instance._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"),
-      ])
-
-
-class _ConfigForDiskWipe:
-  def __init__(self, exp_node_uuid):
-    self._exp_node_uuid = exp_node_uuid
-
-  def SetDiskID(self, device, node_uuid):
-    assert isinstance(device, objects.Disk)
-    assert node_uuid == self._exp_node_uuid
-
-  def GetNodeName(self, node_uuid):
-    assert node_uuid == self._exp_node_uuid
-    return "name.of.expected.node"
-
-
-class _RpcForDiskWipe:
-  def __init__(self, exp_node, pause_cb, wipe_cb):
-    self._exp_node = exp_node
-    self._pause_cb = pause_cb
-    self._wipe_cb = wipe_cb
-
-  def call_blockdev_pause_resume_sync(self, node, disks, pause):
-    assert node == self._exp_node
-    return rpc.RpcResult(data=self._pause_cb(disks, pause))
-
-  def call_blockdev_wipe(self, node, bdev, offset, size):
-    assert node == self._exp_node
-    return rpc.RpcResult(data=self._wipe_cb(bdev, offset, size))
-
-
-class _DiskPauseTracker:
-  def __init__(self):
-    self.history = []
-
-  def __call__(self, (disks, instance), pause):
-    assert not (set(disks) - set(instance.disks))
-
-    self.history.extend((i.logical_id, i.size, pause)
-                        for i in disks)
-
-    return (True, [True] * len(disks))
-
-
-class _DiskWipeProgressTracker:
-  def __init__(self, start_offset):
-    self._start_offset = start_offset
-    self.progress = {}
-
-  def __call__(self, (disk, _), offset, size):
-    assert isinstance(offset, (long, int))
-    assert isinstance(size, (long, int))
-
-    max_chunk_size = (disk.size / 100.0 * constants.MIN_WIPE_CHUNK_PERCENT)
-
-    assert offset >= self._start_offset
-    assert (offset + size) <= disk.size
-
-    assert size > 0
-    assert size <= constants.MAX_WIPE_CHUNK
-    assert size <= max_chunk_size
-
-    assert offset == self._start_offset or disk.logical_id in self.progress
-
-    # Keep track of progress
-    cur_progress = self.progress.setdefault(disk.logical_id, self._start_offset)
-
-    assert cur_progress == offset
-
-    # Record progress
-    self.progress[disk.logical_id] += size
-
-    return (True, None)
-
-
-class TestWipeDisks(unittest.TestCase):
-  def _FailingPauseCb(self, (disks, _), pause):
-    self.assertEqual(len(disks), 3)
-    self.assertTrue(pause)
-    # Simulate an RPC error
-    return (False, "error")
-
-  def testPauseFailure(self):
-    node_name = "node1372.example.com"
-
-    lu = _FakeLU(rpc=_RpcForDiskWipe(node_name, self._FailingPauseCb,
-                                     NotImplemented),
-                 cfg=_ConfigForDiskWipe(node_name))
-
-    disks = [
-      objects.Disk(dev_type=constants.DT_PLAIN),
-      objects.Disk(dev_type=constants.DT_PLAIN),
-      objects.Disk(dev_type=constants.DT_PLAIN),
-      ]
-
-    inst = objects.Instance(name="inst21201",
-                            primary_node=node_name,
-                            disk_template=constants.DT_PLAIN,
-                            disks=disks)
-
-    self.assertRaises(errors.OpExecError, instance.WipeDisks, lu, inst)
-
-  def _FailingWipeCb(self, (disk, _), offset, size):
-    # This should only ever be called for the first disk
-    self.assertEqual(disk.logical_id, "disk0")
-    return (False, None)
-
-  def testFailingWipe(self):
-    node_uuid = "node13445-uuid"
-    pt = _DiskPauseTracker()
-
-    lu = _FakeLU(rpc=_RpcForDiskWipe(node_uuid, pt, self._FailingWipeCb),
-                 cfg=_ConfigForDiskWipe(node_uuid))
-
-    disks = [
-      objects.Disk(dev_type=constants.DT_PLAIN, logical_id="disk0",
-                   size=100 * 1024),
-      objects.Disk(dev_type=constants.DT_PLAIN, logical_id="disk1",
-                   size=500 * 1024),
-      objects.Disk(dev_type=constants.DT_PLAIN, logical_id="disk2", size=256),
-      ]
-
-    inst = objects.Instance(name="inst562",
-                            primary_node=node_uuid,
-                            disk_template=constants.DT_PLAIN,
-                            disks=disks)
-
-    try:
-      instance.WipeDisks(lu, inst)
-    except errors.OpExecError, err:
-      self.assertTrue(str(err), "Could not wipe disk 0 at offset 0 ")
-    else:
-      self.fail("Did not raise exception")
-
-    # Check if all disks were paused and resumed
-    self.assertEqual(pt.history, [
-      ("disk0", 100 * 1024, True),
-      ("disk1", 500 * 1024, True),
-      ("disk2", 256, True),
-      ("disk0", 100 * 1024, False),
-      ("disk1", 500 * 1024, False),
-      ("disk2", 256, False),
-      ])
-
-  def _PrepareWipeTest(self, start_offset, disks):
-    node_name = "node-with-offset%s.example.com" % start_offset
-    pauset = _DiskPauseTracker()
-    progresst = _DiskWipeProgressTracker(start_offset)
-
-    lu = _FakeLU(rpc=_RpcForDiskWipe(node_name, pauset, progresst),
-                 cfg=_ConfigForDiskWipe(node_name))
-
-    instance = objects.Instance(name="inst3560",
-                                primary_node=node_name,
-                                disk_template=constants.DT_PLAIN,
-                                disks=disks)
-
-    return (lu, instance, pauset, progresst)
-
-  def testNormalWipe(self):
-    disks = [
-      objects.Disk(dev_type=constants.DT_PLAIN, logical_id="disk0", size=1024),
-      objects.Disk(dev_type=constants.DT_PLAIN, logical_id="disk1",
-                   size=500 * 1024),
-      objects.Disk(dev_type=constants.DT_PLAIN, logical_id="disk2", size=128),
-      objects.Disk(dev_type=constants.DT_PLAIN, logical_id="disk3",
-                   size=constants.MAX_WIPE_CHUNK),
-      ]
-
-    (lu, inst, pauset, progresst) = self._PrepareWipeTest(0, disks)
-
-    instance.WipeDisks(lu, inst)
-
-    self.assertEqual(pauset.history, [
-      ("disk0", 1024, True),
-      ("disk1", 500 * 1024, True),
-      ("disk2", 128, True),
-      ("disk3", constants.MAX_WIPE_CHUNK, True),
-      ("disk0", 1024, False),
-      ("disk1", 500 * 1024, False),
-      ("disk2", 128, False),
-      ("disk3", constants.MAX_WIPE_CHUNK, False),
-      ])
-
-    # Ensure the complete disk has been wiped
-    self.assertEqual(progresst.progress,
-                     dict((i.logical_id, i.size) for i in disks))
-
-  def testWipeWithStartOffset(self):
-    for start_offset in [0, 280, 8895, 1563204]:
-      disks = [
-        objects.Disk(dev_type=constants.DT_PLAIN, logical_id="disk0",
-                     size=128),
-        objects.Disk(dev_type=constants.DT_PLAIN, logical_id="disk1",
-                     size=start_offset + (100 * 1024)),
-        ]
-
-      (lu, inst, pauset, progresst) = \
-        self._PrepareWipeTest(start_offset, disks)
-
-      # Test start offset with only one disk
-      instance.WipeDisks(lu, inst,
-                         disks=[(1, disks[1], start_offset)])
-
-      # Only the second disk may have been paused and wiped
-      self.assertEqual(pauset.history, [
-        ("disk1", start_offset + (100 * 1024), True),
-        ("disk1", start_offset + (100 * 1024), False),
-        ])
-      self.assertEqual(progresst.progress, {
-        "disk1": disks[1].size,
-        })
-
-
-class TestDiskSizeInBytesToMebibytes(unittest.TestCase):
-  def testLessThanOneMebibyte(self):
-    for i in [1, 2, 7, 512, 1000, 1023]:
-      lu = _FakeLU()
-      result = instance_storage._DiskSizeInBytesToMebibytes(lu, i)
-      self.assertEqual(result, 1)
-      self.assertEqual(len(lu.warning_log), 1)
-      self.assertEqual(len(lu.warning_log[0]), 2)
-      (_, (warnsize, )) = lu.warning_log[0]
-      self.assertEqual(warnsize, (1024 * 1024) - i)
-
-  def testEven(self):
-    for i in [1, 2, 7, 512, 1000, 1023]:
-      lu = _FakeLU()
-      result = instance_storage._DiskSizeInBytesToMebibytes(lu,
-                                                            i * 1024 * 1024)
-      self.assertEqual(result, i)
-      self.assertFalse(lu.warning_log)
-
-  def testLargeNumber(self):
-    for i in [1, 2, 7, 512, 1000, 1023, 2724, 12420]:
-      for j in [1, 2, 486, 326, 986, 1023]:
-        lu = _FakeLU()
-        size = (1024 * 1024 * i) + j
-        result = instance_storage._DiskSizeInBytesToMebibytes(lu, size)
-        self.assertEqual(result, i + 1, msg="Amount was not rounded up")
-        self.assertEqual(len(lu.warning_log), 1)
-        self.assertEqual(len(lu.warning_log[0]), 2)
-        (_, (warnsize, )) = lu.warning_log[0]
-        self.assertEqual(warnsize, (1024 * 1024) - j)
-
-
-class TestCopyLockList(unittest.TestCase):
-  def test(self):
-    self.assertEqual(instance.CopyLockList([]), [])
-    self.assertEqual(instance.CopyLockList(None), None)
-    self.assertEqual(instance.CopyLockList(locking.ALL_SET), locking.ALL_SET)
-
-    names = ["foo", "bar"]
-    output = instance.CopyLockList(names)
-    self.assertEqual(names, output)
-    self.assertNotEqual(id(names), id(output), msg="List was not copied")
-
-
-class TestCheckOpportunisticLocking(unittest.TestCase):
-  class OpTest(opcodes.OpCode):
-    OP_PARAMS = [
-      opcodes._POpportunisticLocking,
-      opcodes._PIAllocFromDesc(""),
-      ]
-
-  @classmethod
-  def _MakeOp(cls, **kwargs):
-    op = cls.OpTest(**kwargs)
-    op.Validate(True)
-    return op
-
-  def testMissingAttributes(self):
-    self.assertRaises(AttributeError, instance._CheckOpportunisticLocking,
-                      object())
-
-  def testDefaults(self):
-    op = self._MakeOp()
-    instance._CheckOpportunisticLocking(op)
-
-  def test(self):
-    for iallocator in [None, "something", "other"]:
-      for opplock in [False, True]:
-        op = self._MakeOp(iallocator=iallocator,
-                          opportunistic_locking=opplock)
-        if opplock and not iallocator:
-          self.assertRaises(errors.OpPrereqError,
-                            instance._CheckOpportunisticLocking, op)
-        else:
-          instance._CheckOpportunisticLocking(op)
-
-
-class _OpTestVerifyErrors(opcodes.OpCode):
-  OP_PARAMS = [
-    opcodes._PDebugSimulateErrors,
-    opcodes._PErrorCodes,
-    opcodes._PIgnoreErrors,
-    ]
-
-
-class _LuTestVerifyErrors(cluster._VerifyErrors):
-  def __init__(self, **kwargs):
-    cluster._VerifyErrors.__init__(self)
-    self.op = _OpTestVerifyErrors(**kwargs)
-    self.op.Validate(True)
-    self.msglist = []
-    self._feedback_fn = self.msglist.append
-    self.bad = False
-
-  def DispatchCallError(self, which, *args, **kwargs):
-    if which:
-      self._Error(*args, **kwargs)
-    else:
-      self._ErrorIf(True, *args, **kwargs)
-
-  def CallErrorIf(self, c, *args, **kwargs):
-    self._ErrorIf(c, *args, **kwargs)
-
-
-class TestVerifyErrors(unittest.TestCase):
-  # Fake cluster-verify error code structures; we use two arbitary real error
-  # codes to pass validation of ignore_errors
-  (_, _ERR1ID, _) = constants.CV_ECLUSTERCFG
-  _NODESTR = "node"
-  _NODENAME = "mynode"
-  _ERR1CODE = (_NODESTR, _ERR1ID, "Error one")
-  (_, _ERR2ID, _) = constants.CV_ECLUSTERCERT
-  _INSTSTR = "instance"
-  _INSTNAME = "myinstance"
-  _ERR2CODE = (_INSTSTR, _ERR2ID, "Error two")
-  # Arguments used to call _Error() or _ErrorIf()
-  _ERR1ARGS = (_ERR1CODE, _NODENAME, "Error1 is %s", "an error")
-  _ERR2ARGS = (_ERR2CODE, _INSTNAME, "Error2 has no argument")
-  # Expected error messages
-  _ERR1MSG = _ERR1ARGS[2] % _ERR1ARGS[3]
-  _ERR2MSG = _ERR2ARGS[2]
-
-  def testNoError(self):
-    lu = _LuTestVerifyErrors()
-    lu.CallErrorIf(False, self._ERR1CODE, *self._ERR1ARGS)
-    self.assertFalse(lu.bad)
-    self.assertFalse(lu.msglist)
-
-  def _InitTest(self, **kwargs):
-    self.lu1 = _LuTestVerifyErrors(**kwargs)
-    self.lu2 = _LuTestVerifyErrors(**kwargs)
-
-  def _CallError(self, *args, **kwargs):
-    # Check that _Error() and _ErrorIf() produce the same results
-    self.lu1.DispatchCallError(True, *args, **kwargs)
-    self.lu2.DispatchCallError(False, *args, **kwargs)
-    self.assertEqual(self.lu1.bad, self.lu2.bad)
-    self.assertEqual(self.lu1.msglist, self.lu2.msglist)
-    # Test-specific checks are made on one LU
-    return self.lu1
-
-  def _checkMsgCommon(self, logstr, errmsg, itype, item, warning):
-    self.assertTrue(errmsg in logstr)
-    if warning:
-      self.assertTrue("WARNING" in logstr)
-    else:
-      self.assertTrue("ERROR" in logstr)
-    self.assertTrue(itype in logstr)
-    self.assertTrue(item in logstr)
-
-  def _checkMsg1(self, logstr, warning=False):
-    self._checkMsgCommon(logstr, self._ERR1MSG, self._NODESTR,
-                         self._NODENAME, warning)
-
-  def _checkMsg2(self, logstr, warning=False):
-    self._checkMsgCommon(logstr, self._ERR2MSG, self._INSTSTR,
-                         self._INSTNAME, warning)
-
-  def testPlain(self):
-    self._InitTest()
-    lu = self._CallError(*self._ERR1ARGS)
-    self.assertTrue(lu.bad)
-    self.assertEqual(len(lu.msglist), 1)
-    self._checkMsg1(lu.msglist[0])
-
-  def testMultiple(self):
-    self._InitTest()
-    self._CallError(*self._ERR1ARGS)
-    lu = self._CallError(*self._ERR2ARGS)
-    self.assertTrue(lu.bad)
-    self.assertEqual(len(lu.msglist), 2)
-    self._checkMsg1(lu.msglist[0])
-    self._checkMsg2(lu.msglist[1])
-
-  def testIgnore(self):
-    self._InitTest(ignore_errors=[self._ERR1ID])
-    lu = self._CallError(*self._ERR1ARGS)
-    self.assertFalse(lu.bad)
-    self.assertEqual(len(lu.msglist), 1)
-    self._checkMsg1(lu.msglist[0], warning=True)
-
-  def testWarning(self):
-    self._InitTest()
-    lu = self._CallError(*self._ERR1ARGS,
-                         code=_LuTestVerifyErrors.ETYPE_WARNING)
-    self.assertFalse(lu.bad)
-    self.assertEqual(len(lu.msglist), 1)
-    self._checkMsg1(lu.msglist[0], warning=True)
-
-  def testWarning2(self):
-    self._InitTest()
-    self._CallError(*self._ERR1ARGS)
-    lu = self._CallError(*self._ERR2ARGS,
-                         code=_LuTestVerifyErrors.ETYPE_WARNING)
-    self.assertTrue(lu.bad)
-    self.assertEqual(len(lu.msglist), 2)
-    self._checkMsg1(lu.msglist[0])
-    self._checkMsg2(lu.msglist[1], warning=True)
-
-  def testDebugSimulate(self):
-    lu = _LuTestVerifyErrors(debug_simulate_errors=True)
-    lu.CallErrorIf(False, *self._ERR1ARGS)
-    self.assertTrue(lu.bad)
-    self.assertEqual(len(lu.msglist), 1)
-    self._checkMsg1(lu.msglist[0])
-
-  def testErrCodes(self):
-    self._InitTest(error_codes=True)
-    lu = self._CallError(*self._ERR1ARGS)
-    self.assertTrue(lu.bad)
-    self.assertEqual(len(lu.msglist), 1)
-    self._checkMsg1(lu.msglist[0])
-    self.assertTrue(self._ERR1ID in lu.msglist[0])
-
-
-class TestGetUpdatedIPolicy(unittest.TestCase):
-  """Tests for cmdlib._GetUpdatedIPolicy()"""
-  _OLD_CLUSTER_POLICY = {
-    constants.IPOLICY_VCPU_RATIO: 1.5,
-    constants.ISPECS_MINMAX: [
-      {
-        constants.ISPECS_MIN: {
-          constants.ISPEC_MEM_SIZE: 32768,
-          constants.ISPEC_CPU_COUNT: 8,
-          constants.ISPEC_DISK_COUNT: 1,
-          constants.ISPEC_DISK_SIZE: 1024,
-          constants.ISPEC_NIC_COUNT: 1,
-          constants.ISPEC_SPINDLE_USE: 1,
-          },
-        constants.ISPECS_MAX: {
-          constants.ISPEC_MEM_SIZE: 65536,
-          constants.ISPEC_CPU_COUNT: 10,
-          constants.ISPEC_DISK_COUNT: 5,
-          constants.ISPEC_DISK_SIZE: 1024 * 1024,
-          constants.ISPEC_NIC_COUNT: 3,
-          constants.ISPEC_SPINDLE_USE: 12,
-          },
-        },
-      constants.ISPECS_MINMAX_DEFAULTS,
-      ],
-    constants.ISPECS_STD: constants.IPOLICY_DEFAULTS[constants.ISPECS_STD],
-    }
-  _OLD_GROUP_POLICY = {
-    constants.IPOLICY_SPINDLE_RATIO: 2.5,
-    constants.ISPECS_MINMAX: [{
-      constants.ISPECS_MIN: {
-        constants.ISPEC_MEM_SIZE: 128,
-        constants.ISPEC_CPU_COUNT: 1,
-        constants.ISPEC_DISK_COUNT: 1,
-        constants.ISPEC_DISK_SIZE: 1024,
-        constants.ISPEC_NIC_COUNT: 1,
-        constants.ISPEC_SPINDLE_USE: 1,
-        },
-      constants.ISPECS_MAX: {
-        constants.ISPEC_MEM_SIZE: 32768,
-        constants.ISPEC_CPU_COUNT: 8,
-        constants.ISPEC_DISK_COUNT: 5,
-        constants.ISPEC_DISK_SIZE: 1024 * 1024,
-        constants.ISPEC_NIC_COUNT: 3,
-        constants.ISPEC_SPINDLE_USE: 12,
-        },
-      }],
-    }
-
-  def _TestSetSpecs(self, old_policy, isgroup):
-    diff_minmax = [{
-      constants.ISPECS_MIN: {
-        constants.ISPEC_MEM_SIZE: 64,
-        constants.ISPEC_CPU_COUNT: 1,
-        constants.ISPEC_DISK_COUNT: 2,
-        constants.ISPEC_DISK_SIZE: 64,
-        constants.ISPEC_NIC_COUNT: 1,
-        constants.ISPEC_SPINDLE_USE: 1,
-        },
-      constants.ISPECS_MAX: {
-        constants.ISPEC_MEM_SIZE: 16384,
-        constants.ISPEC_CPU_COUNT: 10,
-        constants.ISPEC_DISK_COUNT: 12,
-        constants.ISPEC_DISK_SIZE: 1024,
-        constants.ISPEC_NIC_COUNT: 9,
-        constants.ISPEC_SPINDLE_USE: 18,
-        },
-      }]
-    diff_std = {
-        constants.ISPEC_DISK_COUNT: 10,
-        constants.ISPEC_DISK_SIZE: 512,
-        }
-    diff_policy = {
-      constants.ISPECS_MINMAX: diff_minmax
-      }
-    if not isgroup:
-      diff_policy[constants.ISPECS_STD] = diff_std
-    new_policy = common.GetUpdatedIPolicy(old_policy, diff_policy,
-                                          group_policy=isgroup)
-
-    self.assertTrue(constants.ISPECS_MINMAX in new_policy)
-    self.assertEqual(new_policy[constants.ISPECS_MINMAX], diff_minmax)
-    for key in old_policy:
-      if not key in diff_policy:
-        self.assertTrue(key in new_policy)
-        self.assertEqual(new_policy[key], old_policy[key])
-
-    if not isgroup:
-      new_std = new_policy[constants.ISPECS_STD]
-      for key in diff_std:
-        self.assertTrue(key in new_std)
-        self.assertEqual(new_std[key], diff_std[key])
-      old_std = old_policy.get(constants.ISPECS_STD, {})
-      for key in old_std:
-        self.assertTrue(key in new_std)
-        if key not in diff_std:
-          self.assertEqual(new_std[key], old_std[key])
-
-  def _TestSet(self, old_policy, diff_policy, isgroup):
-    new_policy = common.GetUpdatedIPolicy(old_policy, diff_policy,
-                                           group_policy=isgroup)
-    for key in diff_policy:
-      self.assertTrue(key in new_policy)
-      self.assertEqual(new_policy[key], diff_policy[key])
-    for key in old_policy:
-      if not key in diff_policy:
-        self.assertTrue(key in new_policy)
-        self.assertEqual(new_policy[key], old_policy[key])
-
-  def testSet(self):
-    diff_policy = {
-      constants.IPOLICY_VCPU_RATIO: 3,
-      constants.IPOLICY_DTS: [constants.DT_FILE],
-      }
-    self._TestSet(self._OLD_GROUP_POLICY, diff_policy, True)
-    self._TestSetSpecs(self._OLD_GROUP_POLICY, True)
-    self._TestSet({}, diff_policy, True)
-    self._TestSetSpecs({}, True)
-    self._TestSet(self._OLD_CLUSTER_POLICY, diff_policy, False)
-    self._TestSetSpecs(self._OLD_CLUSTER_POLICY, False)
-
-  def testUnset(self):
-    old_policy = self._OLD_GROUP_POLICY
-    diff_policy = {
-      constants.IPOLICY_SPINDLE_RATIO: constants.VALUE_DEFAULT,
-      }
-    new_policy = common.GetUpdatedIPolicy(old_policy, diff_policy,
-                                          group_policy=True)
-    for key in diff_policy:
-      self.assertFalse(key in new_policy)
-    for key in old_policy:
-      if not key in diff_policy:
-        self.assertTrue(key in new_policy)
-        self.assertEqual(new_policy[key], old_policy[key])
-
-    self.assertRaises(errors.OpPrereqError, common.GetUpdatedIPolicy,
-                      old_policy, diff_policy, group_policy=False)
-
-  def testUnsetEmpty(self):
-    old_policy = {}
-    for key in constants.IPOLICY_ALL_KEYS:
-      diff_policy = {
-        key: constants.VALUE_DEFAULT,
-        }
-    new_policy = common.GetUpdatedIPolicy(old_policy, diff_policy,
-                                          group_policy=True)
-    self.assertEqual(new_policy, old_policy)
-
-  def _TestInvalidKeys(self, old_policy, isgroup):
-    INVALID_KEY = "this_key_shouldnt_be_allowed"
-    INVALID_DICT = {
-      INVALID_KEY: 3,
-      }
-    invalid_policy = INVALID_DICT
-    self.assertRaises(errors.OpPrereqError, common.GetUpdatedIPolicy,
-                      old_policy, invalid_policy, group_policy=isgroup)
-    invalid_ispecs = {
-      constants.ISPECS_MINMAX: [INVALID_DICT],
-      }
-    self.assertRaises(errors.TypeEnforcementError, common.GetUpdatedIPolicy,
-                      old_policy, invalid_ispecs, group_policy=isgroup)
-    if isgroup:
-      invalid_for_group = {
-        constants.ISPECS_STD: constants.IPOLICY_DEFAULTS[constants.ISPECS_STD],
-        }
-      self.assertRaises(errors.OpPrereqError, common.GetUpdatedIPolicy,
-                        old_policy, invalid_for_group, group_policy=isgroup)
-    good_ispecs = self._OLD_CLUSTER_POLICY[constants.ISPECS_MINMAX]
-    invalid_ispecs = copy.deepcopy(good_ispecs)
-    invalid_policy = {
-      constants.ISPECS_MINMAX: invalid_ispecs,
-      }
-    for minmax in invalid_ispecs:
-      for key in constants.ISPECS_MINMAX_KEYS:
-        ispec = minmax[key]
-        ispec[INVALID_KEY] = None
-        self.assertRaises(errors.TypeEnforcementError,
-                          common.GetUpdatedIPolicy, old_policy,
-                          invalid_policy, group_policy=isgroup)
-        del ispec[INVALID_KEY]
-        for par in constants.ISPECS_PARAMETERS:
-          oldv = ispec[par]
-          ispec[par] = "this_is_not_good"
-          self.assertRaises(errors.TypeEnforcementError,
-                            common.GetUpdatedIPolicy,
-                            old_policy, invalid_policy, group_policy=isgroup)
-          ispec[par] = oldv
-    # This is to make sure that no two errors were present during the tests
-    common.GetUpdatedIPolicy(old_policy, invalid_policy,
-                             group_policy=isgroup)
-
-  def testInvalidKeys(self):
-    self._TestInvalidKeys(self._OLD_GROUP_POLICY, True)
-    self._TestInvalidKeys(self._OLD_CLUSTER_POLICY, False)
-
-  def testInvalidValues(self):
-    for par in (constants.IPOLICY_PARAMETERS |
-                frozenset([constants.IPOLICY_DTS])):
-      bad_policy = {
-        par: "invalid_value",
-        }
-      self.assertRaises(errors.OpPrereqError, common.GetUpdatedIPolicy, {},
-                        bad_policy, group_policy=True)
-
-if __name__ == "__main__":
-  testutils.GanetiTestProgram()
index 381db00..c45d768 100755 (executable)
@@ -242,6 +242,9 @@ class TestConfigRunner(unittest.TestCase):
         constants.ND_OOB_PROGRAM: "/bin/node-oob",
         constants.ND_SPINDLE_COUNT: 1,
         constants.ND_EXCLUSIVE_STORAGE: False,
+        constants.ND_OVS: True,
+        constants.ND_OVS_NAME: "openvswitch",
+        constants.ND_OVS_LINK: "eth1"
         }
 
     cfg = self._get_object()
@@ -253,15 +256,21 @@ class TestConfigRunner(unittest.TestCase):
   def testGetNdParamsInheritance(self):
     node_ndparams = {
       constants.ND_OOB_PROGRAM: "/bin/node-oob",
+      constants.ND_OVS_LINK: "eth3"
       }
     group_ndparams = {
       constants.ND_SPINDLE_COUNT: 10,
+      constants.ND_OVS: True,
+      constants.ND_OVS_NAME: "openvswitch",
       }
     expected_ndparams = {
       constants.ND_OOB_PROGRAM: "/bin/node-oob",
       constants.ND_SPINDLE_COUNT: 10,
       constants.ND_EXCLUSIVE_STORAGE:
         constants.NDC_DEFAULTS[constants.ND_EXCLUSIVE_STORAGE],
+      constants.ND_OVS: True,
+      constants.ND_OVS_NAME: "openvswitch",
+      constants.ND_OVS_LINK: "eth3"
       }
     cfg = self._get_object()
     node = cfg.GetNodeInfo(cfg.GetNodeList()[0])
index cb1cdf1..a7eee86 100755 (executable)
@@ -30,6 +30,8 @@ from ganeti import constants
 from ganeti import locking
 from ganeti import utils
 
+from ganeti.utils import version
+
 import testutils
 
 
@@ -46,16 +48,16 @@ class TestConstants(unittest.TestCase):
     self.failUnless(constants.CONFIG_VERSION >= 0 and
                     constants.CONFIG_VERSION <= 99999999)
 
-    self.failUnless(constants.BuildVersion(0, 0, 0) == 0)
-    self.failUnless(constants.BuildVersion(10, 10, 1010) == 10101010)
-    self.failUnless(constants.BuildVersion(12, 34, 5678) == 12345678)
-    self.failUnless(constants.BuildVersion(99, 99, 9999) == 99999999)
+    self.failUnless(version.BuildVersion(0, 0, 0) == 0)
+    self.failUnless(version.BuildVersion(10, 10, 1010) == 10101010)
+    self.failUnless(version.BuildVersion(12, 34, 5678) == 12345678)
+    self.failUnless(version.BuildVersion(99, 99, 9999) == 99999999)
 
-    self.failUnless(constants.SplitVersion(00000000) == (0, 0, 0))
-    self.failUnless(constants.SplitVersion(10101010) == (10, 10, 1010))
-    self.failUnless(constants.SplitVersion(12345678) == (12, 34, 5678))
-    self.failUnless(constants.SplitVersion(99999999) == (99, 99, 9999))
-    self.failUnless(constants.SplitVersion(constants.CONFIG_VERSION) ==
+    self.failUnless(version.SplitVersion(00000000) == (0, 0, 0))
+    self.failUnless(version.SplitVersion(10101010) == (10, 10, 1010))
+    self.failUnless(version.SplitVersion(12345678) == (12, 34, 5678))
+    self.failUnless(version.SplitVersion(99999999) == (99, 99, 9999))
+    self.failUnless(version.SplitVersion(constants.CONFIG_VERSION) ==
                     (constants.CONFIG_MAJOR, constants.CONFIG_MINOR,
                      constants.CONFIG_REVISION))
 
@@ -99,12 +101,6 @@ class TestConstants(unittest.TestCase):
     self.assertTrue(constants.DEFAULT_ENABLED_HYPERVISOR in
                     constants.HYPER_TYPES)
 
-  def testExtraLogfiles(self):
-    for daemon in constants.DAEMONS_EXTRA_LOGBASE:
-      self.assertTrue(daemon in constants.DAEMONS)
-      for log_reason in constants.DAEMONS_EXTRA_LOGBASE[daemon]:
-        self.assertTrue(log_reason in constants.VALID_EXTRA_LOGREASONS)
-
 
 class TestExportedNames(unittest.TestCase):
   _VALID_NAME_RE = re.compile(r"^[A-Z][A-Z0-9_]+$")
@@ -168,6 +164,12 @@ class TestDiskTemplateConstants(unittest.TestCase):
       self.assertTrue(
           constants.MAP_DISK_TEMPLATE_STORAGE_TYPE[disk_template] is not None)
 
+  def testLvmDiskTemplates(self):
+    lvm_by_storage_type = [
+        dt for dt in constants.DISK_TEMPLATES
+        if constants.ST_LVM_VG == constants.MAP_DISK_TEMPLATE_STORAGE_TYPE[dt]]
+    self.assertEqual(set(lvm_by_storage_type), set(constants.DTS_LVM))
+
 
 if __name__ == "__main__":
   testutils.GanetiTestProgram()
index c8eaffc..698c080 100755 (executable)
@@ -27,6 +27,7 @@ import unittest
 import socket
 import os
 import struct
+import re
 
 from ganeti import serializer
 from ganeti import constants
@@ -384,5 +385,52 @@ class TestProbeTapVnetHdr(unittest.TestCase):
     self.assertFalse(hv_kvm._ProbeTapVnetHdr(fd, _features_fn=lambda _: None))
 
 
+class TestGenerateDeviceKVMId(unittest.TestCase):
+  def test(self):
+    device = objects.NIC()
+    target = constants.HOTPLUG_TARGET_NIC
+    fn = hv_kvm._GenerateDeviceKVMId
+    self.assertRaises(errors.HotplugError, fn, target, device)
+
+    device.pci = 5
+    device.uuid = "003fc157-66a8-4e6d-8b7e-ec4f69751396"
+    self.assertTrue(re.match("hotnic-003fc157-pci-5", fn(target, device)))
+
+
+class TestGetRuntimeInfo(unittest.TestCase):
+  @classmethod
+  def _GetRuntime(cls):
+    data = testutils.ReadTestData("kvm_runtime.json")
+    return hv_kvm._AnalyzeSerializedRuntime(data)
+
+  def _fail(self, target, device, runtime):
+    device.uuid = "aaaaaaaa-66a8-4e6d-8b7e-ec4f69751396"
+    self.assertRaises(errors.HotplugError,
+                      hv_kvm._GetExistingDeviceInfo,
+                      target, device, runtime)
+
+  def testNIC(self):
+    device = objects.NIC()
+    target = constants.HOTPLUG_TARGET_NIC
+    runtime = self._GetRuntime()
+
+    self._fail(target, device, runtime)
+
+    device.uuid = "003fc157-66a8-4e6d-8b7e-ec4f69751396"
+    devinfo = hv_kvm._GetExistingDeviceInfo(target, device, runtime)
+    self.assertTrue(devinfo.pci==6)
+
+  def testDisk(self):
+    device = objects.Disk()
+    target = constants.HOTPLUG_TARGET_DISK
+    runtime = self._GetRuntime()
+
+    self._fail(target, device, runtime)
+
+    device.uuid = "9f5c5bd4-6f60-480b-acdc-9bb1a4b7df79"
+    (devinfo, _, __) = hv_kvm._GetExistingDeviceInfo(target, device, runtime)
+    self.assertTrue(devinfo.pci==5)
+
+
 if __name__ == "__main__":
   testutils.GanetiTestProgram()
index 60d44dd..c40e50c 100755 (executable)
@@ -269,7 +269,9 @@ class TestGetConfigFileDiskData(unittest.TestCase):
 
   def testManyDisks(self):
     for offset in [0, 1, 10]:
-      disks = [(objects.Disk(dev_type=constants.DT_PLAIN), "/tmp/disk/%s" % idx)
+      disks = [(objects.Disk(dev_type=constants.DT_PLAIN),
+               "/tmp/disk/%s" % idx,
+               NotImplemented)
                for idx in range(len(hv_xen._DISK_LETTERS) + offset)]
 
       if offset == 0:
@@ -289,9 +291,11 @@ class TestGetConfigFileDiskData(unittest.TestCase):
   def testTwoLvDisksWithMode(self):
     disks = [
       (objects.Disk(dev_type=constants.DT_PLAIN, mode=constants.DISK_RDWR),
-       "/tmp/diskFirst"),
+       "/tmp/diskFirst",
+       NotImplemented),
       (objects.Disk(dev_type=constants.DT_PLAIN, mode=constants.DISK_RDONLY),
-       "/tmp/diskLast"),
+       "/tmp/diskLast",
+       NotImplemented),
       ]
 
     result = hv_xen._GetConfigFileDiskData(disks, "hd")
@@ -303,17 +307,21 @@ class TestGetConfigFileDiskData(unittest.TestCase):
   def testFileDisks(self):
     disks = [
       (objects.Disk(dev_type=constants.DT_FILE, mode=constants.DISK_RDWR,
-                    physical_id=[constants.FD_LOOP]),
-       "/tmp/diskFirst"),
+                    logical_id=[constants.FD_LOOP]),
+       "/tmp/diskFirst",
+       NotImplemented),
       (objects.Disk(dev_type=constants.DT_FILE, mode=constants.DISK_RDONLY,
-                    physical_id=[constants.FD_BLKTAP]),
-       "/tmp/diskTwo"),
+                    logical_id=[constants.FD_BLKTAP]),
+       "/tmp/diskTwo",
+       NotImplemented),
       (objects.Disk(dev_type=constants.DT_FILE, mode=constants.DISK_RDWR,
-                    physical_id=[constants.FD_LOOP]),
-       "/tmp/diskThree"),
+                    logical_id=[constants.FD_LOOP]),
+       "/tmp/diskThree",
+       NotImplemented),
       (objects.Disk(dev_type=constants.DT_FILE, mode=constants.DISK_RDWR,
-                    physical_id=[constants.FD_BLKTAP]),
-       "/tmp/diskLast"),
+                    logical_id=[constants.FD_BLKTAP]),
+       "/tmp/diskLast",
+       NotImplemented),
       ]
 
     result = hv_xen._GetConfigFileDiskData(disks, "sd")
@@ -327,8 +335,9 @@ class TestGetConfigFileDiskData(unittest.TestCase):
   def testInvalidFileDisk(self):
     disks = [
       (objects.Disk(dev_type=constants.DT_FILE, mode=constants.DISK_RDWR,
-                    physical_id=["#unknown#"]),
-       "/tmp/diskinvalid"),
+                    logical_id=["#unknown#"]),
+       "/tmp/diskinvalid",
+       NotImplemented),
       ]
 
     self.assertRaises(KeyError, hv_xen._GetConfigFileDiskData, disks, "sd")
@@ -679,9 +688,11 @@ class _TestXenHypervisor(object):
 
     disks = [
       (objects.Disk(dev_type=constants.DT_PLAIN, mode=constants.DISK_RDWR),
-       utils.PathJoin(self.tmpdir, "disk0")),
+       utils.PathJoin(self.tmpdir, "disk0"),
+       NotImplemented),
       (objects.Disk(dev_type=constants.DT_PLAIN, mode=constants.DISK_RDONLY),
-       utils.PathJoin(self.tmpdir, "disk1")),
+       utils.PathJoin(self.tmpdir, "disk1"),
+       NotImplemented),
       ]
 
     inst = objects.Instance(name="server01.example.com",
index d3144b0..4bd613e 100755 (executable)
@@ -217,6 +217,32 @@ class TestProcessStorageInfo(unittest.TestCase):
     self.assertEqual(self.free_storage_lvm, free_disk)
     self.assertEqual(self.total_storage_lvm, total_disk)
 
+  def testComputeStorageDataFromSpaceInfoByTemplate(self):
+    disk_template = constants.DT_FILE
+    node_name = "mynode"
+    (total_disk, free_disk, total_spindles, free_spindles) = \
+        iallocator.IAllocator._ComputeStorageDataFromSpaceInfoByTemplate(
+            self.space_info, node_name, disk_template)
+    self.assertEqual(self.free_storage_file, free_disk)
+    self.assertEqual(self.total_storage_file, total_disk)
+
+  def testComputeStorageDataFromSpaceInfoByTemplateLvm(self):
+    disk_template = constants.DT_PLAIN
+    node_name = "mynode"
+    (total_disk, free_disk, total_spindles, free_spindles) = \
+        iallocator.IAllocator._ComputeStorageDataFromSpaceInfoByTemplate(
+            self.space_info, node_name, disk_template)
+    self.assertEqual(self.free_storage_lvm, free_disk)
+    self.assertEqual(self.total_storage_lvm, total_disk)
+
+  def testComputeStorageDataFromSpaceInfoByTemplateNoReport(self):
+    disk_template = constants.DT_DISKLESS
+    node_name = "mynode"
+    (total_disk, free_disk, total_spindles, free_spindles) = \
+        iallocator.IAllocator._ComputeStorageDataFromSpaceInfoByTemplate(
+            self.space_info, node_name, disk_template)
+    self.assertEqual(0, free_disk)
+    self.assertEqual(0, total_disk)
 
 if __name__ == "__main__":
   testutils.GanetiTestProgram()
index db80e07..9dbe022 100755 (executable)
@@ -167,6 +167,9 @@ class TestClusterObject(unittest.TestCase):
         constants.ND_OOB_PROGRAM: "/bin/group-oob",
         constants.ND_SPINDLE_COUNT: 10,
         constants.ND_EXCLUSIVE_STORAGE: True,
+        constants.ND_OVS: True,
+        constants.ND_OVS_LINK: "eth2",
+        constants.ND_OVS_NAME: "openvswitch",
         }
     fake_group = objects.NodeGroup(name="testgroup",
                                    ndparams=group_ndparams)
@@ -178,6 +181,9 @@ class TestClusterObject(unittest.TestCase):
         constants.ND_OOB_PROGRAM: "/bin/node-oob",
         constants.ND_SPINDLE_COUNT: 2,
         constants.ND_EXCLUSIVE_STORAGE: True,
+        constants.ND_OVS: True,
+        constants.ND_OVS_LINK: "eth2",
+        constants.ND_OVS_NAME: "openvswitch",
         }
     fake_node = objects.Node(name="test",
                              ndparams=node_ndparams,
@@ -192,6 +198,9 @@ class TestClusterObject(unittest.TestCase):
         constants.ND_OOB_PROGRAM: "/bin/node-oob",
         constants.ND_SPINDLE_COUNT: 5,
         constants.ND_EXCLUSIVE_STORAGE: True,
+        constants.ND_OVS: True,
+        constants.ND_OVS_LINK: "eth2",
+        constants.ND_OVS_NAME: "openvswitch",
         }
     fake_node = objects.Node(name="test",
                              ndparams=node_ndparams,
index 98ec769..63658c3 100755 (executable)
@@ -27,6 +27,7 @@ import unittest
 
 from ganeti import utils
 from ganeti import opcodes
+from ganeti import opcodes_base
 from ganeti import ht
 from ganeti import constants
 from ganeti import errors
@@ -35,16 +36,6 @@ 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 = compat.UniqueFrozenset([
-  opcodes.OpTestAllocator,
-  opcodes.OpTestDelay,
-  opcodes.OpTestDummy,
-  opcodes.OpTestJqueue,
-  ])
-
-
 class TestOpcodes(unittest.TestCase):
   def test(self):
     self.assertRaises(ValueError, opcodes.OpCode.LoadOpCode, None)
@@ -56,16 +47,12 @@ class TestOpcodes(unittest.TestCase):
       self.assert_(cls.OP_ID.startswith("OP_"))
       self.assert_(len(cls.OP_ID) > 3)
       self.assertEqual(cls.OP_ID, cls.OP_ID.upper())
-      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()))
-      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.assertEqual(cls.OP_ID, opcodes_base._NameToId(cls.__name__))
+      self.assertFalse(
+        compat.any(cls.OP_ID.startswith(prefix)
+                   for prefix in opcodes_base.SUMMARY_PREFIX.keys()))
+      self.assertTrue(callable(cls.OP_RESULT),
+                      msg=("%s should have a result check" % cls.OP_ID))
 
       self.assertRaises(TypeError, cls, unsupported_parameter="some value")
 
@@ -132,10 +119,11 @@ class TestOpcodes(unittest.TestCase):
     self.assertEqual(OpTest(data="b").Summary(), "TEST(a)")
 
   def testTinySummary(self):
-    self.assertFalse(utils.FindDuplicates(opcodes._SUMMARY_PREFIX.values()))
+    self.assertFalse(
+      utils.FindDuplicates(opcodes_base.SUMMARY_PREFIX.values()))
     self.assertTrue(compat.all(prefix.endswith("_") and supplement.endswith("_")
                                for (prefix, supplement) in
-                                 opcodes._SUMMARY_PREFIX.items()))
+                                 opcodes_base.SUMMARY_PREFIX.items()))
 
     self.assertEqual(opcodes.OpClusterPostInit().TinySummary(), "C_POST_INIT")
     self.assertEqual(opcodes.OpNodeRemove().TinySummary(), "N_REMOVE")
@@ -164,7 +152,7 @@ class TestOpcodes(unittest.TestCase):
   def testParams(self):
     supported_by_all = set(["debug_level", "dry_run", "priority"])
 
-    self.assertTrue(opcodes.BaseOpCode not in opcodes.OP_MAPPING.values())
+    self.assertTrue(opcodes_base.BaseOpCode not in opcodes.OP_MAPPING.values())
     self.assertTrue(opcodes.OpCode not in opcodes.OP_MAPPING.values())
 
     for cls in opcodes.OP_MAPPING.values() + [opcodes.OpCode]:
@@ -195,7 +183,7 @@ class TestOpcodes(unittest.TestCase):
       # Check parameter definitions
       for attr_name, aval, test, doc in cls.GetAllParams():
         self.assert_(attr_name)
-        self.assert_(test is None or test is ht.NoType or callable(test),
+        self.assertTrue(callable(test),
                      msg=("Invalid type check for %s.%s" %
                           (cls.OP_ID, attr_name)))
         self.assertTrue(doc is None or isinstance(doc, basestring))
@@ -206,13 +194,9 @@ class TestOpcodes(unittest.TestCase):
                            msg=("Default value of %s.%s returned by function"
                                 " is callable" % (cls.OP_ID, attr_name)))
         else:
-          self.assertFalse(isinstance(aval, (list, dict, set)),
-                           msg=("Default value of %s.%s is mutable (%s)" %
-                                (cls.OP_ID, attr_name, repr(aval))))
-
           default_value = aval
 
-        if aval is not ht.NoDefault and test is not ht.NoType:
+        if aval is not ht.NoDefault and aval is not None:
           self.assertTrue(test(default_value),
                           msg=("Default value of %s.%s does not verify" %
                                (cls.OP_ID, attr_name)))
@@ -225,10 +209,10 @@ class TestOpcodes(unittest.TestCase):
   def testValidateNoModification(self):
     class OpTest(opcodes.OpCode):
       OP_PARAMS = [
-        ("nodef", ht.NoDefault, ht.TMaybeString, None),
+        ("nodef", None, ht.TString, None),
         ("wdef", "default", ht.TMaybeString, None),
         ("number", 0, ht.TInt, None),
-        ("notype", None, ht.NoType, None),
+        ("notype", None, ht.TAny, None),
         ]
 
     # Missing required parameter "nodef"
@@ -285,11 +269,8 @@ class TestOpcodes(unittest.TestCase):
   def testValidateSetDefaults(self):
     class OpTest(opcodes.OpCode):
       OP_PARAMS = [
-        # Static default value
         ("value1", "default", ht.TMaybeString, None),
-
-        # Default value callback
-        ("value2", lambda: "result", ht.TMaybeString, None),
+        ("value2", "result", ht.TMaybeString, None),
         ]
 
     op = OpTest()
@@ -328,8 +309,8 @@ class TestOpcodes(unittest.TestCase):
 
 class TestOpcodeDepends(unittest.TestCase):
   def test(self):
-    check_relative = opcodes._BuildJobDepCheck(True)
-    check_norelative = opcodes.TNoRelativeJobDependencies
+    check_relative = opcodes_base.BuildJobDepCheck(True)
+    check_norelative = opcodes_base.TNoRelativeJobDependencies
 
     for fn in [check_relative, check_norelative]:
       self.assertTrue(fn(None))
@@ -362,60 +343,34 @@ class TestResultChecks(unittest.TestCase):
   def testJobIdList(self):
     for i in [[], [(False, "error")], [(False, "")],
               [(True, 123), (True, "999")]]:
-      self.assertTrue(opcodes.TJobIdList(i))
+      self.assertTrue(ht.TJobIdList(i))
 
     for i in ["", [("x", 1)], [[], []], [[False, "", None], [True, 123]]]:
-      self.assertFalse(opcodes.TJobIdList(i))
+      self.assertFalse(ht.TJobIdList(i))
 
   def testJobIdListOnly(self):
-    self.assertTrue(opcodes.TJobIdListOnly({
+    self.assertTrue(ht.TJobIdListOnly({
       constants.JOB_IDS_KEY: [],
       }))
-    self.assertTrue(opcodes.TJobIdListOnly({
+    self.assertTrue(ht.TJobIdListOnly({
       constants.JOB_IDS_KEY: [(True, "9282")],
       }))
 
-    self.assertFalse(opcodes.TJobIdListOnly({
+    self.assertFalse(ht.TJobIdListOnly({
       "x": None,
       }))
-    self.assertFalse(opcodes.TJobIdListOnly({
+    self.assertFalse(ht.TJobIdListOnly({
       constants.JOB_IDS_KEY: [],
       "x": None,
       }))
-    self.assertFalse(opcodes.TJobIdListOnly({
+    self.assertFalse(ht.TJobIdListOnly({
       constants.JOB_IDS_KEY: [("foo", "bar")],
       }))
-    self.assertFalse(opcodes.TJobIdListOnly({
+    self.assertFalse(ht.TJobIdListOnly({
       constants.JOB_IDS_KEY: [("one", "two", "three")],
       }))
 
 
-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([]))
@@ -433,7 +388,7 @@ class TestOpInstanceSetParams(unittest.TestCase):
     self.assertFalse(fn([[constants.DDM_ADD]]))
 
   def testNicModifications(self):
-    fn = opcodes.OpInstanceSetParams.TestNicModifications
+    fn = ht.TSetParamsMods(ht.TINicParams)
     self._GenericTests(fn)
 
     for param in constants.INIC_PARAMS:
@@ -441,7 +396,7 @@ class TestOpInstanceSetParams(unittest.TestCase):
       self.assertTrue(fn([[constants.DDM_ADD, {param: param}]]))
 
   def testDiskModifications(self):
-    fn = opcodes.OpInstanceSetParams.TestDiskModifications
+    fn = ht.TSetParamsMods(ht.TIDiskParams)
     self._GenericTests(fn)
 
     for param in constants.IDISK_PARAMS:
index a219673..75833da 100755 (executable)
@@ -503,8 +503,8 @@ class GanetiRapiClientTests(testutils.GanetiTestCase):
   def testInstancesMultiAlloc(self):
     response = {
       constants.JOB_IDS_KEY: ["23423"],
-      opcodes.OpInstanceMultiAlloc.ALLOCATABLE_KEY: ["foobar"],
-      opcodes.OpInstanceMultiAlloc.FAILED_KEY: ["foobar2"],
+      constants.ALLOCATABLE_KEY: ["foobar"],
+      constants.FAILED_KEY: ["foobar2"],
       }
     self.rapi.AddResponse(serializer.DumpJson(response))
     insts = [self.client.InstanceAllocation("create", "foobar",
index cb19811..0109f02 100755 (executable)
@@ -261,7 +261,7 @@ class TestNodeEvacuate(unittest.TestCase):
     handler = _CreateHandler(rlib2.R_2_nodes_name_evacuate, ["node92"], {
       "dry-run": ["1"],
       }, {
-      "mode": constants.IALLOCATOR_NEVAC_SEC,
+      "mode": constants.NODE_EVAC_SEC,
       }, clfactory)
     job_id = handler.POST()
 
@@ -272,7 +272,7 @@ class TestNodeEvacuate(unittest.TestCase):
     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.assertEqual(op.mode, constants.NODE_EVAC_SEC)
     self.assertTrue(op.dry_run)
 
     self.assertRaises(IndexError, cl.GetNextSubmittedJob)
@@ -630,11 +630,12 @@ class TestStorageQuery(unittest.TestCase):
   def testErrors(self):
     clfactory = _FakeClientFactory(_FakeClient)
 
+    # storage type which does not support space reporting
     queryargs = {
-      "output_fields": "name,other",
+      "storage_type": constants.ST_DISKLESS,
       }
     handler = _CreateHandler(rlib2.R_2_nodes_name_storage,
-                             ["node10538"], queryargs, {}, clfactory)
+                             ["node21273"], queryargs, {}, clfactory)
     self.assertRaises(http.HttpBadRequest, handler.GET)
 
     queryargs = {
@@ -1011,7 +1012,6 @@ class TestInstanceCreation(testutils.GanetiTestCase):
       "disks": [],
       "nics": [],
       "mode": constants.INSTANCE_CREATE,
-      "disk_template": constants.DT_PLAIN,
       }
 
     for name in reqfields.keys():
index 38a63d6..841d5ae 100755 (executable)
@@ -39,6 +39,7 @@ import testutils
 
 KNOWN_UNUSED_LUXI = compat.UniqueFrozenset([
   luxi.REQ_SUBMIT_MANY_JOBS,
+  luxi.REQ_SUBMIT_JOB_TO_DRAINED_QUEUE,
   luxi.REQ_ARCHIVE_JOB,
   luxi.REQ_AUTO_ARCHIVE_JOBS,
   luxi.REQ_CHANGE_JOB_PRIORITY,
@@ -112,8 +113,6 @@ class TestVerifyOpResult(unittest.TestCase):
   def testNoResultCheck(self):
     vor = rapi.testutils.VerifyOpResult
 
-    assert opcodes.OpTestDummy.OP_RESULT is None
-
     vor(opcodes.OpTestDummy.OP_ID, None)
 
 
index 38faa02..e8312bd 100755 (executable)
@@ -481,11 +481,11 @@ class TestNodeConfigResolver(unittest.TestCase):
 class TestCompress(unittest.TestCase):
   def test(self):
     for data in ["", "Hello", "Hello World!\nnew\nlines"]:
-      self.assertEqual(rpc._Compress(data),
+      self.assertEqual(rpc._Compress(NotImplemented, data),
                        (constants.RPC_ENCODING_NONE, data))
 
     for data in [512 * " ", 5242 * "Hello World!\n"]:
-      compressed = rpc._Compress(data)
+      compressed = rpc._Compress(NotImplemented, data)
       self.assertEqual(len(compressed), 2)
       self.assertEqual(backend._Decompress(compressed), data)
 
@@ -566,8 +566,8 @@ class TestRpcClientBase(unittest.TestCase):
       ]
 
     encoders = {
-      AT1: hex,
-      AT2: hash,
+      AT1: lambda _, value: hex(value),
+      AT2: lambda _, value: hash(value),
       }
 
     cdef = ("test_call", NotImplemented, None, constants.RPC_TMO_NORMAL, [
@@ -720,6 +720,9 @@ class _FakeConfigForRpcRunner:
   def GetNodeInfo(self, name):
     return objects.Node(name=name)
 
+  def GetMultiNodeInfo(self, names):
+    return [(name, self.GetNodeInfo(name)) for name in names]
+
   def GetClusterInfo(self):
     return self._cluster
 
@@ -736,11 +739,15 @@ class TestRpcRunner(unittest.TestCase):
     tmpfile.flush()
     st = os.stat(tmpfile.name)
 
+    nodes = [
+      "node1.example.com",
+      ]
+
     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(list(uldata[1]), list(rpc._Compress(nodes[0], data)))
       self.assertEqual(uldata[2], st.st_mode)
       self.assertEqual(uldata[3], "user%s" % os.getuid())
       self.assertEqual(uldata[4], "group%s" % os.getgid())
@@ -761,10 +768,6 @@ class TestRpcRunner(unittest.TestCase):
                                   _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))
@@ -832,14 +835,15 @@ class TestRpcRunner(unittest.TestCase):
                        "mymode")
 
     # Generic object serialization
-    result = runner._encoder((rpc_defs.ED_OBJECT_DICT, inst))
+    result = runner._encoder(NotImplemented, (rpc_defs.ED_OBJECT_DICT, inst))
     _CheckBasics(result)
 
-    result = runner._encoder((rpc_defs.ED_OBJECT_DICT_LIST, 5 * [inst]))
+    result = runner._encoder(NotImplemented,
+                             (rpc_defs.ED_OBJECT_DICT_LIST, 5 * [inst]))
     map(_CheckBasics, result)
 
     # Just an instance
-    result = runner._encoder((rpc_defs.ED_INST_DICT, inst))
+    result = runner._encoder(NotImplemented, (rpc_defs.ED_INST_DICT, inst))
     _CheckBasics(result)
     self.assertEqual(result["beparams"][constants.BE_MAXMEM], 256)
     self.assertEqual(result["hvparams"][constants.HT_KVM], {
@@ -851,10 +855,11 @@ class TestRpcRunner(unittest.TestCase):
       })
 
     # Instance with OS parameters
-    result = runner._encoder((rpc_defs.ED_INST_DICT_OSP_DP, (inst, {
-      "role": "webserver",
-      "other": "field",
-      })))
+    result = runner._encoder(NotImplemented,
+                             (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], {
@@ -867,7 +872,8 @@ class TestRpcRunner(unittest.TestCase):
       })
 
     # Instance with hypervisor and backend parameters
-    result = runner._encoder((rpc_defs.ED_INST_DICT_HVP_BEP_DP, (inst, {
+    result = runner._encoder(NotImplemented,
+                             (rpc_defs.ED_INST_DICT_HVP_BEP_DP, (inst, {
       constants.HT_KVM: {
         constants.HV_BOOT_ORDER: "xyz",
         },
@@ -883,11 +889,13 @@ class TestRpcRunner(unittest.TestCase):
       })
     self.assertEqual(result["disks"], [{
       "dev_type": constants.DT_PLAIN,
+      "dynamic_params": {},
       "size": 4096,
       "logical_id": ("vg", "disk6120"),
       "params": constants.DISK_DT_DEFAULTS[inst.disk_template],
       }, {
       "dev_type": constants.DT_PLAIN,
+      "dynamic_params": {},
       "size": 1024,
       "logical_id": ("vg", "disk8508"),
       "params": constants.DISK_DT_DEFAULTS[inst.disk_template],
@@ -899,94 +907,58 @@ class TestRpcRunner(unittest.TestCase):
 
 class TestLegacyNodeInfo(unittest.TestCase):
   KEY_BOOT = "bootid"
-  KEY_VG0 = "name"
-  KEY_VG1 = "storage_free"
-  KEY_VG2 = "storage_size"
-  KEY_HV = "cpu_count"
-  KEY_SP1 = "spindles_free"
-  KEY_SP2 = "spindles_total"
-  KEY_ST = "type" # key for storage type
+  KEY_NAME = "name"
+  KEY_STORAGE_FREE = "storage_free"
+  KEY_STORAGE_TOTAL = "storage_size"
+  KEY_CPU_COUNT = "cpu_count"
+  KEY_SPINDLES_FREE = "spindles_free"
+  KEY_SPINDLES_TOTAL = "spindles_total"
+  KEY_STORAGE_TYPE = "type" # key for storage type
   VAL_BOOT = 0
-  VAL_VG0 = "xy"
-  VAL_VG1 = 11
-  VAL_VG2 = 12
-  VAL_VG3 = "lvm-vg"
-  VAL_HV = 2
-  VAL_SP0 = "ab"
-  VAL_SP1 = 31
-  VAL_SP2 = 32
-  VAL_SP3 = "lvm-pv"
+  VAL_VG_NAME = "xy"
+  VAL_VG_FREE = 11
+  VAL_VG_TOTAL = 12
+  VAL_VG_TYPE = "lvm-vg"
+  VAL_CPU_COUNT = 2
+  VAL_PV_NAME = "ab"
+  VAL_PV_FREE = 31
+  VAL_PV_TOTAL = 32
+  VAL_PV_TYPE = "lvm-pv"
   DICT_VG = {
-    KEY_VG0: VAL_VG0,
-    KEY_VG1: VAL_VG1,
-    KEY_VG2: VAL_VG2,
-    KEY_ST: VAL_VG3,
+    KEY_NAME: VAL_VG_NAME,
+    KEY_STORAGE_FREE: VAL_VG_FREE,
+    KEY_STORAGE_TOTAL: VAL_VG_TOTAL,
+    KEY_STORAGE_TYPE: VAL_VG_TYPE,
     }
-  DICT_HV = {KEY_HV: VAL_HV}
+  DICT_HV = {KEY_CPU_COUNT: VAL_CPU_COUNT}
   DICT_SP = {
-    KEY_ST: VAL_SP3,
-    KEY_VG0: VAL_SP0,
-    KEY_VG1: VAL_SP1,
-    KEY_VG2: VAL_SP2,
+    KEY_STORAGE_TYPE: VAL_PV_TYPE,
+    KEY_NAME: VAL_PV_NAME,
+    KEY_STORAGE_FREE: VAL_PV_FREE,
+    KEY_STORAGE_TOTAL: VAL_PV_TOTAL,
     }
   STD_LST = [VAL_BOOT, [DICT_VG, DICT_SP], [DICT_HV]]
   STD_DICT = {
     KEY_BOOT: VAL_BOOT,
-    KEY_VG0: VAL_VG0,
-    KEY_VG1: VAL_VG1,
-    KEY_VG2: VAL_VG2,
-    KEY_HV: VAL_HV,
+    KEY_NAME: VAL_VG_NAME,
+    KEY_STORAGE_FREE: VAL_VG_FREE,
+    KEY_STORAGE_TOTAL: VAL_VG_TOTAL,
+    KEY_SPINDLES_FREE: VAL_PV_FREE,
+    KEY_SPINDLES_TOTAL: VAL_PV_TOTAL,
+    KEY_CPU_COUNT: VAL_CPU_COUNT,
     }
 
-  def testStandard(self):
-    result = rpc.MakeLegacyNodeInfo(self.STD_LST)
+  def testWithSpindles(self):
+    result = rpc.MakeLegacyNodeInfo(self.STD_LST, constants.DT_PLAIN)
     self.assertEqual(result, self.STD_DICT)
 
-  def testSpindlesRequired(self):
-    my_lst = [self.VAL_BOOT, [], [self.DICT_HV]]
-    self.assertRaises(errors.OpExecError, rpc.MakeLegacyNodeInfo, my_lst,
-        require_spindles=True)
-
-  def testNoSpindlesRequired(self):
-    my_lst = [self.VAL_BOOT, [], [self.DICT_HV]]
-    result = rpc.MakeLegacyNodeInfo(my_lst, require_spindles = False)
-    self.assertEqual(result, {self.KEY_BOOT: self.VAL_BOOT,
-                              self.KEY_HV: self.VAL_HV})
-    result = rpc.MakeLegacyNodeInfo(self.STD_LST, require_spindles = False)
-    self.assertEqual(result, self.STD_DICT)
-
-
-class TestAddDefaultStorageInfoToLegacyNodeInfo(unittest.TestCase):
-
-  def setUp(self):
-    self.free_storage_file = 23
-    self.total_storage_file = 42
-    self.free_storage_lvm = 69
-    self.total_storage_lvm = 666
-    self.node_info = [{"name": "myfile",
-                       "type": constants.ST_FILE,
-                       "storage_free": self.free_storage_file,
-                       "storage_size": self.total_storage_file},
-                      {"name": "myvg",
-                       "type": constants.ST_LVM_VG,
-                       "storage_free": self.free_storage_lvm,
-                       "storage_size": self.total_storage_lvm},
-                      {"name": "myspindle",
-                       "type": constants.ST_LVM_PV,
-                       "storage_free": 33,
-                       "storage_size": 44}]
-
-  def testAddDefaultStorageInfoToLegacyNodeInfo(self):
-    result = {}
-    rpc._AddDefaultStorageInfoToLegacyNodeInfo(result, self.node_info)
-    self.assertEqual(self.free_storage_file, result["storage_free"])
-    self.assertEqual(self.total_storage_file, result["storage_size"])
-
-  def testAddDefaultStorageInfoToLegacyNodeInfoNoDefaults(self):
-    result = {}
-    rpc._AddDefaultStorageInfoToLegacyNodeInfo(result, self.node_info[-1:])
-    self.assertFalse("storage_free" in result)
-    self.assertFalse("storage_size" in result)
+  def testNoSpindles(self):
+    my_lst = [self.VAL_BOOT, [self.DICT_VG], [self.DICT_HV]]
+    result = rpc.MakeLegacyNodeInfo(my_lst, constants.DT_PLAIN)
+    expected_dict = dict((k,v) for k, v in self.STD_DICT.iteritems())
+    expected_dict[self.KEY_SPINDLES_FREE] = 0
+    expected_dict[self.KEY_SPINDLES_TOTAL] = 0
+    self.assertEqual(result, expected_dict)
 
 
 if __name__ == "__main__":
index 73f4143..160dfb2 100755 (executable)
@@ -41,6 +41,7 @@ def _StubGetpwnam(user):
     constants.CONFD_USER: _EntStub(uid=1),
     constants.RAPI_USER: _EntStub(uid=2),
     constants.NODED_USER: _EntStub(uid=3),
+    constants.LUXID_USER: _EntStub(uid=4),
     }
   return users[user]
 
@@ -53,6 +54,7 @@ def _StubGetgrnam(group):
     constants.DAEMONS_GROUP: _EntStub(gid=3),
     constants.ADMIN_GROUP: _EntStub(gid=4),
     constants.NODED_GROUP: _EntStub(gid=5),
+    constants.LUXID_GROUP: _EntStub(gid=6),
     }
   return groups[group]
 
index 41e6487..68d8c61 100755 (executable)
@@ -419,12 +419,18 @@ class TestDRBD8Construction(testutils.GanetiTestCase):
         filename=testutils.TestDataFilename("proc_drbd84.txt"))
 
     self.test_unique_id = ("hosta.com", 123, "host2.com", 123, 0, "secret")
+    self.test_dyn_params = {
+      constants.DDP_LOCAL_IP: "192.0.2.1",
+      constants.DDP_LOCAL_MINOR: 0,
+      constants.DDP_REMOTE_IP: "192.0.2.2",
+      constants.DDP_REMOTE_MINOR: 0,
+    }
 
   @testutils.patch_object(drbd.DRBD8, "GetProcInfo")
   def testConstructionWith80Data(self, mock_create_from_file):
     mock_create_from_file.return_value = self.proc80_info
 
-    inst = drbd.DRBD8Dev(self.test_unique_id, [], 123, {})
+    inst = drbd.DRBD8Dev(self.test_unique_id, [], 123, {}, self.test_dyn_params)
     self.assertEqual(inst._show_info_cls, drbd_info.DRBD83ShowInfo)
     self.assertTrue(isinstance(inst._cmd_gen, drbd_cmdgen.DRBD83CmdGenerator))
 
@@ -432,7 +438,7 @@ class TestDRBD8Construction(testutils.GanetiTestCase):
   def testConstructionWith83Data(self, mock_create_from_file):
     mock_create_from_file.return_value = self.proc83_info
 
-    inst = drbd.DRBD8Dev(self.test_unique_id, [], 123, {})
+    inst = drbd.DRBD8Dev(self.test_unique_id, [], 123, {}, self.test_dyn_params)
     self.assertEqual(inst._show_info_cls, drbd_info.DRBD83ShowInfo)
     self.assertTrue(isinstance(inst._cmd_gen, drbd_cmdgen.DRBD83CmdGenerator))
 
@@ -440,7 +446,7 @@ class TestDRBD8Construction(testutils.GanetiTestCase):
   def testConstructionWith84Data(self, mock_create_from_file):
     mock_create_from_file.return_value = self.proc84_info
 
-    inst = drbd.DRBD8Dev(self.test_unique_id, [], 123, {})
+    inst = drbd.DRBD8Dev(self.test_unique_id, [], 123, {}, self.test_dyn_params)
     self.assertEqual(inst._show_info_cls, drbd_info.DRBD84ShowInfo)
     self.assertTrue(isinstance(inst._cmd_gen, drbd_cmdgen.DRBD84CmdGenerator))
 
index cbcc719..1f2a697 100755 (executable)
@@ -26,7 +26,6 @@ import mock
 import unittest
 
 from ganeti import constants
-from ganeti import objects
 from ganeti.utils import storage
 
 import testutils
@@ -72,38 +71,23 @@ class TestGetStorageUnitForDiskTemplate(unittest.TestCase):
     self.assertEqual(storage_type, constants.ST_DISKLESS)
     self.assertEqual(storage_key, None)
 
-  def testGetDefaultStorageUnitForSpindles(self):
-    (storage_type, storage_key) = \
-        storage._GetDefaultStorageUnitForSpindles(self._cfg)
-    self.assertEqual(storage_type, constants.ST_LVM_PV)
-    self.assertEqual(storage_key, self._default_vg_name)
-
 
-class TestGetStorageUnitsOfCluster(unittest.TestCase):
+class TestGetStorageUnits(unittest.TestCase):
 
   def setUp(self):
     storage._GetDefaultStorageUnitForDiskTemplate = \
         mock.Mock(return_value=("foo", "bar"))
-
-    self._cluster_cfg = objects.Cluster()
-    self._enabled_disk_templates = \
-        [constants.DT_DRBD8, constants.DT_PLAIN, constants.DT_FILE,
-         constants.DT_SHARED_FILE]
-    self._cluster_cfg.enabled_disk_templates = \
-        self._enabled_disk_templates
     self._cfg = mock.Mock()
-    self._cfg.GetClusterInfo = mock.Mock(return_value=self._cluster_cfg)
-    self._cfg.GetVGName = mock.Mock(return_value="some_vg_name")
-
-  def testGetStorageUnitsOfCluster(self):
-    storage_units = storage.GetStorageUnitsOfCluster(self._cfg)
-    self.assertEqual(len(storage_units), len(self._enabled_disk_templates))
-
-  def testGetStorageUnitsOfClusterWithSpindles(self):
-    storage_units = storage.GetStorageUnitsOfCluster(
-        self._cfg, include_spindles=True)
-    self.assertEqual(len(storage_units), len(self._enabled_disk_templates) + 1)
-    self.assertTrue(constants.ST_LVM_PV in [st for (st, sk) in storage_units])
+
+  def testGetStorageUnits(self):
+    disk_templates = [constants.DT_FILE, constants.DT_SHARED_FILE]
+    storage_units = storage.GetStorageUnits(self._cfg, disk_templates)
+    self.assertEqual(len(storage_units), len(disk_templates))
+
+  def testGetStorageUnitsLvm(self):
+    disk_templates = [constants.DT_PLAIN, constants.DT_DRBD8]
+    storage_units = storage.GetStorageUnits(self._cfg, disk_templates)
+    self.assertEqual(len(storage_units), len(disk_templates))
 
 
 class TestLookupSpaceInfoByStorageType(unittest.TestCase):
index b99bcfd..d10a3b7 100755 (executable)
@@ -445,6 +445,24 @@ class TestUnescapeAndSplit(unittest.TestCase):
       b = ["a", "b" + sep + "c", "d" + sep + "e" + sep + "f", "g"]
       self.failUnlessEqual(utils.UnescapeAndSplit(sep.join(a), sep=sep), b)
 
+class TestEscapeAndJoin(unittest.TestCase):
+  def verifyParsesCorrect(self, args):
+    for sep in [",", "+", ".", ":"]:
+      self.assertEqual(utils.UnescapeAndSplit(
+          utils.EscapeAndJoin(args, sep=sep),
+          sep=sep), args)
+
+  def test(self):
+    self.verifyParsesCorrect(["a", "b", "c"])
+    self.verifyParsesCorrect(["2.10.0", "12345"])
+    self.verifyParsesCorrect(["2.10.0~alpha1", "12345"])
+    self.verifyParsesCorrect(["..:", ",,+"])
+    self.verifyParsesCorrect(["a\\", "b\\\\", "c"])
+    self.verifyParsesCorrect(["a"])
+    self.verifyParsesCorrect(["+"])
+    self.verifyParsesCorrect(["\\"])
+    self.verifyParsesCorrect(["\\\\"])
+
 
 class TestCommaJoin(unittest.TestCase):
   def test(self):
diff --git a/test/py/ganeti.utils.version_unittest.py b/test/py/ganeti.utils.version_unittest.py
new file mode 100755 (executable)
index 0000000..06b878f
--- /dev/null
@@ -0,0 +1,76 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2013 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 unittesting the version utility functions"""
+
+import unittest
+
+from ganeti.utils import version
+
+import testutils
+
+class ParseVersionTest(unittest.TestCase):
+    def testParseVersion(self):
+        self.assertEquals(version.ParseVersion("2.10"), (2, 10, 0))
+        self.assertEquals(version.ParseVersion("2.10.1"), (2, 10, 1))
+        self.assertEquals(version.ParseVersion("2.10.1~beta2"), (2, 10, 1))
+        self.assertEquals(version.ParseVersion("2"), None)
+        self.assertEquals(version.ParseVersion("pink bunny"), None)
+
+class UpgradeRangeTest(unittest.TestCase):
+    def testUpgradeRange(self):
+        self.assertEquals(version.UpgradeRange((2,11,0), current=(2,10,0)),
+                          None)
+        self.assertEquals(version.UpgradeRange((2,10,0), current=(2,10,0)),
+                          None)
+        self.assertEquals(version.UpgradeRange((2,11,3), current=(2,12,0)),
+                          None)
+        self.assertEquals(version.UpgradeRange((2,11,3), current=(2,12,99)),
+                          None)
+        self.assertEquals(version.UpgradeRange((3,0,0), current=(2,12,0)),
+                          "different major versions")
+        self.assertEquals(version.UpgradeRange((2,12,0), current=(3,0,0)),
+                          "different major versions")
+        self.assertEquals(version.UpgradeRange((2,10,0), current=(2,12,0)),
+                          "can only downgrade one minor version at a time")
+        self.assertEquals(version.UpgradeRange((2,9,0), current=(2,10,0)),
+                          "automatic upgrades only supported from 2.10 onwards")
+        self.assertEquals(version.UpgradeRange((2,10,0), current=(2,9,0)),
+                          "automatic upgrades only supported from 2.10 onwards")
+
+class ShouldCfgdowngradeTest(unittest.TestCase):
+    def testShouldCfgDowngrade(self):
+        self.assertTrue(version.ShouldCfgdowngrade((2,9,3), current=(2,10,0)))
+        self.assertTrue(version.ShouldCfgdowngrade((2,9,0), current=(2,10,4)))
+        self.assertFalse(version.ShouldCfgdowngrade((2,9,0), current=(2,11,0)))
+        self.assertFalse(version.ShouldCfgdowngrade((2,9,0), current=(3,10,0)))
+        self.assertFalse(version.ShouldCfgdowngrade((2,10,0), current=(3,10,0)))
+
+
+class IsCorrectConfigVersionTest(unittest.TestCase):
+    def testIsCorrectConfigVersion(self):
+        self.assertTrue(version.IsCorrectConfigVersion((2,10,1), (2,10,0)))
+        self.assertFalse(version.IsCorrectConfigVersion((2,11,0), (2,10,0)))
+        self.assertFalse(version.IsCorrectConfigVersion((3,10,0), (2,10,0)))
+
+
+if __name__ == "__main__":
+  testutils.GanetiTestProgram()
index f57b713..2fc0ee5 100755 (executable)
@@ -192,7 +192,8 @@ class TestForceDictType(unittest.TestCase):
     self.assertEqual(self._fdt({"b": "True"}), {"b": True})
     self.assertEqual(self._fdt({"d": "4"}), {"d": 4})
     self.assertEqual(self._fdt({"d": "4M"}), {"d": 4})
-    self.assertEqual(self._fdt({"e": None, }), {"e": None, })
+    self.assertEqual(self._fdt({"e": constants.VALUE_HS_NOTHING, }), {"e":
+                               constants.VALUE_HS_NOTHING, })
     self.assertEqual(self._fdt({"e": "Hello World", }), {"e": "Hello World", })
     self.assertEqual(self._fdt({"e": False, }), {"e": "", })
     self.assertEqual(self._fdt({"b": "hello", }, ["hello"]), {"b": "hello"})
index 21481de..d06d671 100644 (file)
@@ -24,7 +24,6 @@
 
 import os
 
-from ganeti import utils
 from ganeti import netutils
 
 
@@ -37,7 +36,7 @@ FAKE_CLUSTER_KEY = ("AAAAB3NzaC1yc2EAAAABIwAAAQEAsuGLw70et3eApJ/ZEJkAVZogIrm"
                     "vYdB2nQds7/+Bf40C/OpbvnAxna1kVtgFHAo18cQ==")
 
 
-class FakeConfig:
+class FakeConfig(object):
   """Fake configuration object"""
 
   def IsCluster(self):
@@ -61,7 +60,7 @@ class FakeConfig:
   def GetMasterNodeName(self):
     return netutils.Hostname.GetSysName()
 
-  def GetDefaultIAllocator(Self):
+  def GetDefaultIAllocator(self):
     return "testallocator"
 
   def GetNodeName(self, node_uuid):
@@ -74,7 +73,7 @@ class FakeConfig:
     return map(self.GetNodeName, node_uuids)
 
 
-class FakeProc:
+class FakeProc(object):
   """Fake processor object"""
 
   def Log(self, msg, *args, **kwargs):
@@ -90,14 +89,14 @@ class FakeProc:
     pass
 
 
-class FakeGLM:
+class FakeGLM(object):
   """Fake global lock manager object"""
 
-  def list_owned(self, level):
+  def list_owned(self, _):
     return set()
 
 
-class FakeContext:
+class FakeContext(object):
   """Fake context object"""
 
   def __init__(self):
index 8249847..033daa8 100644 (file)
@@ -27,7 +27,6 @@ import stat
 import tempfile
 import unittest
 import logging
-import types
 
 from ganeti import utils
 
@@ -103,18 +102,6 @@ class GanetiTestProgram(unittest.TestProgram):
     else:
       raise Exception("Assertion not evaluated")
 
-    # The following piece of code is a backport from Python 2.6. Python 2.4/2.5
-    # only accept class instances as test runners. Being able to pass classes
-    # reduces the amount of code necessary for using a custom test runner.
-    # 2.6 and above should use their own code, however.
-    if (self.testRunner and sys.hexversion < 0x2060000 and
-        isinstance(self.testRunner, (type, types.ClassType))):
-      try:
-        self.testRunner = self.testRunner(verbosity=self.verbosity)
-      except TypeError:
-        # didn't accept the verbosity argument
-        self.testRunner = self.testRunner()
-
     return unittest.TestProgram.runTests(self)
 
 
@@ -132,7 +119,7 @@ class GanetiTestCase(unittest.TestCase):
     while self._temp_files:
       try:
         utils.RemoveFile(self._temp_files.pop())
-      except EnvironmentError, err:
+      except EnvironmentError:
         pass
 
   def assertFileContent(self, file_name, expected_content):
@@ -219,8 +206,10 @@ def patch_object(*args, **kwargs):
   """
   import mock
   try:
+    # pylint: disable=W0212
     return mock._patch_object(*args, **kwargs)
   except AttributeError:
+    # pylint: disable=E1101
     return mock.patch_object(*args, **kwargs)
 
 
index a7be848..1135103 100755 (executable)
@@ -44,6 +44,8 @@ from ganeti import config
 from ganeti import netutils
 from ganeti import pathutils
 
+from ganeti.utils import version
+
 
 options = None
 args = None
@@ -52,11 +54,11 @@ args = None
 #: Target major version we will upgrade to
 TARGET_MAJOR = 2
 #: Target minor version we will upgrade to
-TARGET_MINOR = 9
+TARGET_MINOR = 10
 #: Target major version for downgrade
 DOWNGRADE_MAJOR = 2
 #: Target minor version for downgrade
-DOWNGRADE_MINOR = 8
+DOWNGRADE_MINOR = 9
 
 # map of legacy device types
 # (mapping differing old LD_* constants to new DT_* constants)
@@ -179,6 +181,14 @@ def GetExclusiveStorageValue(config_data):
   return ret
 
 
+def RemovePhysicalId(disk):
+  if "children" in disk:
+    for d in disk["children"]:
+      RemovePhysicalId(d)
+  if "physical_id" in disk:
+    del disk["physical_id"]
+
+
 def ChangeDiskDevType(disk, dev_type_map):
   """Replaces disk's dev_type attributes according to the given map.
 
@@ -219,6 +229,8 @@ def UpgradeInstances(config_data):
       raise Error("Instance '%s' doesn't have a disks entry?!" % instance)
     disks = iobj["disks"]
     for idx, dobj in enumerate(disks):
+      RemovePhysicalId(dobj)
+
       expected = "disk/%s" % idx
       current = dobj.get("iv_name", "")
       if current != expected:
@@ -370,8 +382,7 @@ def UpgradeInstanceIndices(config_data):
 
 
 def UpgradeAll(config_data):
-  config_data["version"] = constants.BuildVersion(TARGET_MAJOR,
-                                                  TARGET_MINOR, 0)
+  config_data["version"] = version.BuildVersion(TARGET_MAJOR, TARGET_MINOR, 0)
   UpgradeRapiUsers()
   UpgradeWatcher()
   UpgradeFileStoragePaths(config_data)
@@ -383,66 +394,28 @@ def UpgradeAll(config_data):
   UpgradeInstanceIndices(config_data)
 
 
-def DowngradeDiskDevType(disk):
-  """Downgrades the disks' device type."""
-  ChangeDiskDevType(disk, DEV_TYPE_NEW_OLD)
-
-
-def DowngradeDisks(disks, owner):
-  for disk in disks:
-    # Remove spindles to downgrade to 2.8
-    if "spindles" in disk:
-      logging.warning("Removing spindles (value=%s) from disk %s (%s) of"
-                      " instance %s",
-                      disk["spindles"], disk["iv_name"], disk["uuid"], owner)
-      del disk["spindles"]
-    if "dev_type" in disk:
-      DowngradeDiskDevType(disk)
-
-
 def DowngradeInstances(config_data):
   if "instances" not in config_data:
     raise Error("Cannot find the 'instances' key in the configuration!")
   for (iname, iobj) in config_data["instances"].items():
-    if "disks" not in iobj:
-      raise Error("Cannot find 'disks' key for instance %s" % iname)
-    DowngradeDisks(iobj["disks"], iname)
-
-
-def DowngradeNodeIndices(config_data):
-  ChangeNodeIndices(config_data, "uuid", "name")
+    DowngradeNicParamsVLAN(iobj["nics"], iname)
 
 
-def DowngradeInstanceIndices(config_data):
-  ChangeInstanceIndices(config_data, "uuid", "name")
-
-
-def DowngradeHvparams(config_data):
-  """Downgrade the cluster's hypervisor parameters."""
-  cluster = config_data["cluster"]
-  if "hvparams" in cluster:
-    hvparams = cluster["hvparams"]
-    xen_params = None
-    for xen_variant in [constants.HT_XEN_PVM, constants.HT_XEN_HVM]:
-      if xen_variant in hvparams:
-        xen_params = hvparams[xen_variant]
-        # 'xen_cmd' was introduced in 2.9
-        if constants.HV_XEN_CMD in xen_params:
-          del xen_params[constants.HV_XEN_CMD]
-        # 'vif_script' was introducted in 2.9
-        if constants.HV_VIF_SCRIPT in xen_params:
-          del xen_params[constants.HV_VIF_SCRIPT]
+def DowngradeNicParamsVLAN(nics, owner):
+  for nic in nics:
+    vlan = nic["nicparams"].get("vlan", None)
+    if vlan:
+      logging.warning("Instance with name %s found. Removing VLAN information"
+                      " %s.", owner, vlan)
+      del nic["nicparams"]["vlan"]
 
 
 def DowngradeAll(config_data):
   # Any code specific to a particular version should be labeled that way, so
   # it can be removed when updating to the next version.
-  config_data["version"] = constants.BuildVersion(DOWNGRADE_MAJOR,
-                                                  DOWNGRADE_MINOR, 0)
+  config_data["version"] = version.BuildVersion(DOWNGRADE_MAJOR,
+                                                DOWNGRADE_MINOR, 0)
   DowngradeInstances(config_data)
-  DowngradeNodeIndices(config_data)
-  DowngradeInstanceIndices(config_data)
-  DowngradeHvparams(config_data)
 
 
 def main():
@@ -543,7 +516,7 @@ def main():
     raise Error("Unable to determine configuration version")
 
   (config_major, config_minor, config_revision) = \
-    constants.SplitVersion(config_version)
+    version.SplitVersion(config_version)
 
   logging.info("Found configuration version %s (%d.%d.%d)",
                config_version, config_major, config_minor, config_revision)
@@ -563,7 +536,7 @@ def main():
                    config_minor, config_revision))
     DowngradeAll(config_data)
 
-  # Upgrade from 2.{0..7} to 2.9
+  # Upgrade from 2.{0..9} to 2.10
   elif config_major == 2 and config_minor in range(0, 10):
     if config_revision != 0:
       logging.warning("Config revision is %s, not 0", config_revision)
index dd76ce9..f684c9e 100755 (executable)
@@ -49,6 +49,8 @@ from ganeti import utils
 from ganeti import cli
 from ganeti import pathutils
 
+from ganeti.utils import version
+
 
 options = None
 args = None
@@ -345,7 +347,7 @@ def main():
       raise Error("Unsupported configuration version: %s" %
                   old_config_version)
     if "version" not in config_data:
-      config_data["version"] = constants.BuildVersion(2, 0, 0)
+      config_data["version"] = version.BuildVersion(2, 0, 0)
     if F_SERIAL not in config_data:
       config_data[F_SERIAL] = 1
 
index f6bd138..066440e 100755 (executable)
@@ -400,10 +400,6 @@ class Merger(object):
             logical_id[2] = port
             dsk.logical_id = tuple(logical_id)
 
-            physical_id = list(dsk.physical_id)
-            physical_id[1] = physical_id[3] = port
-            dsk.physical_id = tuple(physical_id)
-
         my_config.AddInstance(instance_info,
                               _CLUSTERMERGE_ECID + str(fake_ec_id))
         fake_ec_id += 1
index b0b8a3e..30208ac 100755 (executable)
@@ -110,6 +110,11 @@ DEST_SECONDARY_NODE_OPT = \
                  help=("Secondary node on destination cluster (only"
                        " when moving exactly one instance)"))
 
+DEST_DISK_TEMPLATE_OPT = \
+  cli.cli_option("--dest-disk-template", action="store", type="string",
+                 dest="dest_disk_template", default=None,
+                 help="Disk template to use on destination cluster")
+
 PARALLEL_OPT = \
   cli.cli_option("-p", "--parallel", action="store", type="int", default=1,
                  dest="parallel", metavar="<number>",
@@ -272,7 +277,8 @@ class InstanceMove(object):
   """
   def __init__(self, src_instance_name, dest_instance_name,
                dest_pnode, dest_snode, dest_iallocator,
-               hvparams, beparams, osparams, nics):
+               dest_disk_template, hvparams,
+               beparams, osparams, nics):
     """Initializes this class.
 
     @type src_instance_name: string
@@ -285,6 +291,8 @@ class InstanceMove(object):
     @param dest_snode: Name of secondary node on destination cluster
     @type dest_iallocator: string or None
     @param dest_iallocator: Name of iallocator to use
+    @type dest_disk_template: string or None
+    @param dest_disk_template: Disk template to use instead of the original one
     @type hvparams: dict or None
     @param hvparams: Hypervisor parameters to override
     @type beparams: dict or None
@@ -300,6 +308,7 @@ class InstanceMove(object):
     self.dest_pnode = dest_pnode
     self.dest_snode = dest_snode
     self.dest_iallocator = dest_iallocator
+    self.dest_disk_template = dest_disk_template
     self.hvparams = hvparams
     self.beparams = beparams
     self.osparams = osparams
@@ -429,6 +438,7 @@ class MoveDestExecutor(object):
     job_id = self._CreateInstance(dest_client, mrt.move.dest_instance_name,
                                   mrt.move.dest_pnode, mrt.move.dest_snode,
                                   mrt.move.dest_iallocator,
+                                  mrt.move.dest_disk_template,
                                   mrt.src_instinfo, mrt.src_expinfo,
                                   mrt.move.hvparams, mrt.move.beparams,
                                   mrt.move.beparams, mrt.move.nics)
@@ -454,9 +464,9 @@ class MoveDestExecutor(object):
       mrt.dest_to_source.release()
 
   @staticmethod
-  def _CreateInstance(cl, name, pnode, snode, iallocator, instance, expinfo,
-                      override_hvparams, override_beparams, override_osparams,
-                      override_nics):
+  def _CreateInstance(cl, name, pnode, snode, iallocator, dest_disk_template,
+                      instance, expinfo, override_hvparams, override_beparams,
+                      override_osparams, override_nics):
     """Starts the instance creation in remote import mode.
 
     @type cl: L{rapi.client.GanetiRapiClient}
@@ -469,6 +479,8 @@ class MoveDestExecutor(object):
     @param snode: Name of secondary node on destination cluster
     @type iallocator: string or None
     @param iallocator: Name of iallocator to use
+    @type dest_disk_template: string or None
+    @param dest_disk_template: Disk template to use instead of the original one
     @type instance: dict
     @param instance: Instance details from source cluster
     @type expinfo: dict
@@ -484,7 +496,10 @@ class MoveDestExecutor(object):
     @return: Job ID
 
     """
-    disk_template = instance["disk_template"]
+    if dest_disk_template:
+      disk_template = dest_disk_template
+    else:
+      disk_template = instance["disk_template"]
 
     disks = []
     for idisk in instance["disks"]:
@@ -503,9 +518,11 @@ class MoveDestExecutor(object):
       constants.INIC_MAC: mac,
       constants.INIC_MODE: mode,
       constants.INIC_LINK: link,
+      constants.INIC_NAME: vlan,
       constants.INIC_NETWORK: network,
       constants.INIC_NAME: nic_name
-      } for nic_name, _, ip, mac, mode, link, network, _ in instance["nics"]]
+      } for nic_name, _, ip, mac, mode, link, vlan, network, _
+        in instance["nics"]]
 
     if len(override_nics) > len(nics):
       raise Error("Can not create new NICs")
@@ -759,6 +776,7 @@ def ParseOptions():
   parser.add_option(DEST_INSTANCE_NAME_OPT)
   parser.add_option(DEST_PRIMARY_NODE_OPT)
   parser.add_option(DEST_SECONDARY_NODE_OPT)
+  parser.add_option(DEST_DISK_TEMPLATE_OPT)
   parser.add_option(PARALLEL_OPT)
 
   (options, args) = parser.parse_args()
@@ -787,17 +805,12 @@ def CheckOptions(parser, options, args):
   if options.parallel < 1:
     parser.error("Number of simultaneous moves must be >= 1")
 
-  if not (bool(options.iallocator) ^
-          bool(options.dest_primary_node or options.dest_secondary_node)):
+  if (bool(options.iallocator) and
+      bool(options.dest_primary_node or options.dest_secondary_node)):
     parser.error("Destination node and iallocator options exclude each other")
 
   if len(instance_names) == 1:
     # Moving one instance only
-    if not (options.iallocator or
-            options.dest_primary_node or
-            options.dest_secondary_node):
-      parser.error("An iallocator or the destination node is required")
-
     if options.hvparams:
       utils.ForceDictType(options.hvparams, constants.HVS_PARAMETER_TYPES)
 
@@ -816,13 +829,26 @@ def CheckOptions(parser, options, args):
                    " --backend-parameters, --os-parameters and --net can"
                    " only be used when moving exactly one instance")
 
-    if not options.iallocator:
-      parser.error("An iallocator must be specified for moving more than one"
-                   " instance")
-
   return (src_cluster_name, dest_cluster_name, instance_names)
 
 
+def DestClusterHasDefaultIAllocator(rapi_factory):
+  """Determines if a given cluster has a default iallocator.
+
+  """
+  result = rapi_factory.GetDestClient().GetInfo()
+  ia_name = "default_iallocator"
+  return ia_name in result and result[ia_name]
+
+
+def ExitWithError(message):
+  """Exits after an error and shows a message.
+
+  """
+  sys.stderr.write("move-instance: error: " + message + "\n")
+  sys.exit(constants.EXIT_FAILURE)
+
+
 @UsesRapiClient
 def main():
   """Main routine.
@@ -843,14 +869,17 @@ def main():
 
   CheckRapiSetup(rapi_factory)
 
-  assert (len(instance_names) == 1 or
-          not (options.dest_primary_node or options.dest_secondary_node))
-  assert len(instance_names) == 1 or options.iallocator
-  assert (len(instance_names) > 1 or options.iallocator or
-          options.dest_primary_node or options.dest_secondary_node)
-  assert (len(instance_names) == 1 or
-          not (options.hvparams or options.beparams or options.osparams or
-               options.nics))
+  has_iallocator = options.iallocator or \
+                   DestClusterHasDefaultIAllocator(rapi_factory)
+
+  if len(instance_names) > 1 and not has_iallocator:
+    ExitWithError("When moving multiple nodes, an iallocator must be used. "
+                  "None was provided and the target cluster does not have "
+                  "a default iallocator.")
+  if (len(instance_names) == 1 and not (has_iallocator or
+      options.dest_primary_node or options.dest_secondary_node)):
+    ExitWithError("Target cluster does not have a default iallocator, "
+                  "please specify either destination nodes or an iallocator.")
 
   # Prepare list of instance moves
   moves = []
@@ -865,8 +894,11 @@ def main():
     moves.append(InstanceMove(src_instance_name, dest_instance_name,
                               options.dest_primary_node,
                               options.dest_secondary_node,
-                              options.iallocator, options.hvparams,
-                              options.beparams, options.osparams,
+                              options.iallocator,
+                              options.dest_disk_template,
+                              options.hvparams,
+                              options.beparams,
+                              options.osparams,
                               options.nics))
 
   assert len(moves) == len(instance_names)
index 5556ffb..385cea3 100755 (executable)
@@ -199,13 +199,8 @@ def SanitizeDisks(opts, cfg): # pylint: disable=W0613
       for child in disk["children"]:
         helper(child)
 
-    if disk["dev_type"] == constants.DT_DRBD8:
-      if "physical_id" in disk:
-        del disk["physical_id"]
-
     if disk["dev_type"] == constants.DT_PLAIN and opts.sanitize_lvs:
       disk["logical_id"][1] = _Get(disk["logical_id"][1])
-      disk["physical_id"][1] = disk["logical_id"][1]
 
   lv_map = {}
 
@@ -279,9 +274,7 @@ def main():
 
   config_data = serializer.LoadJson(utils.ReadFile(opts.CONFIG_DATA_PATH))
 
-  # first, do some disk cleanup: remove DRBD physical_ids, since it
-  # contains both IPs (which we want changed) and the DRBD secret, and
-  # it's not needed for normal functioning, and randomize LVM names
+  # Randomize LVM names
   SanitizeDisks(opts, config_data)
 
   SanitizeSecrets(opts, config_data)