Merge branch 'devel-2.2' into devel-2.3
authorMichael Hanselmann <hansmi@google.com>
Tue, 30 Nov 2010 18:26:46 +0000 (19:26 +0100)
committerMichael Hanselmann <hansmi@google.com>
Tue, 30 Nov 2010 18:55:56 +0000 (19:55 +0100)
* devel-2.2:
  Correct version check for release candidates
  Fix version check
  Add script to check version format

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

106 files changed:
.gitignore
INSTALL
Makefile.am
NEWS
autotools/build-bash-completion
configure.ac
daemons/daemon-util.in [changed mode: 0755->0644]
daemons/ensure-dirs.in
daemons/ganeti-cleaner.in [changed mode: 0755->0644]
daemons/import-export
doc/admin.rst
doc/design-2.3.rst [new file with mode: 0644]
doc/devnotes.rst
doc/examples/hooks/ipsec.in [changed mode: 0755->0644]
doc/iallocator.rst
doc/index.rst
doc/rapi.rst
doc/walkthrough.rst
epydoc.conf
lib/asyncnotifier.py
lib/backend.py
lib/bdev.py
lib/bootstrap.py
lib/cli.py
lib/client/__init__.py [new file with mode: 0644]
lib/client/gnt_backup.py [moved from scripts/gnt-backup with 89% similarity, mode: 0644]
lib/client/gnt_cluster.py [moved from scripts/gnt-cluster with 95% similarity, mode: 0644]
lib/client/gnt_debug.py [moved from scripts/gnt-debug with 88% similarity, mode: 0644]
lib/client/gnt_instance.py [moved from scripts/gnt-instance with 96% similarity, mode: 0644]
lib/client/gnt_job.py [moved from scripts/gnt-job with 96% similarity, mode: 0644]
lib/client/gnt_node.py [moved from scripts/gnt-node with 85% similarity, mode: 0644]
lib/client/gnt_os.py [moved from scripts/gnt-os with 94% similarity, mode: 0644]
lib/cmdlib.py
lib/confd/client.py
lib/config.py
lib/constants.py
lib/daemon.py
lib/errors.py
lib/ht.py [new file with mode: 0644]
lib/http/__init__.py
lib/http/auth.py
lib/http/client.py
lib/http/server.py
lib/hypervisor/hv_kvm.py
lib/hypervisor/hv_xen.py
lib/jqueue.py
lib/jstore.py
lib/locking.py
lib/luxi.py
lib/masterd/instance.py
lib/mcpu.py
lib/netutils.py
lib/objects.py
lib/opcodes.py
lib/rapi/client.py
lib/rapi/connector.py
lib/rapi/rlib2.py
lib/rpc.py
lib/runtime.py [new file with mode: 0644]
lib/server/__init__.py [new file with mode: 0644]
lib/server/confd.py [moved from daemons/ganeti-confd with 95% similarity, mode: 0644]
lib/server/masterd.py [moved from daemons/ganeti-masterd with 89% similarity, mode: 0644]
lib/server/noded.py [moved from daemons/ganeti-noded with 95% similarity, mode: 0644]
lib/server/rapi.py [moved from daemons/ganeti-rapi with 69% similarity, mode: 0644]
lib/ssconf.py
lib/ssh.py
lib/utils.py
lib/watcher/__init__.py [moved from daemons/ganeti-watcher with 97% similarity, mode: 0644]
lib/workerpool.py
man/ganeti-os-interface.sgml
man/ganeti-watcher.sgml
man/ganeti.sgml
man/gnt-cluster.sgml
man/gnt-instance.sgml
man/gnt-job.sgml
man/gnt-node.sgml
qa/ganeti-qa.py
qa/qa-sample.json
qa/qa_cluster.py
qa/qa_config.py
qa/qa_daemon.py
qa/qa_env.py
qa/qa_instance.py
qa/qa_node.py
qa/qa_os.py
qa/qa_rapi.py
qa/qa_utils.py
test/ganeti.asyncnotifier_unittest.py
test/ganeti.backend_unittest.py
test/ganeti.cli_unittest.py
test/ganeti.config_unittest.py
test/ganeti.constants_unittest.py
test/ganeti.http_unittest.py
test/ganeti.jqueue_unittest.py
test/ganeti.locking_unittest.py
test/ganeti.luxi_unittest.py
test/ganeti.mcpu_unittest.py
test/ganeti.netutils_unittest.py
test/ganeti.rpc_unittest.py
test/ganeti.runtime_unittest.py [new file with mode: 0755]
test/ganeti.utils_unittest.py
test/ganeti.workerpool_unittest.py
test/mocks.py
test/testutils.py
tools/cfgupgrade
tools/setup-ssh

index a639f30..7207b49 100644 (file)
@@ -8,6 +8,7 @@
 *.py[co]
 *.swp
 *~
+.dir
 
 # /
 /Makefile
@@ -24,7 +25,6 @@
 /config.status
 /configure
 /ganeti
-/stamp-directories
 /stamp-srclinks
 /vcs-version
 /*.patch
 /daemons/daemon-util
 /daemons/ensure-dirs
 /daemons/ganeti-cleaner
+/daemons/ganeti-confd
+/daemons/ganeti-masterd
+/daemons/ganeti-noded
+/daemons/ganeti-rapi
+/daemons/ganeti-watcher
 
 # devel
 /devel/clean-cluster
 /man/*.[0-9]
 /man/*.html
 /man/*.in
+
+# scripts
+/scripts/gnt-backup
+/scripts/gnt-cluster
+/scripts/gnt-debug
+/scripts/gnt-instance
+/scripts/gnt-job
+/scripts/gnt-node
+/scripts/gnt-os
diff --git a/INSTALL b/INSTALL
index dac5ab5..5729386 100644 (file)
--- a/INSTALL
+++ b/INSTALL
@@ -24,6 +24,7 @@ Before installing, please verify that you have the following programs:
 - `bridge utilities <http://www.linuxfoundation.org/en/Net:Bridge>`_
 - `iproute2 <http://www.linuxfoundation.org/en/Net:Iproute2>`_
 - `arping <http://www.skbuff.net/iputils/>`_ (part of iputils)
+- `ndisc6 <http://www.remlab.net/ndisc6/>`_ (if using IPv6)
 - `Python <http://www.python.org/>`_, version 2.4 or above, not 3.0
 - `Python OpenSSL bindings <http://pyopenssl.sourceforge.net/>`_
 - `simplejson Python module <http://code.google.com/p/simplejson/>`_
@@ -44,9 +45,9 @@ Debian/Ubuntu, you can use this command line to install all required
 packages, except for DRBD and Xen::
 
   $ apt-get install lvm2 ssh bridge-utils iproute iputils-arping \
-                    python python-pyopenssl openssl python-pyparsing \
-                    python-simplejson python-pyinotify python-pycurl \
-                    socat
+                    ndisc6 python python-pyopenssl openssl \
+                    python-pyparsing python-simplejson \
+                    python-pyinotify python-pycurl socat
 
 If you want to build from source, please see doc/devnotes.rst for more
 dependencies.
index 34fd055..9436423 100644 (file)
@@ -18,11 +18,14 @@ CHECK_MAN = $(top_srcdir)/autotools/check-man
 CHECK_VERSION = $(top_srcdir)/autotools/check-version
 REPLACE_VARS_SED = autotools/replace_vars.sed
 
+clientdir = $(pkgpythondir)/client
 hypervisordir = $(pkgpythondir)/hypervisor
 httpdir = $(pkgpythondir)/http
 masterddir = $(pkgpythondir)/masterd
 confddir = $(pkgpythondir)/confd
 rapidir = $(pkgpythondir)/rapi
+serverdir = $(pkgpythondir)/server
+watcherdir = $(pkgpythondir)/watcher
 impexpddir = $(pkgpythondir)/impexpd
 toolsdir = $(pkglibdir)/tools
 docdir = $(datadir)/doc/$(PACKAGE)
@@ -39,6 +42,7 @@ DIRS = \
        doc/examples/hooks \
        doc/examples/gnt-debug \
        lib \
+       lib/client \
        lib/build \
        lib/confd \
        lib/http \
@@ -46,16 +50,21 @@ DIRS = \
        lib/impexpd \
        lib/masterd \
        lib/rapi \
+       lib/server \
+       lib/watcher \
        man \
        qa \
-       scripts \
        test \
        test/data \
        tools
 
-BUILDTIME_DIRS = \
+BUILDTIME_DIR_AUTOCREATE = \
+       scripts \
        doc/api \
-       doc/coverage \
+       doc/coverage
+
+BUILDTIME_DIRS = \
+       $(BUILDTIME_DIR_AUTOCREATE) \
        doc/html
 
 DIRCHECK_EXCLUDE = \
@@ -63,6 +72,8 @@ DIRCHECK_EXCLUDE = \
        ganeti-[0-9]*.[0-9]*.[0-9]* \
        doc/html/_*
 
+all_dirfiles = $(addsuffix /.dir,$(DIRS) $(BUILDTIME_DIR_AUTOCREATE))
+
 MAINTAINERCLEANFILES = \
        $(docpng) \
        $(maninput) \
@@ -75,6 +86,8 @@ maintainer-clean-local:
 
 CLEANFILES = \
        $(addsuffix /*.py[co],$(DIRS)) \
+       $(all_dirfiles) \
+       $(PYTHON_BOOTSTRAP) \
        autotools/replace_vars.sed \
        daemons/daemon-util \
        daemons/ensure-dirs \
@@ -87,15 +100,17 @@ CLEANFILES = \
        doc/examples/hooks/ipsec \
        $(man_MANS) \
        $(manhtml) \
-       stamp-directories \
        stamp-srclinks \
        $(nodist_pkgpython_PYTHON)
 
+# BUILT_SOURCES should only be used as a dependency on phony targets. Otherwise
+# it'll cause the target to rebuild every time.
 BUILT_SOURCES = \
        ganeti \
        stamp-srclinks \
-       stamp-directories \
-       lib/_autoconf.py
+       lib/_autoconf.py \
+       $(all_dirfiles) \
+       $(PYTHON_BOOTSTRAP)
 
 nodist_pkgpython_PYTHON = \
        lib/_autoconf.py
@@ -116,6 +131,7 @@ pkgpython_PYTHON = \
        lib/constants.py \
        lib/daemon.py \
        lib/errors.py \
+       lib/ht.py \
        lib/jqueue.py \
        lib/jstore.py \
        lib/locking.py \
@@ -125,6 +141,7 @@ pkgpython_PYTHON = \
        lib/objects.py \
        lib/opcodes.py \
        lib/rpc.py \
+       lib/runtime.py \
        lib/serializer.py \
        lib/ssconf.py \
        lib/ssh.py \
@@ -133,6 +150,16 @@ pkgpython_PYTHON = \
        lib/uidpool.py \
        lib/workerpool.py
 
+client_PYTHON = \
+       lib/client/__init__.py \
+       lib/client/gnt_backup.py \
+       lib/client/gnt_cluster.py \
+       lib/client/gnt_debug.py \
+       lib/client/gnt_instance.py \
+       lib/client/gnt_job.py \
+       lib/client/gnt_node.py \
+       lib/client/gnt_os.py
+
 hypervisor_PYTHON = \
        lib/hypervisor/__init__.py \
        lib/hypervisor/hv_base.py \
@@ -169,11 +196,22 @@ masterd_PYTHON = \
 impexpd_PYTHON = \
        lib/impexpd/__init__.py
 
+watcher_PYTHON = \
+       lib/watcher/__init__.py
+
+server_PYTHON = \
+       lib/server/__init__.py \
+       lib/server/confd.py \
+       lib/server/masterd.py \
+       lib/server/noded.py \
+       lib/server/rapi.py
+
 docrst = \
        doc/admin.rst \
        doc/design-2.0.rst \
        doc/design-2.1.rst \
        doc/design-2.2.rst \
+       doc/design-2.3.rst \
        doc/cluster-merge.rst \
        doc/devnotes.rst \
        doc/glossary.rst \
@@ -189,9 +227,13 @@ docrst = \
        doc/security.rst \
        doc/walkthrough.rst
 
-doc/html/.stamp: $(docrst) $(docpng) doc/conf.py configure.ac
+$(RUN_IN_TEMPDIR): | $(all_dirfiles)
+
+doc/html/index.html: $(docrst) $(docpng) doc/conf.py configure.ac \
+       $(RUN_IN_TEMPDIR)
        @test -n "$(SPHINX)" || \
            { echo 'sphinx-build' not found during configure; exit 1; }
+       @mkdir_p@ $(dir $@)
        PYTHONPATH=. $(RUN_IN_TEMPDIR) $(SPHINX) -q -W -b html \
            -d . \
            -D version="$(VERSION_MAJOR).$(VERSION_MINOR)" \
@@ -200,7 +242,7 @@ doc/html/.stamp: $(docrst) $(docpng) doc/conf.py configure.ac
        rm -f doc/html/.buildinfo doc/html/objects.inv
        touch $@
 
-doc/html: doc/html/.stamp
+doc/html: doc/html/index.html
 
 doc/news.rst: NEWS
        set -e; \
@@ -246,15 +288,24 @@ gnt_scripts = \
        scripts/gnt-node \
        scripts/gnt-os
 
-dist_sbin_SCRIPTS = \
+PYTHON_BOOTSTRAP = \
+       daemons/ganeti-confd \
+       daemons/ganeti-masterd \
        daemons/ganeti-noded \
        daemons/ganeti-watcher \
-       daemons/ganeti-masterd \
-       daemons/ganeti-confd \
        daemons/ganeti-rapi \
-       $(gnt_scripts)
+       scripts/gnt-backup \
+       scripts/gnt-cluster \
+       scripts/gnt-debug \
+       scripts/gnt-instance \
+       scripts/gnt-job \
+       scripts/gnt-node \
+       scripts/gnt-os
+
+dist_sbin_SCRIPTS =
 
 nodist_sbin_SCRIPTS = \
+       $(PYTHON_BOOTSTRAP) \
        daemons/ganeti-cleaner
 
 dist_tools_SCRIPTS = \
@@ -395,6 +446,7 @@ python_tests = \
        test/ganeti.rapi.resources_unittest.py \
        test/ganeti.rapi.rlib2_unittest.py \
        test/ganeti.rpc_unittest.py \
+       test/ganeti.runtime_unittest.py \
        test/ganeti.serializer_unittest.py \
        test/ganeti.ssh_unittest.py \
        test/ganeti.uidpool_unittest.py \
@@ -430,12 +482,15 @@ all_python_code = \
        $(pkglib_python_scripts) \
        $(python_tests) \
        $(pkgpython_PYTHON) \
+       $(client_PYTHON) \
        $(hypervisor_PYTHON) \
        $(rapi_PYTHON) \
+       $(server_PYTHON) \
        $(http_PYTHON) \
        $(confd_PYTHON) \
        $(masterd_PYTHON) \
        $(impexpd_PYTHON) \
+       $(watcher_PYTHON) \
        $(noinst_PYTHON)
 
 srclink_files = \
@@ -456,7 +511,8 @@ lint_python_code = \
        $(dist_sbin_SCRIPTS) \
        $(dist_tools_SCRIPTS) \
        $(pkglib_python_scripts) \
-       $(BUILD_BASH_COMPLETION)
+       $(BUILD_BASH_COMPLETION) \
+       $(PYTHON_BOOTSTRAP)
 
 test/daemon-util_unittest.bash: daemons/daemon-util
 
@@ -466,21 +522,18 @@ devel/upload: devel/upload.in $(REPLACE_VARS_SED)
        sed -f $(REPLACE_VARS_SED) < $< > $@
        chmod u+x $@
 
-daemons/%: daemons/%.in \
-               $(REPLACE_VARS_SED)
+daemons/%: daemons/%.in $(REPLACE_VARS_SED)
        sed -f $(REPLACE_VARS_SED) < $< > $@
        chmod +x $@
 
-doc/examples/%: doc/examples/%.in \
-               $(REPLACE_VARS_SED)
+doc/examples/%: doc/examples/%.in $(REPLACE_VARS_SED)
        sed -f $(REPLACE_VARS_SED) < $< > $@
 
-doc/examples/hooks/%: doc/examples/hooks/%.in \
-               $(REPLACE_VARS_SED)
+doc/examples/hooks/%: doc/examples/hooks/%.in $(REPLACE_VARS_SED)
        sed -f $(REPLACE_VARS_SED) < $< > $@
 
 doc/examples/bash_completion: $(BUILD_BASH_COMPLETION) $(RUN_IN_TEMPDIR) \
-       lib/cli.py $(gnt_scripts) tools/burnin
+       lib/cli.py $(gnt_scripts) $(client_PYTHON) tools/burnin
        PYTHONPATH=. $(RUN_IN_TEMPDIR) $(CURDIR)/$(BUILD_BASH_COMPLETION) > $@
 
 doc/%.png: doc/%.dot
@@ -523,7 +576,7 @@ regen-vcs-version:
          $(MAKE) vcs-version; \
        fi
 
-lib/_autoconf.py: Makefile stamp-directories vcs-version
+lib/_autoconf.py: Makefile vcs-version | lib/.dir
        set -e; \
        VCSVER=`cat $(abs_top_srcdir)/vcs-version`; \
        { echo '# This file is automatically generated, do not edit!'; \
@@ -568,8 +621,14 @@ lib/_autoconf.py: Makefile stamp-directories vcs-version
          echo "DRBD_BARRIERS = $(DRBD_BARRIERS)"; \
          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 "NODED_USER = '$(NODED_USER)'"; \
          echo "VCS_VERSION = '$$VCSVER'"; \
        } > $@
 
@@ -598,9 +657,39 @@ $(REPLACE_VARS_SED): Makefile
          echo 's#@GNTDAEMONSGROUP@#$(DAEMONS_GROUP)#g'; \
        } > $@
 
+# Using deferred evaluation
+daemons/ganeti-%: MODULE = ganeti.server.$(patsubst ganeti-%,%,$(notdir $@))
+daemons/ganeti-watcher: MODULE = ganeti.watcher
+scripts/%: MODULE = ganeti.client.$(subst -,_,$(notdir $@))
+
+$(PYTHON_BOOTSTRAP): Makefile | $(all_dirfiles)
+       test -n "$(MODULE)" || { echo Missing module; exit 1; }
+       set -e; \
+       { echo '#!/usr/bin/python'; \
+         echo '# This file is automatically generated, do not edit!'; \
+         echo "# Edit $(MODULE) instead."; \
+         echo; \
+         echo '"""Bootstrap script for L{$(MODULE)}"""'; \
+         echo; \
+         echo '# pylint: disable-msg=C0103'; \
+         echo '# C0103: Invalid name'; \
+         echo; \
+         echo 'import sys'; \
+         echo 'import $(MODULE) as main'; \
+         echo; \
+         echo '# Temporarily alias commands until bash completion'; \
+         echo '# generator is changed'; \
+         echo 'if hasattr(main, "commands"):'; \
+               echo '  commands = main.commands # pylint: disable-msg=E1101'; \
+         echo; \
+         echo 'if __name__ == "__main__":'; \
+         echo '  sys.exit(main.Main())'; \
+       } > $@
+       chmod u+x $@
+
 # We need to create symlinks because "make distcheck" will not install Python
 # files when building.
-stamp-srclinks: Makefile stamp-directories
+stamp-srclinks: Makefile | $(all_dirfiles)
        set -e; \
        for i in $(srclink_files); do \
                if test ! -f $$i -a -f $(abs_top_srcdir)/$$i; then \
@@ -641,7 +730,7 @@ check-local: check-dirs
        $(CHECK_VERSION) $(VERSION) $(top_srcdir)/NEWS
 
 .PHONY: lint
-lint: ganeti $(BUILT_SOURCES)
+lint: $(BUILT_SOURCES)
        @test -n "$(PYLINT)" || { echo 'pylint' not found during configure; exit 1; }
        $(PYLINT) $(LINT_OPTS) $(lint_python_code)
 
@@ -666,6 +755,15 @@ distcheck-hook:
                echo "Found backup files in final archive." 1>&2; \
                exit 1; \
        fi
+# Empty files or directories should not be distributed. They can cause
+# unnecessary warnings for packagers. Directories used by automake during
+# distcheck must be excluded.
+       if find $(top_distdir) -empty -and -not \( \
+                       -path $(top_distdir)/_build -or \
+                       -path $(top_distdir)/_inst \) | grep .; then \
+               echo "Found empty files or directories in final archive." 1>&2; \
+               exit 1; \
+       fi
        if test -n "$(BUILD_RELEASE)" && \
           grep -n -H -E '^\*.*unreleased' $(top_distdir)/NEWS; then \
                echo "Found unreleased version in NEWS." >&2; \
@@ -689,13 +787,13 @@ install-exec-local:
          "$(DESTDIR)${localstatedir}/log/ganeti" \
          "$(DESTDIR)${localstatedir}/run/ganeti"
 
-stamp-directories: Makefile
-       @mkdir_p@ $(DIRS)
-       touch $@
+# To avoid conflicts between directory names and other targets, a file inside
+# the directory is used to ensure its existence.
+%.dir:
+       @mkdir_p@ $* && touch $@
 
 .PHONY: apidoc
 apidoc: epydoc.conf $(RUN_IN_TEMPDIR) $(BUILT_SOURCES)
-       test -e doc/api || mkdir doc/api
        $(RUN_IN_TEMPDIR) epydoc -v \
                --conf $(CURDIR)/epydoc.conf \
                --output $(CURDIR)/doc/api
@@ -711,7 +809,6 @@ TAGS: $(BUILT_SOURCES)
 .PHONY: coverage
 coverage: $(BUILT_SOURCES) $(python_tests)
        set -e; \
-       mkdir -p doc/coverage; \
        COVERAGE_FILE=$(CURDIR)/doc/coverage/data \
        TEXT_COVERAGE=$(CURDIR)/doc/coverage/report.txt \
        HTML_COVERAGE=$(CURDIR)/doc/coverage \
diff --git a/NEWS b/NEWS
index ba1e881..12e26c3 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -1,6 +1,65 @@
 News
 ====
 
+Version 2.3.0 rc1
+-----------------
+
+*(Released Fri, 19 Nov 2010)*
+
+A number of bugfixes and documentation updates:
+
+- Update ganeti-os-interface documentation
+- Fixed a bug related to duplicate MACs or similar items which should be
+  unique
+- Fix breakage in OS state modify
+- Reinstall instance: disallow offline secondaries (fixes bug related to
+  OS changing but reinstall failing)
+- plus all the other fixes between 2.2.1 and 2.2.2
+
+
+Version 2.3.0 rc0
+-----------------
+
+*(Released Tue, 2 Nov 2010)*
+
+- Fixed clearing of the default iallocator using ``gnt-cluster modify``
+- Fixed master failover race with watcher
+- Fixed a bug in ``gnt-node modify`` which could lead to an inconsistent
+  configuration
+- Accept previously stopped instance for export with instance removal
+- Simplify and extend the environment variables for instance OS scripts
+- Added new node flags, ``master_capable`` and ``vm_capable``
+- Added optional instance disk wiping prior during allocation. This is a
+  cluster-wide option and can be set/modified using
+  ``gnt-cluster {init,modify} --prealloc-wipe-disks``.
+- Added IPv6 support, see :doc:`design document <design-2.3>` and
+  :doc:`install-quick`
+- Added a new watcher option (``--ignore-pause``)
+- Added option to ignore offline node on instance start/stop
+  (``--ignore-offline``)
+- Allow overriding OS parameters with ``gnt-instance reinstall``
+- Added ability to change node's secondary IP address using ``gnt-node
+  modify``
+- Implemented privilege separation for all daemons except
+  ``ganeti-noded``, see ``configure`` options
+- Complain if an instance's disk is marked faulty in ``gnt-cluster
+  verify``
+- Implemented job priorities (see ``ganeti(7)`` manpage)
+- Ignore failures while shutting down instances during failover from
+  offline node
+- Exit daemon's bootstrap process only once daemon is ready
+- Export more information via ``LUQueryInstances``/remote API
+- Improved documentation, QA and unittests
+- RAPI daemon now watches ``rapi_users`` all the time and doesn't need a
+  restart if the file was created or changed
+- Added LUXI protocol version sent with each request and response,
+  allowing detection of server/client mismatches
+- Moved the Python scripts among gnt-* and ganeti-* into modules
+- Moved all code related to setting up SSH to an external script,
+  ``setup-ssh``
+- Infrastructure changes for node group support in future versions
+
+
 Version 2.2.2
 -------------
 
@@ -1182,7 +1241,7 @@ Version 1.2b2
   post-configuration update hook)
 - Other small bugfixes
 
-.. vim: set textwidth=72 :
+.. vim: set textwidth=72 syntax=rst :
 .. Local Variables:
 .. mode: rst
 .. fill-column: 72
index efb381f..b046543 100755 (executable)
@@ -117,6 +117,15 @@ def WritePreamble(sw):
       sw.DecIndent()
     sw.Write("}")
 
+  sw.Write("_ganeti_nodegroup() {")
+  sw.IncIndent()
+  try:
+    nodegroups_path = os.path.join(constants.DATA_DIR, "ssconf_nodegroups")
+    sw.Write("cat %s 2>/dev/null || :", utils.ShellQuote(nodegroups_path))
+  finally:
+    sw.DecIndent()
+  sw.Write("}")
+
   # Params: <offset> <options with values> <options without values>
   # Result variable: $first_arg_idx
   sw.Write("_ganeti_find_first_arg() {")
@@ -324,6 +333,8 @@ class CompletionWriter:
           WriteCompReply(sw, "-W \"$(_ganeti_os)\"", cur=cur)
         elif suggest == cli.OPT_COMPL_ONE_IALLOCATOR:
           WriteCompReply(sw, "-W \"$(_ganeti_iallocator)\"", cur=cur)
+        elif suggest == cli.OPT_COMPL_ONE_NODEGROUP:
+          WriteCompReply(sw, "-W \"$(_ganeti_nodegroup)\"", cur=cur)
         elif suggest == cli.OPT_COMPL_INST_ADD_NODES:
           sw.Write("local tmp= node1= pfx= curvalue=\"${optcur#*:}\"")
 
index 980af41..cae52ef 100644 (file)
@@ -1,8 +1,8 @@
 # Configure script for Ganeti
 m4_define([gnt_version_major], [2])
-m4_define([gnt_version_minor], [2])
-m4_define([gnt_version_revision], [2])
-m4_define([gnt_version_suffix], [])
+m4_define([gnt_version_minor], [3])
+m4_define([gnt_version_revision], [0])
+m4_define([gnt_version_suffix], [~rc1])
 m4_define([gnt_version_full],
           m4_format([%d.%d.%d%s],
                     gnt_version_major, gnt_version_minor,
@@ -141,9 +141,9 @@ AC_ARG_WITH([user-prefix],
     [prefix for daemon users]
     [ (default is to run all daemons as root)]
   )],
-  [user_masterd="root";
+  [user_masterd="${withval}masterd";
    user_rapi="${withval}rapi";
-   user_confd="root";
+   user_confd="${withval}confd";
    user_noded="root"],
   [user_masterd="root";
    user_rapi="root";
@@ -160,10 +160,10 @@ AC_ARG_WITH([group-prefix],
     [prefix for daemon POSIX groups]
     [ (default is to run all daemons under group root)]
   )],
-  [group_rapi="root";
-   group_admin="root";
-   group_confd="root";
-   group_masterd="root";
+  [group_rapi="${withval}rapi";
+   group_admin="${withval}admin";
+   group_confd="${withval}confd";
+   group_masterd="${withval}masterd";
    group_daemons="${withval}daemons";],
   [group_rapi="root";
    group_admin="root";
old mode 100755 (executable)
new mode 100644 (file)
index 4d6054d..819fd6b
@@ -50,6 +50,26 @@ _daemon_executable() {
   echo "@PREFIX@/sbin/$1"
 }
 
+_daemon_usergroup() {
+  case "$1" in
+    masterd)
+      echo "@GNTMASTERUSER@:@GNTMASTERDGROUP@"
+      ;;
+    confd)
+      echo "@GNTCONFDUSER@:@GNTCONFDGROUP@"
+      ;;
+    rapi)
+      echo "@GNTRAPIUSER@:@GNTRAPIGROUP@"
+      ;;
+    noded)
+      echo "@GNTNODEDUSER@:@GNTDAEMONSGROUP@"
+      ;;
+    *)
+      echo "root:@GNTDAEMONSGROUP@"
+      ;;
+  esac
+}
+
 # Checks whether the local machine is part of a cluster
 check_config() {
   local server_pem=@LOCALSTATEDIR@/lib/ganeti/server.pem
@@ -144,7 +164,8 @@ start() {
   local name="$1"; shift
 
   # Convert daemon name to uppercase after removing "ganeti-" prefix
-  local ucname=$(echo ${name#ganeti-} | tr a-z A-Z)
+  local plain_name=${name#ganeti-}
+  local ucname=$(tr a-z A-Z <<<$plain_name)
 
   # Read $<daemon>_ARGS and $EXTRA_<daemon>_ARGS
   eval local args="\"\$${ucname}_ARGS \$EXTRA_${ucname}_ARGS\""
@@ -154,6 +175,7 @@ start() {
   start-stop-daemon --start --quiet --oknodo \
     --pidfile $(_daemon_pidfile $name) \
     --startas $(_daemon_executable $name) \
+    --chuid $(_daemon_usergroup $plain_name) \
     -- $args "$@"
 }
 
index c9de87b..252f24a 100644 (file)
@@ -8,6 +8,7 @@ RUNDIR="@LOCALSTATEDIR@/run"
 GNTRUNDIR="${RUNDIR}/ganeti"
 LOGDIR="@LOCALSTATEDIR@/log"
 GNTLOGDIR="${LOGDIR}/ganeti"
+LOCKDIR="@LOCALSTATEDIR@/lock"
 
 _fileset_owner() {
   case "$1" in
@@ -20,35 +21,120 @@ _fileset_owner() {
     rapi)
       echo "@GNTRAPIUSER@:@GNTRAPIGROUP@"
       ;;
+    noded)
+      echo "root:@GNTMASTERDGROUP@"
+      ;;
     daemons)
       echo "@GNTMASTERUSER@:@GNTDAEMONSGROUP@"
       ;;
+    masterd-confd)
+      echo "@GNTMASTERUSER@:@GNTCONFDGROUP@"
+      ;;
     *)
       echo "root:root"
       ;;
   esac
 }
 
+_ensure_file() {
+  local file="$1"
+  local perm="$2"
+  local owner="$3"
+
+  [[ -e "${file}" ]] || return 1
+  chmod ${perm} "${file}"
+
+  if ! [[ -z "${owner}" ]]; then
+    chown ${owner} "${file}"
+  fi
+
+  return 0
+}
+
 _ensure_dir() {
   local dir="$1"
   local perm="$2"
   local owner="$3"
 
-  [ -d "${dir}" ] || mkdir "${dir}"
-  chmod ${perm} "${dir}"
-  chown ${owner} "${dir}"
+  [[ -d "${dir}" ]] || mkdir "${dir}"
+
+  _ensure_file "${dir}" "${perm}" "${owner}"
+}
+
+_gather_files() {
+  local path="$1"
+  local perm="$2"
+  local user="$3"
+  local group="$4"
+
+  shift 4
+
+  find "${path}" -type f "(" "!" -perm ${perm} -or "(" "!" -user ${user} -or \
+       "!" -group ${group} ")" ")" "$@"
+}
+
+_ensure_datadir() {
+  local full_run="$1"
+
+  _ensure_dir ${DATADIR} 0755 "$(_fileset_owner masterd)"
+  _ensure_dir ${DATADIR}/queue 0700 "$(_fileset_owner masterd)"
+  _ensure_dir ${DATADIR}/queue/archive 0700 "$(_fileset_owner masterd)"
+  _ensure_dir ${DATADIR}/uidpool 0750 "$(_fileset_owner noded)"
+
+  # We ignore these files if they don't exists (incomplete setup)
+  _ensure_file ${DATADIR}/cluster-domain-secret 0640 \
+               "$(_fileset_owner masterd)" || :
+  _ensure_file ${DATADIR}/config.data 0640 "$(_fileset_owner masterd-confd)" || :
+  _ensure_file ${DATADIR}/hmac.key 0440 "$(_fileset_owner confd)" || :
+  _ensure_file ${DATADIR}/known_hosts 0644 "$(_fileset_owner masterd)" || :
+  _ensure_file ${DATADIR}/rapi.pem 0440 "$(_fileset_owner rapi)" || :
+  _ensure_file ${DATADIR}/rapi_users 0640 "$(_fileset_owner rapi)" || :
+  _ensure_file ${DATADIR}/server.pem 0440 "$(_fileset_owner masterd)" || :
+  _ensure_file ${DATADIR}/queue/serial 0600 "$(_fileset_owner masterd)" || :
+
+  # To not change the utils.LockFile object
+  touch ${DATADIR}/queue/lock
+  _ensure_file ${DATADIR}/queue/lock 0600 "$(_fileset_owner masterd)"
+
+  if ! [[ -z "${full_run}" ]]; then
+    for file in $(_gather_files ${DATADIR}/queue 0600 @GNTMASTERUSER@ \
+                  @GNTMASTERDGROUP@); do
+      _ensure_file "${file}" 0600 "$(_fileset_owner masterd)"
+    done
+
+    for file in $(_gather_files ${DATADIR} 0600 root \
+                  @GNTMASTERDGROUP@ -name 'ssconf_*'); do
+      _ensure_file "${file}" 0444 "$(_fileset_owner noded)"
+    done
+  fi
 }
 
 _ensure_rundir() {
-  _ensure_dir "${GNTRUNDIR}" 0775 "$(_fileset_owner "daemons")"
-  _ensure_dir "${GNTRUNDIR}/socket" 0750 "$(_fileset_owner "daemons")"
+  _ensure_dir ${GNTRUNDIR} 0775 "$(_fileset_owner daemons)"
+  _ensure_dir ${GNTRUNDIR}/socket 0750 "$(_fileset_owner daemons)"
+  _ensure_dir ${GNTRUNDIR}/bdev-cache 0755 "$(_fileset_owner noded)"
+  _ensure_dir ${GNTRUNDIR}/instance-disks 0755 "$(_fileset_owner noded)"
+  _ensure_dir ${GNTRUNDIR}/crypto 0700 "$(_fileset_owner noded)"
+  _ensure_dir ${GNTRUNDIR}/import-export 0755 "$(_fileset_owner noded)"
+
+  # We ignore this file if it don't exists (not yet start up)
+  _ensure_file ${GNTRUNDIR}/socket/ganeti-master 0770 \
+               "$(_fileset_owner daemons)" || :
 }
 
 _ensure_logdir() {
-  _ensure_dir "${GNTLOGDIR}" 0770 "$(_fileset_owner "daemons")"
+  _ensure_dir ${GNTLOGDIR} 0770 "$(_fileset_owner daemons)"
+  _ensure_dir ${GNTLOGDIR}/os 0750 "$(_fileset_owner daemons)"
+
+  # We ignore these files if they don't exists (incomplete setup)
+  _ensure_file ${GNTLOGDIR}/master-daemon.log 0600 "$(_fileset_owner masterd)" || :
+  _ensure_file ${GNTLOGDIR}/conf-daemon.log 0600 "$(_fileset_owner confd)" || :
+  _ensure_file ${GNTLOGDIR}/node-daemon.log 0600 "$(_fileset_owner noded)" || :
+  _ensure_file ${GNTLOGDIR}/rapi-daemon.log 0600 "$(_fileset_owner rapi)" || :
+}
 
-  touch "${GNTLOGDIR}/rapi-daemon.log"
-  chown $(_fileset_owner "rapi") "${GNTLOGDIR}/rapi-daemon.log"
+_ensure_lockdir() {
+  _ensure_dir ${LOCKDIR} 1777 ""
 }
 
 _operate_while_hold() {
@@ -56,13 +142,23 @@ _operate_while_hold() {
   local path=$2
   shift 2
 
-  (cd "${path}";
+  (cd ${path};
    ${fn} "$@")
 }
 
 main() {
-  _operate_while_hold "_ensure_rundir" "${RUNDIR}"
-  _operate_while_hold "_ensure_logdir" "${LOGDIR}"
+  local full_run
+
+  while getopts "f" OPTION; do
+    case ${OPTION} in
+      f) full_run=1 ;;
+    esac
+  done
+
+  _operate_while_hold "_ensure_datadir" ${DATADIR} ${full_run}
+  _operate_while_hold "_ensure_rundir" ${RUNDIR}
+  _operate_while_hold "_ensure_logdir" ${LOGDIR}
+  _operate_while_hold "_ensure_lockdir" @LOCALSTATEDIR@
 }
 
 main "$@"
old mode 100755 (executable)
new mode 100644 (file)
index 02a5ad1..ce9c3bd 100755 (executable)
@@ -408,7 +408,7 @@ def ParseOptions():
   # Normalize and check parameters
   if options.host is not None:
     try:
-      options.host = netutils.HostInfo.NormalizeName(options.host)
+      options.host = netutils.Hostname.GetNormalizedName(options.host)
     except errors.OpPrereqError, err:
       parser.error("Invalid hostname '%s': %s" % (options.host, err))
 
index 99ccb83..3daf408 100644 (file)
@@ -78,6 +78,22 @@ Depending on the role, each node will run a set of daemons:
 - the :command:`ganeti-masterd` daemon which runs on the master node and
   allows control of the cluster
 
+Beside the node role, there are other node flags that influence its
+behaviour:
+
+- the *master_capable* flag denotes whether the node can ever become a
+  master candidate; setting this to 'no' means that auto-promotion will
+  never make this node a master candidate; this flag can be useful for a
+  remote node that only runs local instances, and having it become a
+  master is impractical due to networking or other constraints
+- the *vm_capable* flag denotes whether the node can host instances or
+  not; for example, one might use a non-vm_capable node just as a master
+  candidate, for configuration backups; setting this flag to no
+  disallows placement of instances of this node, deactivates hypervisor
+  and related checks on it (e.g. bridge checks, LVM check, etc.), and
+  removes it from cluster capacity computations
+
+
 Instance
 ~~~~~~~~
 
diff --git a/doc/design-2.3.rst b/doc/design-2.3.rst
new file mode 100644 (file)
index 0000000..31fd3cc
--- /dev/null
@@ -0,0 +1,926 @@
+=================
+Ganeti 2.3 design
+=================
+
+This document describes the major changes in Ganeti 2.3 compared to
+the 2.2 version.
+
+.. contents:: :depth: 4
+
+As for 2.1 and 2.2 we divide the 2.3 design into three areas:
+
+- core changes, which affect the master daemon/job queue/locking or
+  all/most logical units
+- logical unit/feature changes
+- external interface changes (e.g. command line, OS API, hooks, ...)
+
+Core changes
+============
+
+Node Groups
+-----------
+
+Current state and shortcomings
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Currently all nodes of a Ganeti cluster are considered as part of the
+same pool, for allocation purposes: DRBD instances for example can be
+allocated on any two nodes.
+
+This does cause a problem in cases where nodes are not all equally
+connected to each other. For example if a cluster is created over two
+set of machines, each connected to its own switch, the internal bandwidth
+between machines connected to the same switch might be bigger than the
+bandwidth for inter-switch connections.
+
+Moreover, some operations inside a cluster require all nodes to be locked
+together for inter-node consistency, and won't scale if we increase the
+number of nodes to a few hundreds.
+
+Proposed changes
+~~~~~~~~~~~~~~~~
+
+With this change we'll divide Ganeti nodes into groups. Nothing will
+change for clusters with only one node group. Bigger clusters will be
+able to have more than one group, and each node will belong to exactly
+one.
+
+Node group management
++++++++++++++++++++++
+
+To manage node groups and the nodes belonging to them, the following new
+commands and flags will be introduced::
+
+  gnt-group add <group> # add a new node group
+  gnt-group del <group> # delete an empty node group
+  gnt-group list # list node groups
+  gnt-group rename <oldname> <newname> # rename a node group
+  gnt-node {list,info} -g <group> # list only nodes belonging to a node group
+  gnt-node modify -g <group> # assign a node to a node group
+
+Node group attributes
++++++++++++++++++++++
+
+In clusters with more than one node group, it may be desirable to
+establish local policies regarding which groups should be preferred when
+performing allocation of new instances, or inter-group instance migrations.
+
+To help with this, we will provide an ``alloc_policy`` attribute for
+node groups. Such attribute will be honored by iallocator plugins when
+making automatic decisions regarding instance placement.
+
+The ``alloc_policy`` attribute can have the following values:
+
+- unallocable: the node group should not be a candidate for instance
+  allocations, and the operation should fail if only groups in this
+  state could be found that would satisfy the requirements.
+
+- last_resort: the node group should not be used for instance
+  allocations, unless this would be the only way to have the operation
+  succeed.
+
+- preferred: the node group can be used freely for allocation of
+  instances (this is the default state for newly created node
+  groups). Note that prioritization among groups in this state will be
+  deferred to the  iallocator plugin that's being used.
+
+Node group operations
++++++++++++++++++++++
+
+One operation at the node group level will be initially provided::
+
+  gnt-group drain <group>
+
+The purpose of this operation is to migrate all instances in a given
+node group to other groups in the cluster, e.g. to reclaim capacity if
+there are enough free resources in other node groups that share a
+storage pool with the evacuated group.
+
+Instance level changes
+++++++++++++++++++++++
+
+With the introduction of node groups, instances will be required to live
+in only one group at a time; this is mostly important for DRBD
+instances, which will not be allowed to have their primary and secondary
+nodes in different node groups. To support this, we envision the
+following changes:
+
+  - The iallocator interface will be augmented, and node groups exposed,
+    so that plugins will be able to make a decision regarding the group
+    in which to place a new instance. By default, all node groups will
+    be considered, but it will be possible to include a list of groups
+    in the creation job, in which case the plugin will limit itself to
+    considering those; in both cases, the ``alloc_policy`` attribute
+    will be honored.
+  - If, on the other hand, a primary and secondary nodes are specified
+    for a new instance, they will be required to be on the same node
+    group.
+  - Moving an instance between groups can only happen via an explicit
+    operation, which for example in the case of DRBD will work by
+    performing internally a replace-disks, a migration, and a second
+    replace-disks. It will be possible to clean up an interrupted
+    group-move operation.
+  - Cluster verify will signal an error if an instance has nodes
+    belonging to different groups. Additionally, changing the group of a
+    given node will be initially only allowed if the node is empty, as a
+    straightforward mechanism to avoid creating such situation.
+  - Inter-group instance migration will have the same operation modes as
+    new instance allocation, defined above: letting an iallocator plugin
+    decide the target group, possibly restricting the set of node groups
+    to consider, or specifying a target primary and secondary nodes. In
+    both cases, the target group or nodes must be able to accept the
+    instance network- and storage-wise; the operation will fail
+    otherwise, though in the future we may be able to allow some
+    parameter to be changed together with the move (in the meantime, an
+    import/export will be required in this scenario).
+
+Internal changes
+++++++++++++++++
+
+We expect the following changes for cluster management:
+
+  - Frequent multinode operations, such as os-diagnose or cluster-verify,
+    will act on one group at a time, which will have to be specified in
+    all cases, except for clusters with just one group. Command line
+    tools will also have a way to easily target all groups, by
+    generating one job per group.
+  - Groups will have a human-readable name, but will internally always
+    be referenced by a UUID, which will be immutable; for example, nodes
+    will contain the UUID of the group they belong to. This is done
+    to simplify referencing while keeping it easy to handle renames and
+    movements. If we see that this works well, we'll transition other
+    config objects (instances, nodes) to the same model.
+  - The addition of a new per-group lock will be evaluated, if we can
+    transition some operations now requiring the BGL to it.
+  - Master candidate status will be allowed to be spread among groups.
+    For the first version we won't add any restriction over how this is
+    done, although in the future we may have a minimum number of master
+    candidates which Ganeti will try to keep in each group, for example.
+
+Other work and future changes
++++++++++++++++++++++++++++++
+
+Commands like ``gnt-cluster command``/``gnt-cluster copyfile`` will
+continue to work on the whole cluster, but it will be possible to target
+one group only by specifying it.
+
+Commands which allow selection of sets of resources (for example
+``gnt-instance start``/``gnt-instance stop``) will be able to select
+them by node group as well.
+
+Initially node groups won't be taggable objects, to simplify the first
+implementation, but we expect this to be easy to add in a future version
+should we see it's useful.
+
+We envision groups as a good place to enhance cluster scalability. In
+the future we may want to use them as units for configuration diffusion,
+to allow a better master scalability. For example it could be possible
+to change some all-nodes RPCs to contact each group once, from the
+master, and make one node in the group perform internal diffusion. We
+won't implement this in the first version, but we'll evaluate it for the
+future, if we see scalability problems on big multi-group clusters.
+
+When Ganeti will support more storage models (e.g. SANs, Sheepdog, Ceph)
+we expect groups to be the basis for this, allowing for example a
+different Sheepdog/Ceph cluster, or a different SAN to be connected to
+each group. In some cases this will mean that inter-group move operation
+will be necessarily performed with instance downtime, unless the
+hypervisor has block-migrate functionality, and we implement support for
+it (this would be theoretically possible, today, with KVM, for example).
+
+Scalability issues with big clusters
+------------------------------------
+
+Current and future issues
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Assuming the node groups feature will enable bigger clusters, other
+parts of Ganeti will be impacted even more by the (in effect) bigger
+clusters.
+
+While many areas will be impacted, one is the most important: the fact
+that the watcher still needs to be able to repair instance data on the
+current 5 minutes time-frame (a shorter time-frame would be even
+better). This means that the watcher itself needs to have parallelism
+when dealing with node groups.
+
+Also, the iallocator plugins are being fed data from Ganeti but also
+need access to the full cluster state, and in general we still rely on
+being able to compute the full cluster state somewhat “cheaply” and
+on-demand. This conflicts with the goal of disconnecting the different
+node groups, and to keep the same parallelism while growing the cluster
+size.
+
+Another issue is that the current capacity calculations are done
+completely outside Ganeti (and they need access to the entire cluster
+state), and this prevents keeping the capacity numbers in sync with the
+cluster state. While this is still acceptable for smaller clusters where
+a small number of allocations/removal are presumed to occur between two
+periodic capacity calculations, on bigger clusters where we aim to
+parallelize heavily between node groups this is no longer true.
+
+
+
+As proposed changes, the main change is introducing a cluster state
+cache (not serialised to disk), and to update many of the LUs and
+cluster operations to account for it. Furthermore, the capacity
+calculations will be integrated via a new OpCode/LU, so that we have
+faster feedback (instead of periodic computation).
+
+Cluster state cache
+~~~~~~~~~~~~~~~~~~~
+
+A new cluster state cache will be introduced. The cache relies on two
+main ideas:
+
+- the total node memory, CPU count are very seldom changing; the total
+  node disk space is also slow changing, but can change at runtime; the
+  free memory and free disk will change significantly for some jobs, but
+  on a short timescale; in general, these values will be mostly “constant”
+  during the lifetime of a job
+- we already have a periodic set of jobs that query the node and
+  instance state, driven the by :command:`ganeti-watcher` command, and
+  we're just discarding the results after acting on them
+
+Given the above, it makes sense to cache the results of node and instance
+state (with a focus on the node state) inside the master daemon.
+
+The cache will not be serialised to disk, and will be for the most part
+transparent to the outside of the master daemon.
+
+Cache structure
++++++++++++++++
+
+The cache will be oriented with a focus on node groups, so that it will
+be easy to invalidate an entire node group, or a subset of nodes, or the
+entire cache. The instances will be stored in the node group of their
+primary node.
+
+Furthermore, since the node and instance properties determine the
+capacity statistics in a deterministic way, the cache will also hold, at
+each node group level, the total capacity as determined by the new
+capacity iallocator mode.
+
+Cache updates
++++++++++++++
+
+The cache will be updated whenever a query for a node state returns
+“full” node information (so as to keep the cache state for a given node
+consistent). Partial results will not update the cache (see next
+paragraph).
+
+Since there will be no way to feed the cache from outside, and we
+would like to have a consistent cache view when driven by the watcher,
+we'll introduce a new OpCode/LU for the watcher to run, instead of the
+current separate opcodes (see below in the watcher section).
+
+Updates to a node that change a node's specs “downward” (e.g. less
+memory) will invalidate the capacity data. Updates that increase the
+node will not invalidate the capacity, as we're more interested in “at
+least available” correctness, not “at most available”.
+
+Cache invalidation
+++++++++++++++++++
+
+If a partial node query is done (e.g. just for the node free space), and
+the returned values don't match with the cache, then the entire node
+state will be invalidated.
+
+By default, all LUs will invalidate the caches for all nodes and
+instances they lock. If an LU uses the BGL, then it will invalidate the
+entire cache. In time, it is expected that LUs will be modified to not
+invalidate, if they are not expected to change the node's and/or
+instance's state (e.g. ``LUConnectConsole``, or
+``LUActivateInstanceDisks``).
+
+Invalidation of a node's properties will also invalidate the capacity
+data associated with that node.
+
+Cache lifetime
+++++++++++++++
+
+The cache elements will have an upper bound on their lifetime; the
+proposal is to make this an hour, which should be a high enough value to
+cover the watcher being blocked by a medium-term job (e.g. 20-30
+minutes).
+
+Cache usage
++++++++++++
+
+The cache will be used by default for most queries (e.g. a Luxi call,
+without locks, for the entire cluster). Since this will be a change from
+the current behaviour, we'll need to allow non-cached responses,
+e.g. via a ``--cache=off`` or similar argument (which will force the
+query).
+
+The cache will also be used for the iallocator runs, so that computing
+allocation solution can proceed independent from other jobs which lock
+parts of the cluster. This is important as we need to separate
+allocation on one group from exclusive blocking jobs on other node
+groups.
+
+The capacity calculations will also use the cache. This is detailed in
+the respective sections.
+
+Watcher operation
+~~~~~~~~~~~~~~~~~
+
+As detailed in the cluster cache section, the watcher also needs
+improvements in order to scale with the the cluster size.
+
+As a first improvement, the proposal is to introduce a new OpCode/LU
+pair that runs with locks held over the entire query sequence (the
+current watcher runs a job with two opcodes, which grab and release the
+locks individually). The new opcode will be called
+``OpUpdateNodeGroupCache`` and will do the following:
+
+- try to acquire all node/instance locks (to examine in more depth, and
+  possibly alter) in the given node group
+- invalidate the cache for the node group
+- acquire node and instance state (possibly via a new single RPC call
+  that combines node and instance information)
+- update cache
+- return the needed data
+
+The reason for the per-node group query is that we don't want a busy
+node group to prevent instance maintenance in other node
+groups. Therefore, the watcher will introduce parallelism across node
+groups, and it will possible to have overlapping watcher runs. The new
+execution sequence will be:
+
+- the parent watcher process acquires global watcher lock
+- query the list of node groups (lockless or very short locks only)
+- fork N children, one for each node group
+- release the global lock
+- poll/wait for the children to finish
+
+Each forked children will do the following:
+
+- try to acquire the per-node group watcher lock
+- if fail to acquire, exit with special code telling the parent that the
+  node group is already being managed by a watcher process
+- otherwise, submit a OpUpdateNodeGroupCache job
+- get results (possibly after a long time, due to busy group)
+- run the needed maintenance operations for the current group
+
+This new mode of execution means that the master watcher processes might
+overlap in running, but not the individual per-node group child
+processes.
+
+This change allows us to keep (almost) the same parallelism when using a
+bigger cluster with node groups versus two separate clusters.
+
+
+Cost of periodic cache updating
++++++++++++++++++++++++++++++++
+
+Currently the watcher only does “small” queries for the node and
+instance state, and at first sight changing it to use the new OpCode
+which populates the cache with the entire state might introduce
+additional costs, which must be payed every five minutes.
+
+However, the OpCodes that the watcher submits are using the so-called
+dynamic fields (need to contact the remote nodes), and the LUs are not
+selective—they always grab all the node and instance state. So in the
+end, we have the same cost, it just becomes explicit rather than
+implicit.
+
+This ‘grab all node state’ behaviour is what makes the cache worth
+implementing.
+
+Intra-node group scalability
+++++++++++++++++++++++++++++
+
+The design above only deals with inter-node group issues. It still makes
+sense to run instance maintenance for nodes A and B if only node C is
+locked (all being in the same node group).
+
+This problem is commonly encountered in previous Ganeti versions, and it
+should be handled similarly, by tweaking lock lifetime in long-duration
+jobs.
+
+TODO: add more ideas here.
+
+
+State file maintenance
+++++++++++++++++++++++
+
+The splitting of node group maintenance to different children which will
+run in parallel requires that the state file handling changes from
+monolithic updates to partial ones.
+
+There are two file that the watcher maintains:
+
+- ``$LOCALSTATEDIR/lib/ganeti/watcher.data``, its internal state file,
+  used for deciding internal actions
+- ``$LOCALSTATEDIR/run/ganeti/instance-status``, a file designed for
+  external consumption
+
+For the first file, since it's used only internally to the watchers, we
+can move to a per node group configuration.
+
+For the second file, even if it's used as an external interface, we will
+need to make some changes to it: because the different node groups can
+return results at different times, we need to either split the file into
+per-group files or keep the single file and add a per-instance timestamp
+(currently the file holds only the instance name and state).
+
+The proposal is that each child process maintains its own node group
+file, and the master process will, right after querying the node group
+list, delete any extra per-node group state file. This leaves the
+consumers to run a simple ``cat instance-status.group-*`` to obtain the
+entire list of instance and their states. If needed, the modify
+timestamp of each file can be used to determine the age of the results.
+
+
+Capacity calculations
+~~~~~~~~~~~~~~~~~~~~~
+
+Currently, the capacity calculations are done completely outside
+Ganeti. As explained in the current problems section, this needs to
+account better for the cluster state changes.
+
+Therefore a new OpCode will be introduced, ``OpComputeCapacity``, that
+will either return the current capacity numbers (if available), or
+trigger a new capacity calculation, via the iallocator framework, which
+will get a new method called ``capacity``.
+
+This method will feed the cluster state (for the complete set of node
+group, or alternative just a subset) to the iallocator plugin (either
+the specified one, or the default if none is specified), and return the
+new capacity in the format currently exported by the htools suite and
+known as the “tiered specs” (see :manpage:`hspace(1)`).
+
+tspec cluster parameters
+++++++++++++++++++++++++
+
+Currently, the “tspec” calculations done in :command:`hspace` require
+some additional parameters:
+
+- maximum instance size
+- type of instance storage
+- maximum ratio of virtual CPUs per physical CPUs
+- minimum disk free
+
+For the integration in Ganeti, there are multiple ways to pass these:
+
+- ignored by Ganeti, and being the responsibility of the iallocator
+  plugin whether to use these at all or not
+- as input to the opcode
+- as proper cluster parameters
+
+Since the first option is not consistent with the intended changes, a
+combination of the last two is proposed:
+
+- at cluster level, we'll have cluster-wide defaults
+- at node groups, we'll allow overriding the cluster defaults
+- and if they are passed in via the opcode, they will override for the
+  current computation the values
+
+Whenever the capacity is requested via different parameters, it will
+invalidate the cache, even if otherwise the cache is up-to-date.
+
+The new parameters are:
+
+- max_inst_spec: (int, int, int), the maximum instance specification
+  accepted by this cluster or node group, in the order of memory, disk,
+  vcpus;
+- default_template: string, the default disk template to use
+- max_cpu_ratio: double, the maximum ratio of VCPUs/PCPUs
+- max_disk_usage: double, the maximum disk usage (as a ratio)
+
+These might also be used in instance creations (to be determined later,
+after they are introduced).
+
+OpCode details
+++++++++++++++
+
+Input:
+
+- iallocator: string (optional, otherwise uses the cluster default)
+- cached: boolean, optional, defaults to true, and denotes whether we
+  accept cached responses
+- the above new parameters, optional; if they are passed, they will
+  overwrite all node group's parameters
+
+Output:
+
+- cluster: list of tuples (memory, disk, vcpu, count), in decreasing
+  order of specifications; the first three members represent the
+  instance specification, the last one the count of how many instances
+  of this specification can be created on the cluster
+- node_groups: a dictionary keyed by node group UUID, with values a
+  dictionary:
+
+  - tspecs: a list like the cluster one
+  - additionally, the new cluster parameters, denoting the input
+    parameters that were used for this node group
+
+- ctime: the date the result has been computed; this represents the
+  oldest creation time amongst all node groups (so as to accurately
+  represent how much out-of-date the global response is)
+
+Note that due to the way the tspecs are computed, for any given
+specification, the total available count is the count for the given
+entry, plus the sum of counts for higher specifications.
+
+
+Node flags
+----------
+
+Current state and shortcomings
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Currently all nodes are, from the point of view of their capabilities,
+homogeneous. This means the cluster considers all nodes capable of
+becoming master candidates, and of hosting instances.
+
+This prevents some deployment scenarios: e.g. having a Ganeti instance
+(in another cluster) be just a master candidate, in case all other
+master candidates go down (but not, of course, host instances), or
+having a node in a remote location just host instances but not become
+master, etc.
+
+Proposed changes
+~~~~~~~~~~~~~~~~
+
+Two new capability flags will be added to the node:
+
+- master_capable, denoting whether the node can become a master
+  candidate or master
+- vm_capable, denoting whether the node can host instances
+
+In terms of the other flags, master_capable is a stronger version of
+"not master candidate", and vm_capable is a stronger version of
+"drained".
+
+For the master_capable flag, it will affect auto-promotion code and node
+modifications.
+
+The vm_capable flag will affect the iallocator protocol, capacity
+calculations, node checks in cluster verify, and will interact in novel
+ways with locking (unfortunately).
+
+It is envisaged that most nodes will be both vm_capable and
+master_capable, and just a few will have one of these flags
+removed. Ganeti itself will allow clearing of both flags, even though
+this doesn't make much sense currently.
+
+
+Job priorities
+--------------
+
+Current state and shortcomings
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Currently all jobs and opcodes have the same priority. Once a job
+started executing, its thread won't be released until all opcodes got
+their locks and did their work. When a job is finished, the next job is
+selected strictly by its incoming order. This does not mean jobs are run
+in their incoming order—locks and other delays can cause them to be
+stalled for some time.
+
+In some situations, e.g. an emergency shutdown, one may want to run a
+job as soon as possible. This is not possible currently if there are
+pending jobs in the queue.
+
+Proposed changes
+~~~~~~~~~~~~~~~~
+
+Each opcode will be assigned a priority on submission. Opcode priorities
+are integers and the lower the number, the higher the opcode's priority
+is. Within the same priority, jobs and opcodes are initially processed
+in their incoming order.
+
+Submitted opcodes can have one of the priorities listed below. Other
+priorities are reserved for internal use. The absolute range is
+-20..+19. Opcodes submitted without a priority (e.g. by older clients)
+are assigned the default priority.
+
+  - High (-10)
+  - Normal (0, default)
+  - Low (+10)
+
+As a change from the current model where executing a job blocks one
+thread for the whole duration, the new job processor must return the job
+to the queue after each opcode and also if it can't get all locks in a
+reasonable timeframe. This will allow opcodes of higher priority
+submitted in the meantime to be processed or opcodes of the same
+priority to try to get their locks. When added to the job queue's
+workerpool, the priority is determined by the first unprocessed opcode
+in the job.
+
+If an opcode is deferred, the job will go back to the "queued" status,
+even though it's just waiting to try to acquire its locks again later.
+
+If an opcode can not be processed after a certain number of retries or a
+certain amount of time, it should increase its priority. This will avoid
+starvation.
+
+A job's priority can never go below -20. If a job hits priority -20, it
+must acquire its locks in blocking mode.
+
+Opcode priorities are synchronised to disk in order to be restored after
+a restart or crash of the master daemon.
+
+Priorities also need to be considered inside the locking library to
+ensure opcodes with higher priorities get locks first. See
+:ref:`locking priorities <locking-priorities>` for more details.
+
+Worker pool
++++++++++++
+
+To support job priorities in the job queue, the worker pool underlying
+the job queue must be enhanced to support task priorities. Currently
+tasks are processed in the order they are added to the queue (but, due
+to their nature, they don't necessarily finish in that order). All tasks
+are equal. To support tasks with higher or lower priority, a few changes
+have to be made to the queue inside a worker pool.
+
+Each task is assigned a priority when added to the queue. This priority
+can not be changed until the task is executed (this is fine as in all
+current use-cases, tasks are added to a pool and then forgotten about
+until they're done).
+
+A task's priority can be compared to Unix' process priorities. The lower
+the priority number, the closer to the queue's front it is. A task with
+priority 0 is going to be run before one with priority 10. Tasks with
+the same priority are executed in the order in which they were added.
+
+While a task is running it can query its own priority. If it's not ready
+yet for finishing, it can raise an exception to defer itself, optionally
+changing its own priority. This is useful for the following cases:
+
+- A task is trying to acquire locks, but those locks are still held by
+  other tasks. By deferring itself, the task gives others a chance to
+  run. This is especially useful when all workers are busy.
+- If a task decides it hasn't gotten its locks in a long time, it can
+  start to increase its own priority.
+- Tasks waiting for long-running operations running asynchronously could
+  defer themselves while waiting for a long-running operation.
+
+With these changes, the job queue will be able to implement per-job
+priorities.
+
+.. _locking-priorities:
+
+Locking
++++++++
+
+In order to support priorities in Ganeti's own lock classes,
+``locking.SharedLock`` and ``locking.LockSet``, the internal structure
+of the former class needs to be changed. The last major change in this
+area was done for Ganeti 2.1 and can be found in the respective
+:doc:`design document <design-2.1>`.
+
+The plain list (``[]``) used as a queue is replaced by a heap queue,
+similar to the `worker pool`_. The heap or priority queue does automatic
+sorting, thereby automatically taking care of priorities. For each
+priority there's a plain list with pending acquires, like the single
+queue of pending acquires before this change.
+
+When the lock is released, the code locates the list of pending acquires
+for the highest priority waiting. The first condition (index 0) is
+notified. Once all waiting threads received the notification, the
+condition is removed from the list. If the list of conditions is empty
+it's removed from the heap queue.
+
+Like before, shared acquires are grouped and skip ahead of exclusive
+acquires if there's already an existing shared acquire for a priority.
+To accomplish this, a separate dictionary of shared acquires per
+priority is maintained.
+
+To simplify the code and reduce memory consumption, the concept of the
+"active" and "inactive" condition for shared acquires is abolished. The
+lock can't predict what priorities the next acquires will use and even
+keeping a cache can become computationally expensive for arguable
+benefit (the underlying POSIX pipe, see ``pipe(2)``, needs to be
+re-created for each notification anyway).
+
+The following diagram shows a possible state of the internal queue from
+a high-level view. Conditions are shown as (waiting) threads. Assuming
+no modifications are made to the queue (e.g. more acquires or timeouts),
+the lock would be acquired by the threads in this order (concurrent
+acquires in parentheses): ``threadE1``, ``threadE2``, (``threadS1``,
+``threadS2``, ``threadS3``), (``threadS4``, ``threadS5``), ``threadE3``,
+``threadS6``, ``threadE4``, ``threadE5``.
+
+::
+
+  [
+    (0, [exc/threadE1, exc/threadE2, shr/threadS1/threadS2/threadS3]),
+    (2, [shr/threadS4/threadS5]),
+    (10, [exc/threadE3]),
+    (33, [shr/threadS6, exc/threadE4, exc/threadE5]),
+  ]
+
+
+IPv6 support
+------------
+
+Currently Ganeti does not support IPv6. This is true for nodes as well
+as instances. Due to the fact that IPv4 exhaustion is threateningly near
+the need of using IPv6 is increasing, especially given that bigger and
+bigger clusters are supported.
+
+Supported IPv6 setup
+~~~~~~~~~~~~~~~~~~~~
+
+In Ganeti 2.3 we introduce additionally to the ordinary pure IPv4
+setup a hybrid IPv6/IPv4 mode. The latter works as follows:
+
+- all nodes in a cluster have a primary IPv6 address
+- the master has a IPv6 address
+- all nodes **must** have a secondary IPv4 address
+
+The reason for this hybrid setup is that key components that Ganeti
+depends on do not or only partially support IPv6. More precisely, Xen
+does not support instance migration via IPv6 in version 3.4 and 4.0.
+Similarly, KVM does not support instance migration nor VNC access for
+IPv6 at the time of this writing.
+
+This led to the decision of not supporting pure IPv6 Ganeti clusters, as
+very important cluster operations would not have been possible. Using
+IPv4 as secondary address does not affect any of the goals
+of the IPv6 support: since secondary addresses do not need to be
+publicly accessible, they need not be globally unique. In other words,
+one can practically use private IPv4 secondary addresses just for
+intra-cluster communication without propagating them across layer 3
+boundaries.
+
+netutils: Utilities for handling common network tasks
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Currently common utility functions are kept in the ``utils`` module.
+Since this module grows bigger and bigger network-related functions are
+moved to a separate module named *netutils*. Additionally all these
+utilities will be IPv6-enabled.
+
+Cluster initialization
+~~~~~~~~~~~~~~~~~~~~~~
+
+As mentioned above there will be two different setups in terms of IP
+addressing: pure IPv4 and hybrid IPv6/IPv4 address. To choose that a
+new cluster init parameter *--primary-ip-version* is introduced. This is
+needed as a given name can resolve to both an IPv4 and IPv6 address on a
+dual-stack host effectively making it impossible to infer that bit.
+
+Once a cluster is initialized and the primary IP version chosen all
+nodes that join have to conform to that setup. In the case of our
+IPv6/IPv4 setup all nodes *must* have a secondary IPv4 address.
+
+Furthermore we store the primary IP version in ssconf which is consulted
+every time a daemon starts to determine the default bind address (either
+*0.0.0.0* or *::*. In a IPv6/IPv4 setup we need to bind the Ganeti
+daemon listening on network sockets to the IPv6 address.
+
+Node addition
+~~~~~~~~~~~~~
+
+When adding a new node to a IPv6/IPv4 cluster it must have a IPv6
+address to be used as primary and a IPv4 address used as secondary. As
+explained above, every time a daemon is started we use the cluster
+primary IP version to determine to which any address to bind to. The
+only exception to this is when a node is added to the cluster. In this
+case there is no ssconf available when noded is started and therefore
+the correct address needs to be passed to it.
+
+Name resolution
+~~~~~~~~~~~~~~~
+
+Since the gethostbyname*() functions do not support IPv6 name resolution
+will be done by using the recommended getaddrinfo().
+
+IPv4-only components
+~~~~~~~~~~~~~~~~~~~~
+
+============================  ===================  ====================
+Component                     IPv6 Status          Planned Version
+============================  ===================  ====================
+Xen instance migration        Not supported        Xen 4.1: libxenlight
+KVM instance migration        Not supported        Unknown
+KVM VNC access                Not supported        Unknown
+============================  ===================  ====================
+
+
+Privilege Separation
+--------------------
+
+Current state and shortcomings
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In Ganeti 2.2 we introduced privilege separation for the RAPI daemon.
+This was done directly in the daemon's code in the process of
+daemonizing itself. Doing so leads to several potential issues. For
+example, a file could be opened while the code is still running as
+``root`` and for some reason not be closed again. Even after changing
+the user ID, the file descriptor can be written to.
+
+Implementation
+~~~~~~~~~~~~~~
+
+To address these shortcomings, daemons will be started under the target
+user right away. The ``start-stop-daemon`` utility used to start daemons
+supports the ``--chuid`` option to change user and group ID before
+starting the executable.
+
+The intermediate solution for the RAPI daemon from Ganeti 2.2 will be
+removed again.
+
+Files written by the daemons may need to have an explicit owner and
+group set (easily done through ``utils.WriteFile``).
+
+All SSH-related code is removed from the ``ganeti.bootstrap`` module and
+core components and moved to a separate script. The core code will
+simply assume a working SSH setup to be in place.
+
+Security Domains
+~~~~~~~~~~~~~~~~
+
+In order to separate the permissions of file sets we separate them
+into the following 3 overall security domain chunks:
+
+1. Public: ``0755`` respectively ``0644``
+2. Ganeti wide: shared between the daemons (gntdaemons)
+3. Secret files: shared among a specific set of daemons/users
+
+So for point 3 this tables shows the correlation of the sets to groups
+and their users:
+
+=== ========== ============================== ==========================
+Set Group      Users                          Description
+=== ========== ============================== ==========================
+A   gntrapi    gntrapi, gntmasterd            Share data between
+                                              gntrapi and gntmasterd
+B   gntadmins  gntrapi, gntmasterd, *users*   Shared between users who
+                                              needs to call gntmasterd
+C   gntconfd   gntconfd, gntmasterd           Share data between
+                                              gntconfd and gntmasterd
+D   gntmasterd gntmasterd                     masterd only; Currently
+                                              only to redistribute the
+                                              configuration, has access
+                                              to all files under
+                                              ``lib/ganeti``
+E   gntdaemons gntmasterd, gntrapi, gntconfd  Shared between the various
+                                              Ganeti daemons to exchange
+                                              data
+=== ========== ============================== ==========================
+
+Restricted commands
+~~~~~~~~~~~~~~~~~~~
+
+The following commands needs still root to fulfill their functions:
+
+::
+
+  gnt-cluster {init|destroy|command|copyfile|rename|masterfailover|renew-crypto}
+  gnt-node {add|remove}
+  gnt-instance {console}
+
+Directory structure and permissions
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Here's how we propose to change the filesystem hierarchy and their
+permissions.
+
+Assuming it follows the defaults: ``gnt${daemon}`` for user and
+the groups from the section `Security Domains`_::
+
+  ${localstatedir}/lib/ganeti/ (0755; gntmasterd:gntmasterd)
+     cluster-domain-secret (0600; gntmasterd:gntmasterd)
+     config.data (0640; gntmasterd:gntconfd)
+     hmac.key (0440; gntmasterd:gntconfd)
+     known_host (0644; gntmasterd:gntmasterd)
+     queue/ (0700; gntmasterd:gntmasterd)
+       archive/ (0700; gntmasterd:gntmasterd)
+         * (0600; gntmasterd:gntmasterd)
+       * (0600; gntmasterd:gntmasterd)
+     rapi.pem (0440; gntrapi:gntrapi)
+     rapi_users (0640; gntrapi:gntrapi)
+     server.pem (0440; gntmasterd:gntmasterd)
+     ssconf_* (0444; root:gntmasterd)
+     uidpool/ (0750; root:gntmasterd)
+     watcher.data (0600; root:gntmasterd)
+  ${localstatedir}/run/ganeti/ (0770; gntmasterd:gntdaemons)
+     socket/ (0750; gntmasterd:gntadmins)
+       ganeti-master (0770; gntmasterd:gntadmins)
+  ${localstatedir}/log/ganeti/ (0770; gntmasterd:gntdaemons)
+     master-daemon.log (0600; gntmasterd:gntdaemons)
+     rapi-daemon.log (0600; gntrapi:gntdaemons)
+     conf-daemon.log (0600; gntconfd:gntdaemons)
+     node-daemon.log (0600; gntnoded:gntdaemons)
+
+
+Feature changes
+===============
+
+
+External interface changes
+==========================
+
+
+.. vim: set textwidth=72 :
+.. Local Variables:
+.. mode: rst
+.. fill-column: 72
+.. End:
index 0aa9898..898d03a 100644 (file)
@@ -29,8 +29,7 @@ Run the following command (only use ``PYTHON=...`` if you need to use a
 different python version)::
 
   ./autogen.sh && \
-  ./configure PYTHON=python2.4 \
-    --prefix=/usr/local --sysconfdir=/etc --localstatedir=/var
+  ./configure --prefix=/usr/local --sysconfdir=/etc --localstatedir=/var
 
 
 Packaging notes
old mode 100755 (executable)
new mode 100644 (file)
index de2759c..6b90e86 100644 (file)
@@ -171,6 +171,14 @@ request
   nodes
     the names of the nodes to be evacuated
 
+nodegroups
+  a dictionary with the data for the cluster's node groups; it is keyed
+  on the group UUID, and the values are a dictionary with the following
+  keys:
+
+  name
+    the node group name
+
 instances
   a dictionary with the data for the current existing instance on the
   cluster, indexed by instance name; the contents are similar to the
@@ -231,6 +239,9 @@ nodes
   i_pri_up_memory:
     total memory required by running primary instances
 
+  group:
+    the node group that this node belongs to
+
   No allocations should be made on nodes having either the ``drained``
   or ``offline`` flags set. More details about these of node status
   flags is available in the manpage :manpage:`ganeti(7)`.
index 668c945..80ae4c0 100644 (file)
@@ -17,6 +17,7 @@ Contents:
    design-2.0.rst
    design-2.1.rst
    design-2.2.rst
+   design-2.3.rst
    cluster-merge.rst
    locking.rst
    hooks.rst
index 2299cb7..c25ba6e 100644 (file)
@@ -21,8 +21,8 @@ Users and passwords
 -------------------
 
 ``ganeti-rapi`` reads users and passwords from a file (usually
-``/var/lib/ganeti/rapi_users``) on startup. After modifying the password
-file, ``ganeti-rapi`` must be restarted.
+``/var/lib/ganeti/rapi_users``) on startup. Changes to the file will be
+read automatically.
 
 Each line consists of two or three fields separated by whitespace. The
 first two fields are for username and password. The third field is
index f2cafa2..ce0edc3 100644 (file)
@@ -1023,6 +1023,25 @@ hexadecimal, and 0x93 represents DRBD's major number. Thus we can see
 from the above that instance2 has /dev/drbd0, instance3 /dev/drbd1, and
 instance4 /dev/drbd2.
 
+LUXI version mismatch
++++++++++++++++++++++
+
+LUXI is the protocol used for communication between clients and the
+master daemon. Starting in Ganeti 2.3, the peers exchange their version
+in each message. When they don't match, an error is raised::
+
+  $ gnt-node modify -O yes node3
+  Unhandled Ganeti error: LUXI version mismatch, server 2020000, request 2030000
+
+Usually this means that server and client are from different Ganeti
+versions or import their libraries from different, consistent paths
+(e.g. an older version installed in another place). You can print the
+import path for Ganeti's modules using the following command (note that
+depending on your setup you might have to use an explicit version in the
+Python command, e.g. ``python2.6``)::
+
+  python -c 'import ganeti; print ganeti.__file__'
+
 .. vim: set textwidth=72 :
 .. Local Variables:
 .. mode: rst
index 4f3cbb7..1217299 100644 (file)
@@ -25,3 +25,5 @@ inheritance: listed
 
 parse: yes
 introspect: no
+
+fail-on: docstring_warnings
index e60c260..9d5610e 100644 (file)
@@ -76,11 +76,58 @@ class ErrorLoggingAsyncNotifier(AsyncNotifier,
   """
 
 
-class SingleFileEventHandler(pyinotify.ProcessEvent):
-  """Handle modify events for a single file.
+class FileEventHandlerBase(pyinotify.ProcessEvent):
+  """Base class for file event handlers.
+
+  @ivar watch_manager: Inotify watch manager
 
   """
+  def __init__(self, watch_manager):
+    """Initializes this class.
+
+    @type watch_manager: pyinotify.WatchManager
+    @param watch_manager: inotify watch manager
+
+    """
+    # pylint: disable-msg=W0231
+    # no need to call the parent's constructor
+    self.watch_manager = watch_manager
+
+  def process_default(self, event):
+    logging.error("Received unhandled inotify event: %s", event)
+
+  def AddWatch(self, filename, mask):
+    """Adds a file watch.
+
+    @param filename: Path to file
+    @param mask: Inotify event mask
+    @return: Result
+
+    """
+    result = self.watch_manager.add_watch(filename, mask)
+
+    ret = result.get(filename, -1)
+    if ret <= 0:
+      raise errors.InotifyError("Could not add inotify watcher (%s)" % ret)
+
+    return result[filename]
 
+  def RemoveWatch(self, handle):
+    """Removes a handle from the watcher.
+
+    @param handle: Inotify handle
+    @return: Whether removal was successful
+
+    """
+    result = self.watch_manager.rm_watch(handle)
+
+    return result[handle]
+
+
+class SingleFileEventHandler(FileEventHandlerBase):
+  """Handle modify events for a single file.
+
+  """
   def __init__(self, watch_manager, callback, filename):
     """Constructor for SingleFileEventHandler
 
@@ -92,34 +139,33 @@ class SingleFileEventHandler(pyinotify.ProcessEvent):
     @param filename: config file to watch
 
     """
-    # pylint: disable-msg=W0231
-    # no need to call the parent's constructor
-    self.watch_manager = watch_manager
-    self.callback = callback
-    self.mask = pyinotify.EventsCodes.ALL_FLAGS["IN_IGNORED"] | \
-                pyinotify.EventsCodes.ALL_FLAGS["IN_MODIFY"]
-    self.file = filename
-    self.watch_handle = None
+    FileEventHandlerBase.__init__(self, watch_manager)
+
+    self._callback = callback
+    self._filename = filename
+
+    self._watch_handle = None
 
   def enable(self):
-    """Watch the given file
+    """Watch the given file.
 
     """
-    if self.watch_handle is None:
-      result = self.watch_manager.add_watch(self.file, self.mask)
-      if not self.file in result or result[self.file] <= 0:
-        raise errors.InotifyError("Could not add inotify watcher")
-      else:
-        self.watch_handle = result[self.file]
+    if self._watch_handle is not None:
+      return
+
+    # Different Pyinotify versions have the flag constants at different places,
+    # hence not accessing them directly
+    mask = (pyinotify.EventsCodes.ALL_FLAGS["IN_MODIFY"] |
+            pyinotify.EventsCodes.ALL_FLAGS["IN_IGNORED"])
+
+    self._watch_handle = self.AddWatch(self._filename, mask)
 
   def disable(self):
-    """Stop watching the given file
+    """Stop watching the given file.
 
     """
-    if self.watch_handle is not None:
-      result = self.watch_manager.rm_watch(self.watch_handle)
-      if result[self.watch_handle]:
-        self.watch_handle = None
+    if self._watch_handle is not None and self.RemoveWatch(self._watch_handle):
+      self._watch_handle = None
 
   # pylint: disable-msg=C0103
   # this overrides a method in pyinotify.ProcessEvent
@@ -132,8 +178,8 @@ class SingleFileEventHandler(pyinotify.ProcessEvent):
     # case we'll need to create a watcher for the "new" file. This can be done
     # by the callback by calling "enable" again on us.
     logging.debug("Received 'ignored' inotify event for %s", event.path)
-    self.watch_handle = None
-    self.callback(False)
+    self._watch_handle = None
+    self._callback(False)
 
   # pylint: disable-msg=C0103
   # this overrides a method in pyinotify.ProcessEvent
@@ -143,7 +189,4 @@ class SingleFileEventHandler(pyinotify.ProcessEvent):
     # replacing any file with a new one, at filesystem level, rather than
     # actually changing it. (see utils.WriteFile)
     logging.debug("Received 'modify' inotify event for %s", event.path)
-    self.callback(True)
-
-  def process_default(self, event):
-    logging.error("Received unhandled inotify event: %s", event)
+    self._callback(True)
index 0216146..1e5afa0 100644 (file)
@@ -59,6 +59,7 @@ from ganeti import objects
 from ganeti import ssconf
 from ganeti import serializer
 from ganeti import netutils
+from ganeti import runtime
 
 
 _BOOT_ID_PATH = "/proc/sys/kernel/random/boot_id"
@@ -225,7 +226,7 @@ def GetMasterInfo():
   for consumption here or from the node daemon.
 
   @rtype: tuple
-  @return: master_netdev, master_ip, master_name
+  @return: master_netdev, master_ip, master_name, primary_ip_family
   @raise RPCFail: in case of errors
 
   """
@@ -234,9 +235,10 @@ def GetMasterInfo():
     master_netdev = cfg.GetMasterNetdev()
     master_ip = cfg.GetMasterIP()
     master_node = cfg.GetMasterNode()
+    primary_ip_family = cfg.GetPrimaryIPFamily()
   except errors.ConfigurationError, err:
     _Fail("Cluster configuration incomplete: %s", err, exc=True)
-  return (master_netdev, master_ip, master_node)
+  return (master_netdev, master_ip, master_node, primary_ip_family)
 
 
 def StartMaster(start_daemons, no_voting):
@@ -257,7 +259,7 @@ def StartMaster(start_daemons, no_voting):
 
   """
   # GetMasterInfo will raise an exception if not able to return data
-  master_netdev, master_ip, _ = GetMasterInfo()
+  master_netdev, master_ip, _, family = GetMasterInfo()
 
   err_msgs = []
   # either start the master and rapi daemons
@@ -279,7 +281,7 @@ def StartMaster(start_daemons, no_voting):
   # or activate the IP
   else:
     if netutils.TcpPing(master_ip, constants.DEFAULT_NODED_PORT):
-      if netutils.OwnIpAddress(master_ip):
+      if netutils.IPAddress.Own(master_ip):
         # we already have the ip:
         logging.debug("Master IP already configured, doing nothing")
       else:
@@ -287,7 +289,12 @@ def StartMaster(start_daemons, no_voting):
         logging.error(msg)
         err_msgs.append(msg)
     else:
-      result = utils.RunCmd(["ip", "address", "add", "%s/32" % master_ip,
+      ipcls = netutils.IP4Address
+      if family == netutils.IP6Address.family:
+        ipcls = netutils.IP6Address
+
+      result = utils.RunCmd(["ip", "address", "add",
+                             "%s/%d" % (master_ip, ipcls.iplen),
                              "dev", master_netdev, "label",
                              "%s:0" % master_netdev])
       if result.failed:
@@ -295,9 +302,16 @@ def StartMaster(start_daemons, no_voting):
         logging.error(msg)
         err_msgs.append(msg)
 
-      result = utils.RunCmd(["arping", "-q", "-U", "-c 3", "-I", master_netdev,
-                             "-s", master_ip, master_ip])
-      # we'll ignore the exit code of arping
+      # we ignore the exit code of the following cmds
+      if ipcls == netutils.IP4Address:
+        utils.RunCmd(["arping", "-q", "-U", "-c 3", "-I", master_netdev, "-s",
+                      master_ip, master_ip])
+      elif ipcls == netutils.IP6Address:
+        try:
+          utils.RunCmd(["ndisc6", "-q", "-r 3", master_ip, master_netdev])
+        except errors.OpExecError:
+          # TODO: Better error reporting
+          logging.warning("Can't execute ndisc6, please install if missing")
 
   if err_msgs:
     _Fail("; ".join(err_msgs))
@@ -320,9 +334,14 @@ def StopMaster(stop_daemons):
   # need to decide in which case we fail the RPC for this
 
   # GetMasterInfo will raise an exception if not able to return data
-  master_netdev, master_ip, _ = GetMasterInfo()
+  master_netdev, master_ip, _, family = GetMasterInfo()
+
+  ipcls = netutils.IP4Address
+  if family == netutils.IP6Address.family:
+    ipcls = netutils.IP6Address
 
-  result = utils.RunCmd(["ip", "address", "del", "%s/32" % master_ip,
+  result = utils.RunCmd(["ip", "address", "del",
+                         "%s/%d" % (master_ip, ipcls.iplen),
                          "dev", master_netdev])
   if result.failed:
     logging.error("Can't remove the master IP, error: %s", result.output)
@@ -336,52 +355,26 @@ def StopMaster(stop_daemons):
                     result.cmd, result.exit_code, result.output)
 
 
-def AddNode(dsa, dsapub, rsa, rsapub, sshkey, sshpub):
-  """Joins this node to the cluster.
-
-  This does the following:
-      - updates the hostkeys of the machine (rsa and dsa)
-      - adds the ssh private key to the user
-      - adds the ssh public key to the users' authorized_keys file
-
-  @type dsa: str
-  @param dsa: the DSA private key to write
-  @type dsapub: str
-  @param dsapub: the DSA public key to write
-  @type rsa: str
-  @param rsa: the RSA private key to write
-  @type rsapub: str
-  @param rsapub: the RSA public key to write
-  @type sshkey: str
-  @param sshkey: the SSH private key to write
-  @type sshpub: str
-  @param sshpub: the SSH public key to write
-  @rtype: boolean
-  @return: the success of the operation
-
-  """
-  sshd_keys =  [(constants.SSH_HOST_RSA_PRIV, rsa, 0600),
-                (constants.SSH_HOST_RSA_PUB, rsapub, 0644),
-                (constants.SSH_HOST_DSA_PRIV, dsa, 0600),
-                (constants.SSH_HOST_DSA_PUB, dsapub, 0644)]
-  for name, content, mode in sshd_keys:
-    utils.WriteFile(name, data=content, mode=mode)
-
-  try:
-    priv_key, pub_key, auth_keys = ssh.GetUserFiles(constants.GANETI_RUNAS,
-                                                    mkdir=True)
-  except errors.OpExecError, err:
-    _Fail("Error while processing user ssh files: %s", err, exc=True)
+def EtcHostsModify(mode, host, ip):
+  """Modify a host entry in /etc/hosts.
 
-  for name, content in [(priv_key, sshkey), (pub_key, sshpub)]:
-    utils.WriteFile(name, data=content, mode=0600)
+  @param mode: The mode to operate. Either add or remove entry
+  @param host: The host to operate on
+  @param ip: The ip associated with the entry
 
-  utils.AddAuthorizedKey(auth_keys, sshpub)
-
-  result = utils.RunCmd([constants.DAEMON_UTIL, "reload-ssh-keys"])
-  if result.failed:
-    _Fail("Unable to reload SSH keys (command %r, exit code %s, output %r)",
-          result.cmd, result.exit_code, result.output)
+  """
+  if mode == constants.ETC_HOSTS_ADD:
+    if not ip:
+      RPCFail("Mode 'add' needs 'ip' parameter, but parameter not"
+              " present")
+    utils.AddHostToEtcHosts(host, ip)
+  elif mode == constants.ETC_HOSTS_REMOVE:
+    if ip:
+      RPCFail("Mode 'remove' does not allow 'ip' parameter, but"
+              " parameter is present")
+    utils.RemoveHostFromEtcHosts(host)
+  else:
+    RPCFail("Mode not supported")
 
 
 def LeaveCluster(modify_ssh_setup):
@@ -446,9 +439,15 @@ def GetNodeInfo(vgname, hypervisor_type):
 
   """
   outputarray = {}
-  vginfo = _GetVGInfo(vgname)
-  outputarray['vg_size'] = vginfo['vg_size']
-  outputarray['vg_free'] = vginfo['vg_free']
+
+  vginfo = bdev.LogicalVolume.GetVGInfo([vgname])
+  vg_free = vg_size = None
+  if vginfo:
+    vg_free = int(round(vginfo[0][0], 0))
+    vg_size = int(round(vginfo[0][1], 0))
+
+  outputarray['vg_size'] = vg_size
+  outputarray['vg_free'] = vg_free
 
   hyper = hypervisor.GetHypervisor(hypervisor_type)
   hyp_info = hyper.GetNodeInfo()
@@ -490,10 +489,11 @@ def VerifyNode(what, cluster_name):
 
   """
   result = {}
-  my_name = netutils.HostInfo().name
+  my_name = netutils.Hostname.GetSysName()
   port = netutils.GetDaemonPort(constants.NODED)
+  vm_capable = my_name not in what.get(constants.NV_VMNODES, [])
 
-  if constants.NV_HYPERVISOR in what:
+  if constants.NV_HYPERVISOR in what and vm_capable:
     result[constants.NV_HYPERVISOR] = tmp = {}
     for hv_name in what[constants.NV_HYPERVISOR]:
       try:
@@ -548,14 +548,14 @@ def VerifyNode(what, cluster_name):
     result[constants.NV_MASTERIP] = netutils.TcpPing(master_ip, port,
                                                   source=source)
 
-  if constants.NV_LVLIST in what:
+  if constants.NV_LVLIST in what and vm_capable:
     try:
       val = GetVolumeList(what[constants.NV_LVLIST])
     except RPCFail, err:
       val = str(err)
     result[constants.NV_LVLIST] = val
 
-  if constants.NV_INSTANCELIST in what:
+  if constants.NV_INSTANCELIST in what and vm_capable:
     # GetInstanceList can fail
     try:
       val = GetInstanceList(what[constants.NV_INSTANCELIST])
@@ -563,10 +563,10 @@ def VerifyNode(what, cluster_name):
       val = str(err)
     result[constants.NV_INSTANCELIST] = val
 
-  if constants.NV_VGLIST in what:
+  if constants.NV_VGLIST in what and vm_capable:
     result[constants.NV_VGLIST] = utils.ListVolumeGroups()
 
-  if constants.NV_PVLIST in what:
+  if constants.NV_PVLIST in what and vm_capable:
     result[constants.NV_PVLIST] = \
       bdev.LogicalVolume.GetPVInfo(what[constants.NV_PVLIST],
                                    filter_allocatable=False)
@@ -575,11 +575,11 @@ def VerifyNode(what, cluster_name):
     result[constants.NV_VERSION] = (constants.PROTOCOL_VERSION,
                                     constants.RELEASE_VERSION)
 
-  if constants.NV_HVINFO in what:
+  if constants.NV_HVINFO in what and vm_capable:
     hyper = hypervisor.GetHypervisor(what[constants.NV_HVINFO])
     result[constants.NV_HVINFO] = hyper.GetNodeInfo()
 
-  if constants.NV_DRBDLIST in what:
+  if constants.NV_DRBDLIST in what and vm_capable:
     try:
       used_minors = bdev.DRBD8.GetUsedDevs().keys()
     except errors.BlockDeviceError, err:
@@ -587,7 +587,7 @@ def VerifyNode(what, cluster_name):
       used_minors = str(err)
     result[constants.NV_DRBDLIST] = used_minors
 
-  if constants.NV_DRBDHELPER in what:
+  if constants.NV_DRBDHELPER in what and vm_capable:
     status = True
     try:
       payload = bdev.BaseDRBD.GetUsermodeHelper()
@@ -612,7 +612,7 @@ def VerifyNode(what, cluster_name):
   if constants.NV_TIME in what:
     result[constants.NV_TIME] = utils.SplitTime(time.time())
 
-  if constants.NV_OSLIST in what:
+  if constants.NV_OSLIST in what and vm_capable:
     result[constants.NV_OSLIST] = DiagnoseOS()
 
   return result
@@ -937,46 +937,6 @@ def RunRenameInstance(instance, old_name, debug):
           " log file:\n%s", result.fail_reason, "\n".join(lines), log=False)
 
 
-def _GetVGInfo(vg_name):
-  """Get information about the volume group.
-
-  @type vg_name: str
-  @param vg_name: the volume group which we query
-  @rtype: dict
-  @return:
-    A dictionary with the following keys:
-      - C{vg_size} is the total size of the volume group in MiB
-      - C{vg_free} is the free size of the volume group in MiB
-      - C{pv_count} are the number of physical disks in that VG
-
-    If an error occurs during gathering of data, we return the same dict
-    with keys all set to None.
-
-  """
-  retdic = dict.fromkeys(["vg_size", "vg_free", "pv_count"])
-
-  retval = utils.RunCmd(["vgs", "-ovg_size,vg_free,pv_count", "--noheadings",
-                         "--nosuffix", "--units=m", "--separator=:", vg_name])
-
-  if retval.failed:
-    logging.error("volume group %s not present", vg_name)
-    return retdic
-  valarr = retval.stdout.strip().rstrip(':').split(':')
-  if len(valarr) == 3:
-    try:
-      retdic = {
-        "vg_size": int(round(float(valarr[0]), 0)),
-        "vg_free": int(round(float(valarr[1]), 0)),
-        "pv_count": int(valarr[2]),
-        }
-    except (TypeError, ValueError), err:
-      logging.exception("Fail to parse vgs output: %s", err)
-  else:
-    logging.error("vgs output has the wrong number of fields (expected"
-                  " three): %s", str(valarr))
-  return retdic
-
-
 def _GetBlockDevSymlinkPath(instance_name, idx):
   return utils.PathJoin(constants.DISK_LINKS_DIR,
                         "%s:%d" % (instance_name, idx))
@@ -1328,6 +1288,52 @@ def BlockdevCreate(disk, size, owner, on_primary, info):
   return device.unique_id
 
 
+def _WipeDevice(path, offset, size):
+  """This function actually wipes the device.
+
+  @param path: The path to the device to wipe
+  @param offset: The offset in MiB in the file
+  @param size: The size in MiB to write
+
+  """
+  cmd = [constants.DD_CMD, "if=/dev/zero", "seek=%d" % offset,
+         "bs=%d" % constants.WIPE_BLOCK_SIZE, "oflag=direct", "of=%s" % path,
+         "count=%d" % size]
+  result = utils.RunCmd(cmd)
+
+  if result.failed:
+    _Fail("Wipe command '%s' exited with error: %s; output: %s", result.cmd,
+          result.fail_reason, result.output)
+
+
+def BlockdevWipe(disk, offset, size):
+  """Wipes a block device.
+
+  @type disk: L{objects.Disk}
+  @param disk: the disk object we want to wipe
+  @type offset: int
+  @param offset: The offset in MiB in the file
+  @type size: int
+  @param size: The size in MiB to write
+
+  """
+  try:
+    rdev = _RecursiveFindBD(disk)
+  except errors.BlockDeviceError:
+    rdev = None
+
+  if not rdev:
+    _Fail("Cannot execute wipe for device %s: device not found", disk.iv_name)
+
+  # Do cross verify some of the parameters
+  if offset > rdev.size:
+    _Fail("Offset is bigger than device size")
+  if (offset + size) > rdev.size:
+    _Fail("The provided offset and size to wipe is bigger than device size")
+
+  _WipeDevice(rdev.dev_path, offset, size)
+
+
 def BlockdevRemove(disk):
   """Remove a block device.
 
@@ -1533,9 +1539,7 @@ def BlockdevGetmirrorstatus(disks):
   @type disks: list of L{objects.Disk}
   @param disks: the list of disks which we should query
   @rtype: disk
-  @return:
-      a list of (mirror_done, estimated_time) tuples, which
-      are the result of L{bdev.BlockDev.CombinedSyncStatus}
+  @return: List of L{objects.BlockDevStatus}, one for each disk
   @raise errors.BlockDeviceError: if any of the disks cannot be
       found
 
@@ -1551,6 +1555,37 @@ def BlockdevGetmirrorstatus(disks):
   return stats
 
 
+def BlockdevGetmirrorstatusMulti(disks):
+  """Get the mirroring status of a list of devices.
+
+  @type disks: list of L{objects.Disk}
+  @param disks: the list of disks which we should query
+  @rtype: disk
+  @return: List of tuples, (bool, status), one for each disk; bool denotes
+    success/failure, status is L{objects.BlockDevStatus} on success, string
+    otherwise
+
+  """
+  result = []
+  for disk in disks:
+    try:
+      rbd = _RecursiveFindBD(disk)
+      if rbd is None:
+        result.append((False, "Can't find device %s" % disk))
+        continue
+
+      status = rbd.CombinedSyncStatus()
+    except errors.BlockDeviceError, err:
+      logging.exception("Error while getting disk status")
+      result.append((False, str(err)))
+    else:
+      result.append((True, status))
+
+  assert len(disks) == len(result)
+
+  return result
+
+
 def _RecursiveFindBD(disk):
   """Check if a device is activated.
 
@@ -1712,8 +1747,9 @@ def UploadFile(file_name, data, mode, uid, gid, atime, mtime):
 
   raw_data = _Decompress(data)
 
-  utils.WriteFile(file_name, data=raw_data, mode=mode, uid=uid, gid=gid,
-                  atime=atime, mtime=mtime)
+  utils.SafeWriteFile(file_name, None,
+                      data=raw_data, mode=mode, uid=uid, gid=gid,
+                      atime=atime, mtime=mtime)
 
 
 def WriteSsconfFiles(values):
@@ -2007,8 +2043,9 @@ def OSEnvironment(instance, inst_os, debug=0):
   """
   result = OSCoreEnv(instance.os, inst_os, instance.osparams, debug=debug)
 
-  result['INSTANCE_NAME'] = instance.name
-  result['INSTANCE_OS'] = instance.os
+  for attr in ["name", "os", "uuid", "ctime", "mtime"]:
+    result["INSTANCE_%s" % attr.upper()] = str(getattr(instance, attr))
+
   result['HYPERVISOR'] = instance.hypervisor
   result['DISK_COUNT'] = '%d' % len(instance.disks)
   result['NIC_COUNT'] = '%d' % len(instance.nics)
@@ -2412,9 +2449,11 @@ def JobQueueUpdate(file_name, content):
 
   """
   _EnsureJobQueueFile(file_name)
+  getents = runtime.GetEnts()
 
   # Write and replace the file atomically
-  utils.WriteFile(file_name, data=_Decompress(content))
+  utils.WriteFile(file_name, data=_Decompress(content), uid=getents.masterd_uid,
+                  gid=getents.masterd_gid)
 
 
 def JobQueueRename(old, new):
@@ -2596,7 +2635,7 @@ def CreateX509Certificate(validity, cryptodir=constants.CRYPTO_KEYS_DIR):
 
   """
   (key_pem, cert_pem) = \
-    utils.GenerateSelfSignedX509Cert(netutils.HostInfo.SysName(),
+    utils.GenerateSelfSignedX509Cert(netutils.Hostname.GetSysName(),
                                      min(validity, _MAX_SSL_CERT_VALIDITY))
 
   cert_dir = tempfile.mkdtemp(dir=cryptodir,
@@ -2939,7 +2978,7 @@ def _FindDisks(nodes_ip, disks):
 
   """
   # set the correct physical ID
-  my_name = netutils.HostInfo().name
+  my_name = netutils.Hostname.GetSysName()
   for cf in disks:
     cf.SetPhysicalID(my_name, nodes_ip)
 
index 19c991f..1378a2f 100644 (file)
@@ -418,7 +418,40 @@ class LogicalVolume(BlockDev):
     return LogicalVolume(unique_id, children, size)
 
   @staticmethod
-  def GetPVInfo(vg_names, filter_allocatable=True):
+  def _GetVolumeInfo(lvm_cmd, fields):
+    """Returns LVM Volumen infos using lvm_cmd
+
+    @param lvm_cmd: Should be one of "pvs", "vgs" or "lvs"
+    @param fields: Fields to return
+    @return: A list of dicts each with the parsed fields
+
+    """
+    if not fields:
+      raise errors.ProgrammerError("No fields specified")
+
+    sep = "|"
+    cmd = [lvm_cmd, "--noheadings", "--nosuffix", "--units=m", "--unbuffered",
+           "--separator=%s" % sep, "-o%s" % ",".join(fields)]
+
+    result = utils.RunCmd(cmd)
+    if result.failed:
+      raise errors.CommandError("Can't get the volume information: %s - %s" %
+                                (result.fail_reason, result.output))
+
+    data = []
+    for line in result.stdout.splitlines():
+      splitted_fields = line.strip().split(sep)
+
+      if len(fields) != len(splitted_fields):
+        raise errors.CommandError("Can't parse %s output: line '%s'" %
+                                  (lvm_cmd, line))
+
+      data.append(splitted_fields)
+
+    return data
+
+  @classmethod
+  def GetPVInfo(cls, vg_names, filter_allocatable=True):
     """Get the free space info for PVs in a volume group.
 
     @param vg_names: list of volume group names, if empty all will be returned
@@ -428,28 +461,53 @@ class LogicalVolume(BlockDev):
     @return: list of tuples (free_space, name) with free_space in mebibytes
 
     """
-    sep = "|"
-    command = ["pvs", "--noheadings", "--nosuffix", "--units=m",
-               "-opv_name,vg_name,pv_free,pv_attr", "--unbuffered",
-               "--separator=%s" % sep ]
-    result = utils.RunCmd(command)
-    if result.failed:
-      logging.error("Can't get the PV information: %s - %s",
-                    result.fail_reason, result.output)
+    try:
+      info = cls._GetVolumeInfo("pvs", ["pv_name", "vg_name", "pv_free",
+                                        "pv_attr"])
+    except errors.GenericError, err:
+      logging.error("Can't get PV information: %s", err)
       return None
+
     data = []
-    for line in result.stdout.splitlines():
-      fields = line.strip().split(sep)
-      if len(fields) != 4:
-        logging.error("Can't parse pvs output: line '%s'", line)
-        return None
+    for pv_name, vg_name, pv_free, pv_attr in info:
       # (possibly) skip over pvs which are not allocatable
-      if filter_allocatable and fields[3][0] != 'a':
+      if filter_allocatable and pv_attr[0] != "a":
         continue
       # (possibly) skip over pvs which are not in the right volume group(s)
-      if vg_names and fields[1] not in vg_names:
+      if vg_names and vg_name not in vg_names:
         continue
-      data.append((float(fields[2]), fields[0], fields[1]))
+      data.append((float(pv_free), pv_name, vg_name))
+
+    return data
+
+  @classmethod
+  def GetVGInfo(cls, vg_names, filter_readonly=True):
+    """Get the free space info for specific VGs.
+
+    @param vg_names: list of volume group names, if empty all will be returned
+    @param filter_readonly: whether to skip over readonly VGs
+
+    @rtype: list
+    @return: list of tuples (free_space, total_size, name) with free_space in
+             MiB
+
+    """
+    try:
+      info = cls._GetVolumeInfo("vgs", ["vg_name", "vg_free", "vg_attr",
+                                        "vg_size"])
+    except errors.GenericError, err:
+      logging.error("Can't get VG information: %s", err)
+      return None
+
+    data = []
+    for vg_name, vg_free, vg_attr, vg_size in info:
+      # (possibly) skip over vgs which are not writable
+      if filter_readonly and vg_attr[0] == "r":
+        continue
+      # (possibly) skip over vgs which are not in the right volume group(s)
+      if vg_names and vg_name not in vg_names:
+        continue
+      data.append((float(vg_free), float(vg_size), vg_name))
 
     return data
 
@@ -643,12 +701,10 @@ class LogicalVolume(BlockDev):
     snap = LogicalVolume((self._vg_name, snap_name), None, size)
     _IgnoreError(snap.Remove)
 
-    pvs_info = self.GetPVInfo([self._vg_name])
-    if not pvs_info:
-      _ThrowError("Can't compute PV info for vg %s", self._vg_name)
-    pvs_info.sort()
-    pvs_info.reverse()
-    free_size, _, _ = pvs_info[0]
+    vg_info = self.GetVGInfo([self._vg_name])
+    if not vg_info:
+      _ThrowError("Can't compute VG info for vg %s", self._vg_name)
+    free_size, _, _ = vg_info[0]
     if free_size < size:
       _ThrowError("Not enough free space: required %s,"
                   " available %s", size, free_size)
@@ -1301,13 +1357,13 @@ class DRBD8(BaseDRBD):
     # about its peer.
     cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED)
 
-    if netutils.IsValidIP6(lhost):
-      if not netutils.IsValidIP6(rhost):
+    if netutils.IP6Address.IsValid(lhost):
+      if not netutils.IP6Address.IsValid(rhost):
         _ThrowError("drbd%d: can't connect ip %s to ip %s" %
                     (minor, lhost, rhost))
       family = "ipv6"
-    elif netutils.IsValidIP4(lhost):
-      if not netutils.IsValidIP4(rhost):
+    elif netutils.IP4Address.IsValid(lhost):
+      if not netutils.IP4Address.IsValid(rhost):
         _ThrowError("drbd%d: can't connect ip %s to ip %s" %
                     (minor, lhost, rhost))
       family = "ipv4"
index 5a2c5ba..24f9a0a 100644 (file)
@@ -41,6 +41,10 @@ from ganeti import serializer
 from ganeti import hypervisor
 from ganeti import bdev
 from ganeti import netutils
+from ganeti import backend
+
+# ec_id for InitConfig's temporary reservation manager
+_INITCONF_ECID = "initconfig-ecid"
 
 
 def _InitSSHSetup():
@@ -222,7 +226,8 @@ def InitCluster(cluster_name, mac_prefix,
                 nicparams=None, hvparams=None, enabled_hypervisors=None,
                 modify_etc_hosts=True, modify_ssh_setup=True,
                 maintain_node_health=False, drbd_helper=None,
-                uid_pool=None, default_iallocator=None):
+                uid_pool=None, default_iallocator=None,
+                primary_ip_version=None, prealloc_wipe_disks=False):
   """Initialise the cluster.
 
   @type candidate_pool_size: int
@@ -243,40 +248,56 @@ def InitCluster(cluster_name, mac_prefix,
                                " entries: %s" % invalid_hvs,
                                errors.ECODE_INVAL)
 
-  hostname = netutils.GetHostInfo()
 
-  if hostname.ip.startswith("127."):
-    raise errors.OpPrereqError("This host's IP resolves to the private"
-                               " range (%s). Please fix DNS or %s." %
+  ipcls = None
+  if primary_ip_version == constants.IP4_VERSION:
+    ipcls = netutils.IP4Address
+  elif primary_ip_version == constants.IP6_VERSION:
+    ipcls = netutils.IP6Address
+  else:
+    raise errors.OpPrereqError("Invalid primary ip version: %d." %
+                               primary_ip_version)
+
+  hostname = netutils.GetHostname(family=ipcls.family)
+  if not ipcls.IsValid(hostname.ip):
+    raise errors.OpPrereqError("This host's IP (%s) is not a valid IPv%d"
+                               " address." % (hostname.ip, primary_ip_version))
+
+  if ipcls.IsLoopback(hostname.ip):
+    raise errors.OpPrereqError("This host's IP (%s) resolves to a loopback"
+                               " address. Please fix DNS or %s." %
                                (hostname.ip, constants.ETC_HOSTS),
                                errors.ECODE_ENVIRON)
 
-  if not netutils.OwnIpAddress(hostname.ip):
+  if not ipcls.Own(hostname.ip):
     raise errors.OpPrereqError("Inconsistency: this host's name resolves"
                                " to %s,\nbut this ip address does not"
-                               " belong to this host. Aborting." %
+                               " belong to this host" %
                                hostname.ip, errors.ECODE_ENVIRON)
 
-  clustername = \
-    netutils.GetHostInfo(netutils.HostInfo.NormalizeName(cluster_name))
+  clustername = netutils.GetHostname(name=cluster_name, family=ipcls.family)
 
-  if netutils.TcpPing(clustername.ip, constants.DEFAULT_NODED_PORT,
-                   timeout=5):
-    raise errors.OpPrereqError("Cluster IP already active. Aborting.",
+  if netutils.TcpPing(clustername.ip, constants.DEFAULT_NODED_PORT, timeout=5):
+    raise errors.OpPrereqError("Cluster IP already active",
                                errors.ECODE_NOTUNIQUE)
 
-  if secondary_ip:
-    if not netutils.IsValidIP4(secondary_ip):
-      raise errors.OpPrereqError("Invalid secondary ip given",
+  if not secondary_ip:
+    if primary_ip_version == constants.IP6_VERSION:
+      raise errors.OpPrereqError("When using a IPv6 primary address, a valid"
+                                 " IPv4 address must be given as secondary",
                                  errors.ECODE_INVAL)
-    if (secondary_ip != hostname.ip and
-        not netutils.OwnIpAddress(secondary_ip)):
-      raise errors.OpPrereqError("You gave %s as secondary IP,"
-                                 " but it does not belong to this host." %
-                                 secondary_ip, errors.ECODE_ENVIRON)
-  else:
     secondary_ip = hostname.ip
 
+  if not netutils.IP4Address.IsValid(secondary_ip):
+    raise errors.OpPrereqError("Secondary IP address (%s) has to be a valid"
+                               " IPv4 address." % secondary_ip,
+                               errors.ECODE_INVAL)
+
+  if not netutils.IP4Address.Own(secondary_ip):
+    raise errors.OpPrereqError("You gave %s as secondary IP,"
+                               " but it does not belong to this host." %
+                               secondary_ip, errors.ECODE_ENVIRON)
+
   if vg_name is not None:
     # Check if volume group is valid
     vgstatus = utils.CheckVolumeGroupSize(utils.ListVolumeGroups(), vg_name,
@@ -325,15 +346,12 @@ def InitCluster(cluster_name, mac_prefix,
     hv_class = hypervisor.GetHypervisor(hv_name)
     hv_class.CheckParameterSyntax(hv_params)
 
-  # set up the inter-node password and certificate, start noded
-  _InitGanetiServerSetup(hostname.name)
-
   # set up ssh config and /etc/hosts
   sshline = utils.ReadFile(constants.SSH_HOST_RSA_PUB)
   sshkey = sshline.split(" ")[1]
 
   if modify_etc_hosts:
-    utils.AddHostToEtcHosts(hostname.name)
+    utils.AddHostToEtcHosts(hostname.name, hostname.ip)
 
   if modify_ssh_setup:
     _InitSSHSetup()
@@ -372,10 +390,11 @@ def InitCluster(cluster_name, mac_prefix,
     uid_pool=uid_pool,
     ctime=now,
     mtime=now,
-    uuid=utils.NewUUID(),
     maintain_node_health=maintain_node_health,
     drbd_usermode_helper=drbd_helper,
     default_iallocator=default_iallocator,
+    primary_ip_family=ipcls.family,
+    prealloc_wipe_disks=prealloc_wipe_disks,
     )
   master_node_config = objects.Node(name=hostname.name,
                                     primary_ip=hostname.ip,
@@ -385,9 +404,13 @@ def InitCluster(cluster_name, mac_prefix,
                                     offline=False, drained=False,
                                     )
   InitConfig(constants.CONFIG_VERSION, cluster_config, master_node_config)
-  cfg = config.ConfigWriter()
+  cfg = config.ConfigWriter(offline=True)
   ssh.WriteKnownHostsFile(cfg, constants.SSH_KNOWN_HOSTS_FILE)
   cfg.Update(cfg.GetClusterInfo(), logging.error)
+  backend.WriteSsconfFiles(cfg.GetSsconfValues())
+
+  # set up the inter-node password and certificate
+  _InitGanetiServerSetup(hostname.name)
 
   # start the master ip
   # TODO: Review rpc call from bootstrap
@@ -412,13 +435,26 @@ def InitConfig(version, cluster_config, master_node_config,
   @param cfg_file: configuration file path
 
   """
+  uuid_generator = config.TemporaryReservationManager()
+  cluster_config.uuid = uuid_generator.Generate([], utils.NewUUID,
+                                                _INITCONF_ECID)
+  master_node_config.uuid = uuid_generator.Generate([], utils.NewUUID,
+                                                    _INITCONF_ECID)
   nodes = {
     master_node_config.name: master_node_config,
     }
-
+  default_nodegroup = objects.NodeGroup(
+    uuid=uuid_generator.Generate([], utils.NewUUID, _INITCONF_ECID),
+    name="default",
+    members=[master_node_config.name],
+    )
+  nodegroups = {
+    default_nodegroup.uuid: default_nodegroup,
+    }
   now = time.time()
   config_data = objects.ConfigData(version=version,
                                    cluster=cluster_config,
+                                   nodegroups=nodegroups,
                                    nodes=nodes,
                                    instances={},
                                    serial_no=1,
@@ -460,7 +496,9 @@ def SetupNodeDaemon(cluster_name, node, ssh_key_check):
   @param ssh_key_check: whether to do a strict key check
 
   """
-  sshrunner = ssh.SshRunner(cluster_name)
+  family = ssconf.SimpleStore().GetPrimaryIPFamily()
+  sshrunner = ssh.SshRunner(cluster_name,
+                            ipv6=family==netutils.IP6Address.family)
 
   noded_cert = utils.ReadFile(constants.NODED_CERT_FILE)
   rapi_cert = utils.ReadFile(constants.RAPI_CERT_FILE)
@@ -482,30 +520,25 @@ def SetupNodeDaemon(cluster_name, node, ssh_key_check):
   if not confd_hmac_key.endswith("\n"):
     confd_hmac_key += "\n"
 
+  bind_address = constants.IP4_ADDRESS_ANY
+  if family == netutils.IP6Address.family:
+    bind_address = constants.IP6_ADDRESS_ANY
+
   # set up inter-node password and certificate and restarts the node daemon
   # and then connect with ssh to set password and start ganeti-noded
   # note that all the below variables are sanitized at this point,
   # either by being constants or by the checks above
-  # TODO: Could this command exceed a shell's maximum command length?
-  mycommand = ("umask 077 && "
-               "cat > '%s' << '!EOF.' && \n"
-               "%s!EOF.\n"
-               "cat > '%s' << '!EOF.' && \n"
-               "%s!EOF.\n"
-               "cat > '%s' << '!EOF.' && \n"
-               "%s!EOF.\n"
-               "chmod 0400 %s %s %s && "
-               "%s start %s" %
-               (constants.NODED_CERT_FILE, noded_cert,
-                constants.RAPI_CERT_FILE, rapi_cert,
-                constants.CONFD_HMAC_KEY, confd_hmac_key,
-                constants.NODED_CERT_FILE, constants.RAPI_CERT_FILE,
-                constants.CONFD_HMAC_KEY,
-                constants.DAEMON_UTIL, constants.NODED))
+  sshrunner.CopyFileToNode(node, constants.NODED_CERT_FILE)
+  sshrunner.CopyFileToNode(node, constants.RAPI_CERT_FILE)
+  sshrunner.CopyFileToNode(node, constants.CONFD_HMAC_KEY)
+  mycommand = ("%s stop-all; %s start %s -b '%s'" % (constants.DAEMON_UTIL,
+                                                     constants.DAEMON_UTIL,
+                                                     constants.NODED,
+                                                     bind_address))
 
   result = sshrunner.Run(node, 'root', mycommand, batch=False,
                          ask_key=ssh_key_check,
-                         use_cluster_key=False,
+                         use_cluster_key=True,
                          strict_host_check=ssh_key_check)
   if result.failed:
     raise errors.OpExecError("Remote command on node %s, error: %s,"
@@ -569,12 +602,36 @@ def MasterFailover(no_voting=False):
 
   logging.info("Setting master to %s, old master: %s", new_master, old_master)
 
+  try:
+    # instantiate a real config writer, as we now know we have the
+    # configuration data
+    cfg = config.ConfigWriter(accept_foreign=True)
+
+    cluster_info = cfg.GetClusterInfo()
+    cluster_info.master_node = new_master
+    # this will also regenerate the ssconf files, since we updated the
+    # cluster info
+    cfg.Update(cluster_info, logging.error)
+  except errors.ConfigurationError, err:
+    logging.error("Error while trying to set the new master: %s",
+                  str(err))
+    return 1
+
+  # if cfg.Update worked, then it means the old master daemon won't be
+  # able now to write its own config file (we rely on locking in both
+  # backend.UploadFile() and ConfigWriter._Write(); hence the next
+  # step is to kill the old master
+
+  logging.info("Stopping the master daemon on node %s", old_master)
+
   result = rpc.RpcRunner.call_node_stop_master(old_master, True)
   msg = result.fail_msg
   if msg:
     logging.error("Could not disable the master role on the old master"
                  " %s, please disable manually: %s", old_master, msg)
 
+  logging.info("Checking master IP non-reachability...")
+
   master_ip = sstore.GetMasterIP()
   total_timeout = 30
   # Here we have a phase where no master should be running
@@ -589,15 +646,7 @@ def MasterFailover(no_voting=False):
                     " continuing but activating the master on the current"
                     " node will probably fail", total_timeout)
 
-  # instantiate a real config writer, as we now know we have the
-  # configuration data
-  cfg = config.ConfigWriter()
-
-  cluster_info = cfg.GetClusterInfo()
-  cluster_info.master_node = new_master
-  # this will also regenerate the ssconf files, since we updated the
-  # cluster info
-  cfg.Update(cluster_info, logging.error)
+  logging.info("Starting the master daemons on the new master")
 
   result = rpc.RpcRunner.call_node_start_master(new_master, True, no_voting)
   msg = result.fail_msg
@@ -606,6 +655,7 @@ def MasterFailover(no_voting=False):
                   " %s, please check: %s", new_master, msg)
     rcode = 1
 
+  logging.info("Master failed over from %s to %s", old_master, new_master)
   return rcode
 
 
@@ -646,7 +696,7 @@ def GatherMasterVotes(node_list):
   @return: list of (node, votes)
 
   """
-  myself = netutils.HostInfo().name
+  myself = netutils.Hostname.GetSysName()
   try:
     node_list.remove(myself)
   except ValueError:
@@ -668,6 +718,7 @@ def GatherMasterVotes(node_list):
     if msg:
       logging.warning("Error contacting node %s: %s", node, msg)
       fail = True
+    # for now we accept both length 3 and 4 (data[3] is primary ip version)
     elif not isinstance(data, (tuple, list)) or len(data) < 3:
       logging.warning("Invalid data received from node %s: %s", node, data)
       fail = True
index a92f793..ad4e4e7 100644 (file)
@@ -53,6 +53,8 @@ __all__ = [
   "AUTO_REPLACE_OPT",
   "BACKEND_OPT",
   "BLK_OS_OPT",
+  "CAPAB_MASTER_OPT",
+  "CAPAB_VM_OPT",
   "CLEANUP_OPT",
   "CLUSTER_DOMAIN_SECRET_OPT",
   "CONFIRM_OPT",
@@ -83,6 +85,7 @@ __all__ = [
   "IDENTIFY_DEFAULTS_OPT",
   "IGNORE_CONSIST_OPT",
   "IGNORE_FAILURES_OPT",
+  "IGNORE_OFFLINE_OPT",
   "IGNORE_REMOVE_FAILURES_OPT",
   "IGNORE_SECONDARIES_OPT",
   "IGNORE_SIZE_OPT",
@@ -101,6 +104,7 @@ __all__ = [
   "NIC_PARAMS_OPT",
   "NODE_LIST_OPT",
   "NODE_PLACEMENT_OPT",
+  "NODEGROUP_OPT",
   "NODRBD_STORAGE_OPT",
   "NOHDR_OPT",
   "NOIPCHECK_OPT",
@@ -123,6 +127,9 @@ __all__ = [
   "OSPARAMS_OPT",
   "OS_OPT",
   "OS_SIZE_OPT",
+  "PREALLOC_WIPE_DISKS_OPT",
+  "PRIMARY_IP_VERSION_OPT",
+  "PRIORITY_OPT",
   "RAPI_CERT_OPT",
   "READD_OPT",
   "REBOOT_TYPE_OPT",
@@ -194,16 +201,30 @@ __all__ = [
   "OPT_COMPL_ONE_IALLOCATOR",
   "OPT_COMPL_ONE_INSTANCE",
   "OPT_COMPL_ONE_NODE",
+  "OPT_COMPL_ONE_NODEGROUP",
   "OPT_COMPL_ONE_OS",
   "cli_option",
   "SplitNodeOption",
   "CalculateOSNames",
   "ParseFields",
+  "COMMON_CREATE_OPTS",
   ]
 
 NO_PREFIX = "no_"
 UN_PREFIX = "-"
 
+#: Priorities (sorted)
+_PRIORITY_NAMES = [
+  ("low", constants.OP_PRIO_LOW),
+  ("normal", constants.OP_PRIO_NORMAL),
+  ("high", constants.OP_PRIO_HIGH),
+  ]
+
+#: Priority dictionary for easier lookup
+# TODO: Replace this and _PRIORITY_NAMES with a single sorted dictionary once
+# we migrate to Python 2.6
+_PRIONAME_TO_VALUE = dict(_PRIORITY_NAMES)
+
 
 class _Argument:
   def __init__(self, min=0, max=None): # pylint: disable-msg=W0622
@@ -503,7 +524,8 @@ def check_bool(option, opt, value): # pylint: disable-msg=W0613
  OPT_COMPL_ONE_INSTANCE,
  OPT_COMPL_ONE_OS,
  OPT_COMPL_ONE_IALLOCATOR,
- OPT_COMPL_INST_ADD_NODES) = range(100, 106)
+ OPT_COMPL_INST_ADD_NODES,
+ OPT_COMPL_ONE_NODEGROUP) = range(100, 107)
 
 OPT_COMPL_ALL = frozenset([
   OPT_COMPL_MANY_NODES,
@@ -512,6 +534,7 @@ OPT_COMPL_ALL = frozenset([
   OPT_COMPL_ONE_OS,
   OPT_COMPL_ONE_IALLOCATOR,
   OPT_COMPL_INST_ADD_NODES,
+  OPT_COMPL_ONE_NODEGROUP,
   ])
 
 
@@ -567,6 +590,11 @@ FORCE_OPT = cli_option("-f", "--force", dest="force", action="store_true",
 CONFIRM_OPT = cli_option("--yes", dest="confirm", action="store_true",
                          default=False, help="Do not require confirmation")
 
+IGNORE_OFFLINE_OPT = cli_option("--ignore-offline", dest="ignore_offline",
+                                  action="store_true", default=False,
+                                  help=("Ignore offline nodes and do as much"
+                                        " as possible"))
+
 TAG_SRC_OPT = cli_option("--from", dest="tags_source",
                          default=None, help="File with tag names")
 
@@ -722,6 +750,13 @@ NODE_LIST_OPT = cli_option("-n", "--node", dest="nodes", default=[],
                            " times, if not given defaults to all nodes)",
                            completion_suggest=OPT_COMPL_ONE_NODE)
 
+NODEGROUP_OPT = cli_option("-g", "--node-group",
+                           dest="nodegroup",
+                           help="Node group (name or uuid)",
+                           metavar="<nodegroup>",
+                           default=None, type="string",
+                           completion_suggest=OPT_COMPL_ONE_NODEGROUP)
+
 SINGLE_NODE_OPT = cli_option("-n", "--node", dest="node", help="Target node",
                              metavar="<node>",
                              completion_suggest=OPT_COMPL_ONE_NODE)
@@ -838,6 +873,14 @@ DRAINED_OPT = cli_option("-D", "--drained", dest="drained", metavar=_YORNO,
                          type="bool", default=None,
                          help="Set the drained flag on the node")
 
+CAPAB_MASTER_OPT = cli_option("--master-capable", dest="master_capable",
+                    type="bool", default=None, metavar=_YORNO,
+                    help="Set the master_capable flag on the node")
+
+CAPAB_VM_OPT = cli_option("--vm-capable", dest="vm_capable",
+                    type="bool", default=None, metavar=_YORNO,
+                    help="Set the vm_capable flag on the node")
+
 ALLOCATABLE_OPT = cli_option("--allocatable", dest="allocatable",
                              type="bool", default=None, metavar=_YORNO,
                              help="Set the allocatable flag on a volume")
@@ -860,7 +903,7 @@ CP_SIZE_OPT = cli_option("-C", "--candidate-pool-size", default=None,
                          dest="candidate_pool_size", type="int",
                          help="Set the candidate pool size")
 
-VG_NAME_OPT = cli_option("-g", "--vg-name", dest="vg_name",
+VG_NAME_OPT = cli_option("--vg-name", dest="vg_name",
                          help="Enables LVM and specifies the volume group"
                          " name (cluster-wide) for disk allocation [xenvg]",
                          metavar="VG", default=None)
@@ -1031,6 +1074,18 @@ 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",
+               metavar="%d|%d" % (constants.IP4_VERSION,
+                                  constants.IP6_VERSION),
+               help="Cluster-wide IP version for primary IP")
+
+PRIORITY_OPT = cli_option("--priority", default=None, dest="priority",
+                          metavar="|".join(name for name, _ in _PRIORITY_NAMES),
+                          choices=_PRIONAME_TO_VALUE.keys(),
+                          help="Priority for opcode processing")
+
 HID_OS_OPT = cli_option("--hidden", dest="hidden",
                         type="bool", default=None, metavar=_YORNO,
                         help="Sets the hidden flag on the OS")
@@ -1039,10 +1094,39 @@ BLK_OS_OPT = cli_option("--blacklisted", dest="blacklisted",
                         type="bool", default=None, metavar=_YORNO,
                         help="Sets the blacklisted flag on the OS")
 
+PREALLOC_WIPE_DISKS_OPT = cli_option("--prealloc-wipe-disks", default=None,
+                                     type="bool", metavar=_YORNO,
+                                     dest="prealloc_wipe_disks",
+                                     help=("Wipe disks prior to instance"
+                                           " creation"))
+
 
 #: Options provided by all commands
 COMMON_OPTS = [DEBUG_OPT]
 
+# common options for creating instances. add and import then add their own
+# specific ones.
+COMMON_CREATE_OPTS = [
+  BACKEND_OPT,
+  DISK_OPT,
+  DISK_TEMPLATE_OPT,
+  FILESTORE_DIR_OPT,
+  FILESTORE_DRIVER_OPT,
+  HYPERVISOR_OPT,
+  IALLOCATOR_OPT,
+  NET_OPT,
+  NODE_PLACEMENT_OPT,
+  NOIPCHECK_OPT,
+  NONAMECHECK_OPT,
+  NONICS_OPT,
+  NWSYNC_OPT,
+  OSPARAMS_OPT,
+  OS_SIZE_OPT,
+  SUBMIT_OPT,
+  DRY_RUN_OPT,
+  PRIORITY_OPT,
+  ]
+
 
 def _ParseArgs(argv, commands, aliases):
   """Parser for the command line arguments.
@@ -1631,9 +1715,11 @@ def SetGenericOpcodeOpts(opcode_list, options):
   if not options:
     return
   for op in opcode_list:
+    op.debug_level = options.debug
     if hasattr(options, "dry_run"):
       op.dry_run = options.dry_run
-    op.debug_level = options.debug
+    if getattr(options, "priority", None) is not None:
+      op.priority = _PRIONAME_TO_VALUE[options.priority]
 
 
 def GetClient():
@@ -1689,7 +1775,7 @@ def FormatError(err):
   elif isinstance(err, errors.HooksFailure):
     obuf.write("Failure: hooks general failure: %s" % msg)
   elif isinstance(err, errors.ResolverError):
-    this_host = netutils.HostInfo.SysName()
+    this_host = netutils.Hostname.GetSysName()
     if err.args[0] == this_host:
       msg = "Failure: can't resolve my own hostname ('%s')"
     else:
diff --git a/lib/client/__init__.py b/lib/client/__init__.py
new file mode 100644 (file)
index 0000000..308b5fe
--- /dev/null
@@ -0,0 +1,23 @@
+#
+#
+
+# Copyright (C) 2010 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.
+
+"""Common command line client code.
+
+"""
old mode 100755 (executable)
new mode 100644 (file)
similarity index 89%
rename from scripts/gnt-backup
rename to lib/client/gnt_backup.py
index 877933a..c154ae8
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#
 #
 
 # Copyright (C) 2006, 2007, 2010 Google Inc.
@@ -26,8 +26,6 @@
 # W0614: Unused import %s from wildcard import (since we need cli)
 # C0103: Invalid name gnt-backup
 
-import sys
-
 from ganeti.cli import *
 from ganeti import opcodes
 from ganeti import constants
@@ -117,26 +115,9 @@ def RemoveExport(opts, args):
 
 # this is defined separately due to readability only
 import_opts = [
-  BACKEND_OPT,
-  DISK_OPT,
-  DISK_TEMPLATE_OPT,
-  FILESTORE_DIR_OPT,
-  FILESTORE_DRIVER_OPT,
-  HYPERVISOR_OPT,
-  IALLOCATOR_OPT,
   IDENTIFY_DEFAULTS_OPT,
-  NET_OPT,
-  NODE_PLACEMENT_OPT,
-  NOIPCHECK_OPT,
-  NONAMECHECK_OPT,
-  NONICS_OPT,
-  NWSYNC_OPT,
-  OSPARAMS_OPT,
-  OS_SIZE_OPT,
   SRC_DIR_OPT,
   SRC_NODE_OPT,
-  SUBMIT_OPT,
-  DRY_RUN_OPT,
   ]
 
 
@@ -148,18 +129,19 @@ commands = {
   'export': (
     ExportInstance, ARGS_ONE_INSTANCE,
     [FORCE_OPT, SINGLE_NODE_OPT, NOSHUTDOWN_OPT, SHUTDOWN_TIMEOUT_OPT,
-     REMOVE_INSTANCE_OPT, IGNORE_REMOVE_FAILURES_OPT, DRY_RUN_OPT],
+     REMOVE_INSTANCE_OPT, IGNORE_REMOVE_FAILURES_OPT, DRY_RUN_OPT,
+     PRIORITY_OPT],
     "-n <target_node> [opts...] <name>",
     "Exports an instance to an image"),
   'import': (
-    ImportInstance, ARGS_ONE_INSTANCE, import_opts,
+    ImportInstance, ARGS_ONE_INSTANCE, COMMON_CREATE_OPTS + import_opts,
     "[...] -t disk-type -n node[:secondary-node] <name>",
     "Imports an instance from an exported image"),
   'remove': (
-    RemoveExport, [ArgUnknown(min=1, max=1)], [DRY_RUN_OPT],
+    RemoveExport, [ArgUnknown(min=1, max=1)], [DRY_RUN_OPT, PRIORITY_OPT],
     "<name>", "Remove exports of named instance from the filesystem."),
   }
 
 
-if __name__ == '__main__':
-  sys.exit(GenericMain(commands))
+def Main():
+  return GenericMain(commands)
old mode 100755 (executable)
new mode 100644 (file)
similarity index 95%
rename from scripts/gnt-cluster
rename to lib/client/gnt_cluster.py
index c65cc80..8714a21
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#
 #
 
 # Copyright (C) 2006, 2007, 2010 Google Inc.
@@ -26,7 +26,6 @@
 # W0614: Unused import %s from wildcard import (since we need cli)
 # C0103: Invalid name gnt-cluster
 
-import sys
 import os.path
 import time
 import OpenSSL
@@ -105,6 +104,15 @@ def InitCluster(opts, args):
   if uid_pool is not None:
     uid_pool = uidpool.ParseUidPool(uid_pool)
 
+  if opts.prealloc_wipe_disks is None:
+    opts.prealloc_wipe_disks = False
+
+  try:
+    primary_ip_version = int(opts.primary_ip_version)
+  except (ValueError, TypeError), err:
+    ToStderr("Invalid primary ip version value: %s" % str(err))
+    return 1
+
   bootstrap.InitCluster(cluster_name=args[0],
                         secondary_ip=opts.secondary_ip,
                         vg_name=vg_name,
@@ -122,6 +130,8 @@ def InitCluster(opts, args):
                         drbd_helper=drbd_helper,
                         uid_pool=uid_pool,
                         default_iallocator=opts.default_iallocator,
+                        primary_ip_version=primary_ip_version,
+                        prealloc_wipe_disks=opts.prealloc_wipe_disks,
                         )
   op = opcodes.OpPostInitCluster()
   SubmitOpCode(op, opts=opts)
@@ -318,6 +328,8 @@ def ShowClusterConfig(opts, args):
             uidpool.FormatUidPool(result["uid_pool"],
                                   roman=opts.roman_integers))
   ToStdout("  - default instance allocator: %s", result["default_iallocator"])
+  ToStdout("  - primary ip version: %d", result["primary_ip_version"])
+  ToStdout("  - preallocation wipe disks: %s", result["prealloc_wipe_disks"])
 
   ToStdout("Default instance parameters:")
   _PrintGroupedParams(result["beparams"], roman=opts.roman_integers)
@@ -701,7 +713,8 @@ def SetClusterParams(opts, args):
           opts.add_uids is not None or
           opts.remove_uids is not None or
           opts.default_iallocator is not None or
-          opts.reserved_lvs is not None):
+          opts.reserved_lvs is not None or
+          opts.prealloc_wipe_disks is not None):
     ToStderr("Please give at least one of the parameters.")
     return 1
 
@@ -770,6 +783,7 @@ def SetClusterParams(opts, args):
                                   add_uids=add_uids,
                                   remove_uids=remove_uids,
                                   default_iallocator=opts.default_iallocator,
+                                  prealloc_wipe_disks=opts.prealloc_wipe_disks,
                                   reserved_lvs=opts.reserved_lvs)
   SubmitOpCode(op, opts=opts)
   return 0
@@ -854,7 +868,7 @@ commands = {
      NOLVM_STORAGE_OPT, NOMODIFY_ETCHOSTS_OPT, NOMODIFY_SSH_SETUP_OPT,
      SECONDARY_IP_OPT, VG_NAME_OPT, MAINTAIN_NODE_HEALTH_OPT,
      UIDPOOL_OPT, DRBD_HELPER_OPT, NODRBD_STORAGE_OPT,
-     DEFAULT_IALLOCATOR_OPT],
+     DEFAULT_IALLOCATOR_OPT, PRIMARY_IP_VERSION_OPT, PREALLOC_WIPE_DISKS_OPT],
     "[opts...] <cluster_name>", "Initialises a new cluster configuration"),
   'destroy': (
     DestroyCluster, ARGS_NONE, [YES_DOIT_OPT],
@@ -865,19 +879,19 @@ commands = {
     "<new_name>",
     "Renames the cluster"),
   'redist-conf': (
-    RedistributeConfig, ARGS_NONE, [SUBMIT_OPT, DRY_RUN_OPT],
+    RedistributeConfig, ARGS_NONE, [SUBMIT_OPT, DRY_RUN_OPT, PRIORITY_OPT],
     "", "Forces a push of the configuration file and ssconf files"
     " to the nodes in the cluster"),
   'verify': (
     VerifyCluster, ARGS_NONE,
     [VERBOSE_OPT, DEBUG_SIMERR_OPT, ERROR_CODES_OPT, NONPLUS1_OPT,
-     DRY_RUN_OPT],
+     DRY_RUN_OPT, PRIORITY_OPT],
     "", "Does a check on the cluster configuration"),
   'verify-disks': (
-    VerifyDisks, ARGS_NONE, [],
+    VerifyDisks, ARGS_NONE, [PRIORITY_OPT],
     "", "Does a check on the cluster disk status"),
   'repair-disk-sizes': (
-    RepairDiskSizes, ARGS_MANY_INSTANCES, [DRY_RUN_OPT],
+    RepairDiskSizes, ARGS_MANY_INSTANCES, [DRY_RUN_OPT, PRIORITY_OPT],
     "", "Updates mismatches in recorded disk sizes"),
   'master-failover': (
     MasterFailover, ARGS_NONE, [NOVOTING_OPT],
@@ -905,14 +919,14 @@ commands = {
   'list-tags': (
     ListTags, ARGS_NONE, [], "", "List the tags of the cluster"),
   'add-tags': (
-    AddTags, [ArgUnknown()], [TAG_SRC_OPT],
+    AddTags, [ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
     "tag...", "Add tags to the cluster"),
   'remove-tags': (
-    RemoveTags, [ArgUnknown()], [TAG_SRC_OPT],
+    RemoveTags, [ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
     "tag...", "Remove tags from the cluster"),
   'search-tags': (
-    SearchTags, [ArgUnknown(min=1, max=1)],
-    [], "", "Searches the tags on all objects on"
+    SearchTags, [ArgUnknown(min=1, max=1)], [PRIORITY_OPT], "",
+    "Searches the tags on all objects on"
     " the cluster for a given pattern (regex)"),
   'queue': (
     QueueOps,
@@ -930,7 +944,7 @@ commands = {
      NIC_PARAMS_OPT, NOLVM_STORAGE_OPT, VG_NAME_OPT, MAINTAIN_NODE_HEALTH_OPT,
      UIDPOOL_OPT, ADD_UIDS_OPT, REMOVE_UIDS_OPT, DRBD_HELPER_OPT,
      NODRBD_STORAGE_OPT, DEFAULT_IALLOCATOR_OPT, RESERVED_LVS_OPT,
-     DRY_RUN_OPT],
+     DRY_RUN_OPT, PRIORITY_OPT, PREALLOC_WIPE_DISKS_OPT],
     "[opts...]",
     "Alters the parameters of the cluster"),
   "renew-crypto": (
@@ -949,6 +963,6 @@ aliases = {
 }
 
 
-if __name__ == '__main__':
-  sys.exit(GenericMain(commands, override={"tag_type": constants.TAG_CLUSTER},
-                       aliases=aliases))
+def Main():
+  return GenericMain(commands, override={"tag_type": constants.TAG_CLUSTER},
+                     aliases=aliases)
old mode 100755 (executable)
new mode 100644 (file)
similarity index 88%
rename from scripts/gnt-debug
rename to lib/client/gnt_debug.py
index e54a34a..2cd537c
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#
 #
 
 # Copyright (C) 2006, 2007, 2010 Google Inc.
@@ -25,7 +25,6 @@
 # W0614: Unused import %s from wildcard import (since we need cli)
 # C0103: Invalid name gnt-backup
 
-import sys
 import simplejson
 import time
 import socket
@@ -37,6 +36,7 @@ from ganeti import constants
 from ganeti import opcodes
 from ganeti import utils
 from ganeti import errors
+from ganeti import compat
 
 
 #: Default fields for L{ListLocks}
@@ -167,6 +167,64 @@ def TestAllocator(opts, args):
   return 0
 
 
+def _TestJobSubmission(opts):
+  """Tests submitting jobs.
+
+  """
+  ToStdout("Testing job submission")
+
+  testdata = [
+    (0, 0, constants.OP_PRIO_LOWEST),
+    (0, 0, constants.OP_PRIO_HIGHEST),
+    ]
+
+  for priority in (constants.OP_PRIO_SUBMIT_VALID |
+                   frozenset([constants.OP_PRIO_LOWEST,
+                              constants.OP_PRIO_HIGHEST])):
+    for offset in [-1, +1]:
+      testdata.extend([
+        (0, 0, priority + offset),
+        (3, 0, priority + offset),
+        (0, 3, priority + offset),
+        (4, 2, priority + offset),
+        ])
+
+  cl = cli.GetClient()
+
+  for before, after, failpriority in testdata:
+    ops = []
+    ops.extend([opcodes.OpTestDelay(duration=0) for _ in range(before)])
+    ops.append(opcodes.OpTestDelay(duration=0, priority=failpriority))
+    ops.extend([opcodes.OpTestDelay(duration=0) for _ in range(after)])
+
+    try:
+      cl.SubmitJob(ops)
+    except errors.GenericError, err:
+      if opts.debug:
+        ToStdout("Ignoring error: %s", err)
+    else:
+      raise errors.OpExecError("Submitting opcode with priority %s did not"
+                               " fail when it should (allowed are %s)" %
+                               (failpriority, constants.OP_PRIO_SUBMIT_VALID))
+
+    jobs = [
+      [opcodes.OpTestDelay(duration=0),
+       opcodes.OpTestDelay(duration=0, dry_run=False),
+       opcodes.OpTestDelay(duration=0, dry_run=True)],
+      ops,
+      ]
+    result = cl.SubmitManyJobs(jobs)
+    if not (len(result) == 2 and
+            compat.all(len(i) == 2 for i in result) and
+            compat.all(isinstance(i[1], basestring) for i in result) and
+            result[0][0] and not result[1][0]):
+      raise errors.OpExecError("Submitting multiple jobs did not work as"
+                               " expected, result %s" % result)
+    assert len(result) == 2
+
+  ToStdout("Job submission tests were successful")
+
+
 class _JobQueueTestReporter(cli.StdioJobPollReportCb):
   def __init__(self):
     """Initializes this class.
@@ -268,6 +326,8 @@ def TestJobqueue(opts, _):
   """Runs a few tests on the job queue.
 
   """
+  _TestJobSubmission(opts)
+
   (TM_SUCCESS,
    TM_MULTISUCCESS,
    TM_FAIL,
@@ -471,7 +531,7 @@ commands = {
                 action="append", help="Select nodes to sleep on"),
      cli_option("-r", "--repeat", type="int", default="0", dest="repeat",
                 help="Number of times to repeat the sleep"),
-     DRY_RUN_OPT,
+     DRY_RUN_OPT, PRIORITY_OPT,
      ],
     "[opts...] <duration>", "Executes a TestDelay OpCode"),
   'submit-job': (
@@ -485,7 +545,7 @@ commands = {
                 action="store_true", help="Show timing stats"),
      cli_option("--each", default=False, action="store_true",
                 help="Submit each job separately"),
-     DRY_RUN_OPT,
+     DRY_RUN_OPT, PRIORITY_OPT,
      ],
     "<op_list_file...>", "Submits jobs built from json files"
     " containing a list of serialized opcodes"),
@@ -513,11 +573,11 @@ commands = {
                 help="Select number of VCPUs for the instance"),
      cli_option("--tags", default=None,
                 help="Comma separated list of tags"),
-     DRY_RUN_OPT,
+     DRY_RUN_OPT, PRIORITY_OPT,
      ],
     "{opts...} <instance>", "Executes a TestAllocator OpCode"),
   "test-jobqueue": (
-    TestJobqueue, ARGS_NONE, [],
+    TestJobqueue, ARGS_NONE, [PRIORITY_OPT],
     "", "Test a few aspects of the job queue"),
   "locks": (
     ListLocks, ARGS_NONE, [NOHDR_OPT, SEP_OPT, FIELDS_OPT, INTERVAL_OPT],
@@ -525,5 +585,5 @@ commands = {
   }
 
 
-if __name__ == '__main__':
-  sys.exit(GenericMain(commands))
+def Main():
+  return GenericMain(commands)
old mode 100755 (executable)
new mode 100644 (file)
similarity index 96%
rename from scripts/gnt-instance
rename to lib/client/gnt_instance.py
index d921ca0..431bc17
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#
 #
 
 # Copyright (C) 2006, 2007, 2008, 2009, 2010 Google Inc.
@@ -25,7 +25,6 @@
 # W0614: Unused import %s from wildcard import (since we need cli)
 # C0103: Invalid name gnt-instance
 
-import sys
 import os
 import itertools
 import simplejson
@@ -572,7 +571,8 @@ def ReinstallInstance(opts, args):
   for instance_name in inames:
     op = opcodes.OpReinstallInstance(instance_name=instance_name,
                                      os_type=os_name,
-                                     force_variant=opts.force_variant)
+                                     force_variant=opts.force_variant,
+                                     osparams=opts.osparams)
     jex.QueueJob(instance_name, op)
 
   jex.WaitOrShow(not opts.submit_only)
@@ -744,7 +744,8 @@ def _StartupInstance(name, opts):
 
   """
   op = opcodes.OpStartupInstance(instance_name=name,
-                                 force=opts.force)
+                                 force=opts.force,
+                                 ignore_offline_nodes=opts.ignore_offline)
   # do not add these parameters to the opcode unless they're defined
   if opts.hvparams:
     op.hvparams = opts.hvparams
@@ -782,7 +783,8 @@ def _ShutdownInstance(name, opts):
 
   """
   return opcodes.OpShutdownInstance(instance_name=name,
-                                    timeout=opts.timeout)
+                                    timeout=opts.timeout,
+                                    ignore_offline_nodes=opts.ignore_offline)
 
 
 def ReplaceDisks(opts, args):
@@ -1212,7 +1214,7 @@ def ShowInstanceConfig(opts, args):
         vnc_console_port = "%s:%s (display %s)" % (instance["pnode"],
                                                    port,
                                                    display)
-      elif display > 0 and netutils.IsValidIP4(vnc_bind_address):
+      elif display > 0 and netutils.IP4Address.IsValid(vnc_bind_address):
         vnc_console_port = ("%s:%s (node %s) (display %s)" %
                              (vnc_bind_address, port,
                               instance["pnode"], display))
@@ -1375,62 +1377,46 @@ m_inst_tags_opt = cli_option("--tags", dest="multi_mode",
 
 # this is defined separately due to readability only
 add_opts = [
-  BACKEND_OPT,
-  DISK_OPT,
-  DISK_TEMPLATE_OPT,
-  FILESTORE_DIR_OPT,
-  FILESTORE_DRIVER_OPT,
-  HYPERVISOR_OPT,
-  IALLOCATOR_OPT,
-  NET_OPT,
-  NODE_PLACEMENT_OPT,
-  NOIPCHECK_OPT,
-  NONAMECHECK_OPT,
-  NONICS_OPT,
   NOSTART_OPT,
-  NWSYNC_OPT,
-  OSPARAMS_OPT,
   OS_OPT,
   FORCE_VARIANT_OPT,
   NO_INSTALL_OPT,
-  OS_SIZE_OPT,
-  SUBMIT_OPT,
-  DRY_RUN_OPT,
   ]
 
 commands = {
   'add': (
-    AddInstance, [ArgHost(min=1, max=1)], add_opts,
+    AddInstance, [ArgHost(min=1, max=1)], COMMON_CREATE_OPTS + add_opts,
     "[...] -t disk-type -n node[:secondary-node] -o os-type <name>",
     "Creates and adds a new instance to the cluster"),
   'batch-create': (
-    BatchCreate, [ArgFile(min=1, max=1)], [DRY_RUN_OPT],
+    BatchCreate, [ArgFile(min=1, max=1)], [DRY_RUN_OPT, PRIORITY_OPT],
     "<instances.json>",
     "Create a bunch of instances based on specs in the file."),
   'console': (
     ConnectToInstanceConsole, ARGS_ONE_INSTANCE,
-    [SHOWCMD_OPT],
+    [SHOWCMD_OPT, PRIORITY_OPT],
     "[--show-cmd] <instance>", "Opens a console on the specified instance"),
   'failover': (
     FailoverInstance, ARGS_ONE_INSTANCE,
     [FORCE_OPT, IGNORE_CONSIST_OPT, SUBMIT_OPT, SHUTDOWN_TIMEOUT_OPT,
-     DRY_RUN_OPT],
+     DRY_RUN_OPT, PRIORITY_OPT],
     "[-f] <instance>", "Stops the instance and starts it on the backup node,"
     " using the remote mirror (only for instances of type drbd)"),
   'migrate': (
     MigrateInstance, ARGS_ONE_INSTANCE,
-    [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, CLEANUP_OPT, DRY_RUN_OPT],
+    [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, CLEANUP_OPT, DRY_RUN_OPT,
+     PRIORITY_OPT],
     "[-f] <instance>", "Migrate instance to its secondary node"
     " (only for instances of type drbd)"),
   'move': (
     MoveInstance, ARGS_ONE_INSTANCE,
     [FORCE_OPT, SUBMIT_OPT, SINGLE_NODE_OPT, SHUTDOWN_TIMEOUT_OPT,
-     DRY_RUN_OPT],
+     DRY_RUN_OPT, PRIORITY_OPT],
     "[-f] <instance>", "Move instance to an arbitrary node"
     " (only for instances of type file and lv)"),
   'info': (
     ShowInstanceConfig, ARGS_MANY_INSTANCES,
-    [STATIC_OPT, ALL_OPT, ROMAN_OPT],
+    [STATIC_OPT, ALL_OPT, ROMAN_OPT, PRIORITY_OPT],
     "[-s] {--all | <instance>...}",
     "Show information on the specified instance(s)"),
   'list': (
@@ -1455,78 +1441,80 @@ commands = {
     [FORCE_OPT, OS_OPT, FORCE_VARIANT_OPT, m_force_multi, m_node_opt,
      m_pri_node_opt, m_sec_node_opt, m_clust_opt, m_inst_opt, m_node_tags_opt,
      m_pri_node_tags_opt, m_sec_node_tags_opt, m_inst_tags_opt, SELECT_OS_OPT,
-     SUBMIT_OPT, DRY_RUN_OPT],
+     SUBMIT_OPT, DRY_RUN_OPT, PRIORITY_OPT, OSPARAMS_OPT],
     "[-f] <instance>", "Reinstall a stopped instance"),
   'remove': (
     RemoveInstance, ARGS_ONE_INSTANCE,
     [FORCE_OPT, SHUTDOWN_TIMEOUT_OPT, IGNORE_FAILURES_OPT, SUBMIT_OPT,
-     DRY_RUN_OPT],
+     DRY_RUN_OPT, PRIORITY_OPT],
     "[-f] <instance>", "Shuts down the instance and removes it"),
   'rename': (
     RenameInstance,
     [ArgInstance(min=1, max=1), ArgHost(min=1, max=1)],
-    [NOIPCHECK_OPT, NONAMECHECK_OPT, SUBMIT_OPT, DRY_RUN_OPT],
+    [NOIPCHECK_OPT, NONAMECHECK_OPT, SUBMIT_OPT, DRY_RUN_OPT, PRIORITY_OPT],
     "<instance> <new_name>", "Rename the instance"),
   'replace-disks': (
     ReplaceDisks, ARGS_ONE_INSTANCE,
     [AUTO_REPLACE_OPT, DISKIDX_OPT, IALLOCATOR_OPT, EARLY_RELEASE_OPT,
      NEW_SECONDARY_OPT, ON_PRIMARY_OPT, ON_SECONDARY_OPT, SUBMIT_OPT,
-     DRY_RUN_OPT],
+     DRY_RUN_OPT, PRIORITY_OPT],
     "[-s|-p|-n NODE|-I NAME] <instance>",
     "Replaces all disks for the instance"),
   'modify': (
     SetInstanceParams, ARGS_ONE_INSTANCE,
     [BACKEND_OPT, DISK_OPT, FORCE_OPT, HVOPTS_OPT, NET_OPT, SUBMIT_OPT,
      DISK_TEMPLATE_OPT, SINGLE_NODE_OPT, OS_OPT, FORCE_VARIANT_OPT,
-     OSPARAMS_OPT, DRY_RUN_OPT],
+     OSPARAMS_OPT, DRY_RUN_OPT, PRIORITY_OPT],
     "<instance>", "Alters the parameters of an instance"),
   'shutdown': (
     GenericManyOps("shutdown", _ShutdownInstance), [ArgInstance()],
     [m_node_opt, m_pri_node_opt, m_sec_node_opt, m_clust_opt,
      m_node_tags_opt, m_pri_node_tags_opt, m_sec_node_tags_opt,
      m_inst_tags_opt, m_inst_opt, m_force_multi, TIMEOUT_OPT, SUBMIT_OPT,
-     DRY_RUN_OPT],
+     DRY_RUN_OPT, PRIORITY_OPT, IGNORE_OFFLINE_OPT],
     "<instance>", "Stops an instance"),
   'startup': (
     GenericManyOps("startup", _StartupInstance), [ArgInstance()],
     [FORCE_OPT, m_force_multi, m_node_opt, m_pri_node_opt, m_sec_node_opt,
      m_node_tags_opt, m_pri_node_tags_opt, m_sec_node_tags_opt,
      m_inst_tags_opt, m_clust_opt, m_inst_opt, SUBMIT_OPT, HVOPTS_OPT,
-     BACKEND_OPT, DRY_RUN_OPT],
+     BACKEND_OPT, DRY_RUN_OPT, PRIORITY_OPT, IGNORE_OFFLINE_OPT],
     "<instance>", "Starts an instance"),
   'reboot': (
     GenericManyOps("reboot", _RebootInstance), [ArgInstance()],
     [m_force_multi, REBOOT_TYPE_OPT, IGNORE_SECONDARIES_OPT, m_node_opt,
      m_pri_node_opt, m_sec_node_opt, m_clust_opt, m_inst_opt, SUBMIT_OPT,
      m_node_tags_opt, m_pri_node_tags_opt, m_sec_node_tags_opt,
-     m_inst_tags_opt, SHUTDOWN_TIMEOUT_OPT, DRY_RUN_OPT],
+     m_inst_tags_opt, SHUTDOWN_TIMEOUT_OPT, DRY_RUN_OPT, PRIORITY_OPT],
     "<instance>", "Reboots an instance"),
   'activate-disks': (
     ActivateDisks, ARGS_ONE_INSTANCE,
-    [SUBMIT_OPT, IGNORE_SIZE_OPT],
+    [SUBMIT_OPT, IGNORE_SIZE_OPT, PRIORITY_OPT],
     "<instance>", "Activate an instance's disks"),
   'deactivate-disks': (
-    DeactivateDisks, ARGS_ONE_INSTANCE, [SUBMIT_OPT, DRY_RUN_OPT],
+    DeactivateDisks, ARGS_ONE_INSTANCE,
+    [SUBMIT_OPT, DRY_RUN_OPT, PRIORITY_OPT],
     "<instance>", "Deactivate an instance's disks"),
   'recreate-disks': (
-    RecreateDisks, ARGS_ONE_INSTANCE, [SUBMIT_OPT, DISKIDX_OPT, DRY_RUN_OPT],
+    RecreateDisks, ARGS_ONE_INSTANCE,
+    [SUBMIT_OPT, DISKIDX_OPT, DRY_RUN_OPT, PRIORITY_OPT],
     "<instance>", "Recreate an instance's disks"),
   'grow-disk': (
     GrowDisk,
     [ArgInstance(min=1, max=1), ArgUnknown(min=1, max=1),
      ArgUnknown(min=1, max=1)],
-    [SUBMIT_OPT, NWSYNC_OPT, DRY_RUN_OPT],
+    [SUBMIT_OPT, NWSYNC_OPT, DRY_RUN_OPT, PRIORITY_OPT],
     "<instance> <disk> <size>", "Grow an instance's disk"),
   'list-tags': (
-    ListTags, ARGS_ONE_INSTANCE, [],
+    ListTags, ARGS_ONE_INSTANCE, [PRIORITY_OPT],
     "<instance_name>", "List the tags of the given instance"),
   'add-tags': (
     AddTags, [ArgInstance(min=1, max=1), ArgUnknown()],
-    [TAG_SRC_OPT],
+    [TAG_SRC_OPT, PRIORITY_OPT],
     "<instance_name> tag...", "Add tags to the given instance"),
   'remove-tags': (
     RemoveTags, [ArgInstance(min=1, max=1), ArgUnknown()],
-    [TAG_SRC_OPT],
+    [TAG_SRC_OPT, PRIORITY_OPT],
     "<instance_name> tag...", "Remove tags from given instance"),
   }
 
@@ -1537,6 +1525,6 @@ aliases = {
   }
 
 
-if __name__ == '__main__':
-  sys.exit(GenericMain(commands, aliases=aliases,
-                       override={"tag_type": constants.TAG_INSTANCE}))
+def Main():
+  return GenericMain(commands, aliases=aliases,
+                     override={"tag_type": constants.TAG_INSTANCE})
old mode 100755 (executable)
new mode 100644 (file)
similarity index 96%
rename from scripts/gnt-job
rename to lib/client/gnt_job.py
index 80dbfd8..9951928
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#
 #
 
 # Copyright (C) 2006, 2007 Google Inc.
@@ -26,8 +26,6 @@
 # W0614: Unused import %s from wildcard import (since we need cli)
 # C0103: Invalid name gnt-job
 
-import sys
-
 from ganeti.cli import *
 from ganeti import constants
 from ganeti import errors
@@ -69,6 +67,7 @@ def ListJobs(opts, args):
     headers = {
       "id": "ID",
       "status": "Status",
+      "priority": "Prio",
       "ops": "OpCodes",
       "opresult": "OpCode_result",
       "opstatus": "OpCode_status",
@@ -77,6 +76,7 @@ def ListJobs(opts, args):
       "opstart": "OpCode_start",
       "opexec": "OpCode_exec",
       "opend": "OpCode_end",
+      "oppriority": "OpCode_prio",
       "start_ts": "Start",
       "end_ts": "End",
       "received_ts": "Received",
@@ -84,6 +84,8 @@ def ListJobs(opts, args):
   else:
     headers = None
 
+  numfields = ["priority"]
+
   # change raw values to nicer strings
   for row_id, row in enumerate(output):
     if row is None:
@@ -107,7 +109,8 @@ def ListJobs(opts, args):
       row[idx] = str(val)
 
   data = GenerateTable(separator=opts.separator, headers=headers,
-                       fields=selected_fields, data=output)
+                       fields=selected_fields, data=output,
+                       numfields=numfields)
   for line in data:
     ToStdout(line)
 
@@ -176,13 +179,17 @@ def CancelJobs(opts, args):
 
   """
   client = GetClient()
+  result = constants.EXIT_SUCCESS
 
   for job_id in args:
-    (_, msg) = client.CancelJob(job_id)
+    (success, msg) = client.CancelJob(job_id)
+
+    if not success:
+      result = constants.EXIT_FAILURE
+
     ToStdout(msg)
 
-  # TODO: Different exit value if not all jobs were canceled?
-  return 0
+  return result
 
 
 def ShowJobs(opts, args):
@@ -379,5 +386,5 @@ commands = {
   }
 
 
-if __name__ == '__main__':
-  sys.exit(GenericMain(commands))
+def Main():
+  return GenericMain(commands)
old mode 100755 (executable)
new mode 100644 (file)
similarity index 85%
rename from scripts/gnt-node
rename to lib/client/gnt_node.py
index c17e2c6..71d48ba
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#
 #
 
 # Copyright (C) 2006, 2007, 2008, 2009, 2010 Google Inc.
 # W0614: Unused import %s from wildcard import (since we need cli)
 # C0103: Invalid name gnt-node
 
-import sys
-
 from ganeti.cli import *
+from ganeti import bootstrap
 from ganeti import opcodes
 from ganeti import utils
 from ganeti import constants
 from ganeti import compat
 from ganeti import errors
-from ganeti import bootstrap
 from ganeti import netutils
 
 
@@ -77,7 +75,8 @@ _LIST_HEADERS = {
   "master": "IsMaster",
   "offline": "Offline", "drained": "Drained",
   "role": "Role",
-  "ctime": "CTime", "mtime": "MTime", "uuid": "UUID"
+  "ctime": "CTime", "mtime": "MTime", "uuid": "UUID",
+  "master_capable": "MasterCapable", "vm_capable": "VMCapable",
   }
 
 
@@ -116,6 +115,12 @@ _REPAIRABLE_STORAGE_TYPES = \
 _MODIFIABLE_STORAGE_TYPES = constants.MODIFIABLE_STORAGE_FIELDS.keys()
 
 
+NONODE_SETUP_OPT = cli_option("--no-node-setup", default=True,
+                              action="store_false", dest="node_setup",
+                              help=("Do not make initial SSH setup on remote"
+                                    " node (needs to be done manually)"))
+
+
 def ConvertStorageType(user_storage_type):
   """Converts a user storage type to its internal name.
 
@@ -127,6 +132,34 @@ def ConvertStorageType(user_storage_type):
                                errors.ECODE_INVAL)
 
 
+def _RunSetupSSH(options, nodes):
+  """Wrapper around utils.RunCmd to call setup-ssh
+
+  @param options: The command line options
+  @param nodes: The nodes to setup
+
+  """
+  cmd = [constants.SETUP_SSH]
+
+  # Pass --debug|--verbose to the external script if set on our invocation
+  # --debug overrides --verbose
+  if options.debug:
+    cmd.append("--debug")
+  elif options.verbose:
+    cmd.append("--verbose")
+  if not options.ssh_key_check:
+    cmd.append("--no-ssh-key-check")
+
+  cmd.extend(nodes)
+
+  result = utils.RunCmd(cmd, interactive=True)
+
+  if result.failed:
+    errmsg = ("Command '%s' failed with exit code %s; output %r" %
+              (result.cmd, result.exit_code, result.output))
+    raise errors.OpExecError(errmsg)
+
+
 @UsesRPC
 def AddNode(opts, args):
   """Add a node to the cluster.
@@ -139,8 +172,7 @@ def AddNode(opts, args):
 
   """
   cl = GetClient()
-  dns_data = netutils.GetHostInfo(netutils.HostInfo.NormalizeName(args[0]))
-  node = dns_data.name
+  node = netutils.GetHostname(name=args[0]).name
   readd = opts.readd
 
   try:
@@ -167,7 +199,7 @@ def AddNode(opts, args):
   output = cl.QueryConfigValues(['cluster_name'])
   cluster_name = output[0]
 
-  if not readd:
+  if not readd and opts.node_setup:
     ToStderr("-- WARNING -- \n"
              "Performing this operation is going to replace the ssh daemon"
              " keypair\n"
@@ -175,10 +207,15 @@ def AddNode(opts, args):
              " current one\n"
              "and grant full intra-cluster ssh root access to/from it\n", node)
 
+  if opts.node_setup:
+    _RunSetupSSH(opts, [node])
+
   bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
 
   op = opcodes.OpAddNode(node_name=args[0], secondary_ip=sip,
-                         readd=opts.readd)
+                         readd=opts.readd, group=opts.nodegroup,
+                         vm_capable=opts.vm_capable,
+                         master_capable=opts.master_capable)
   SubmitOpCode(op, opts=opts)
 
 
@@ -215,7 +252,8 @@ def ListNodes(opts, args):
       val = row[idx]
       if field in list_type_fields:
         val = ",".join(val)
-      elif field in ('master', 'master_candidate', 'offline', 'drained'):
+      elif field in ('master', 'master_candidate', 'offline', 'drained',
+                     'master_capable', 'vm_capable'):
         if val:
           val = 'Y'
         else:
@@ -387,29 +425,33 @@ def ShowNodeConfig(opts, args):
   cl = GetClient()
   result = cl.QueryNodes(fields=["name", "pip", "sip",
                                  "pinst_list", "sinst_list",
-                                 "master_candidate", "drained", "offline"],
+                                 "master_candidate", "drained", "offline",
+                                 "master_capable", "vm_capable"],
                          names=args, use_locking=False)
 
   for (name, primary_ip, secondary_ip, pinst, sinst,
-       is_mc, drained, offline) in result:
+       is_mc, drained, offline, master_capable, vm_capable) in result:
     ToStdout("Node name: %s", name)
     ToStdout("  primary ip: %s", primary_ip)
     ToStdout("  secondary ip: %s", secondary_ip)
     ToStdout("  master candidate: %s", is_mc)
     ToStdout("  drained: %s", drained)
     ToStdout("  offline: %s", offline)
-    if pinst:
-      ToStdout("  primary for instances:")
-      for iname in utils.NiceSort(pinst):
-        ToStdout("    - %s", iname)
-    else:
-      ToStdout("  primary for no instances")
-    if sinst:
-      ToStdout("  secondary for instances:")
-      for iname in utils.NiceSort(sinst):
-        ToStdout("    - %s", iname)
-    else:
-      ToStdout("  secondary for no instances")
+    ToStdout("  master_capable: %s", master_capable)
+    ToStdout("  vm_capable: %s", vm_capable)
+    if vm_capable:
+      if pinst:
+        ToStdout("  primary for instances:")
+        for iname in utils.NiceSort(pinst):
+          ToStdout("    - %s", iname)
+      else:
+        ToStdout("  primary for no instances")
+      if sinst:
+        ToStdout("  secondary for instances:")
+        for iname in utils.NiceSort(sinst):
+          ToStdout("    - %s", iname)
+      else:
+        ToStdout("  secondary for no instances")
 
   return 0
 
@@ -613,7 +655,9 @@ def SetNodeParams(opts, args):
   @return: the desired exit code
 
   """
-  if [opts.master_candidate, opts.drained, opts.offline].count(None) == 3:
+  all_changes = [opts.master_candidate, opts.drained, opts.offline,
+                 opts.master_capable, opts.vm_capable, opts.secondary_ip]
+  if all_changes.count(None) == len(all_changes):
     ToStderr("Please give at least one of the parameters.")
     return 1
 
@@ -621,6 +665,9 @@ def SetNodeParams(opts, args):
                                master_candidate=opts.master_candidate,
                                offline=opts.offline,
                                drained=opts.drained,
+                               master_capable=opts.master_capable,
+                               vm_capable=opts.vm_capable,
+                               secondary_ip=opts.secondary_ip,
                                force=opts.force,
                                auto_promote=opts.auto_promote)
 
@@ -637,22 +684,27 @@ def SetNodeParams(opts, args):
 commands = {
   'add': (
     AddNode, [ArgHost(min=1, max=1)],
-    [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT],
-    "[-s ip] [--readd] [--no-ssh-key-check] <node_name>",
+    [SECONDARY_IP_OPT, READD_OPT, NOSSH_KEYCHECK_OPT, NONODE_SETUP_OPT,
+     VERBOSE_OPT, NODEGROUP_OPT, PRIORITY_OPT, CAPAB_MASTER_OPT,
+     CAPAB_VM_OPT],
+    "[-s ip] [--readd] [--no-ssh-key-check] [--no-node-setup]  [--verbose] "
+    " <node_name>",
     "Add a node to the cluster"),
   'evacuate': (
     EvacuateNode, [ArgNode(min=1)],
-    [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT],
+    [FORCE_OPT, IALLOCATOR_OPT, NEW_SECONDARY_OPT, EARLY_RELEASE_OPT,
+     PRIORITY_OPT],
     "[-f] {-I <iallocator> | -n <dst>} <node>",
     "Relocate the secondary instances from a node"
     " to other nodes (only for instances with drbd disk template)"),
   'failover': (
-    FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT],
+    FailoverNode, ARGS_ONE_NODE, [FORCE_OPT, IGNORE_CONSIST_OPT, PRIORITY_OPT],
     "[-f] <node>",
     "Stops the primary instances on a node and start them on their"
     " secondary node (only for instances with drbd disk template)"),
   'migrate': (
-    MigrateNode, ARGS_ONE_NODE, [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT],
+    MigrateNode, ARGS_ONE_NODE,
+    [FORCE_OPT, NONLIVE_OPT, MIGRATION_MODE_OPT, PRIORITY_OPT],
     "[-f] <node>",
     "Migrate all the primary instance on a node away from it"
     " (only for instances of type drbd)"),
@@ -669,22 +721,24 @@ commands = {
   'modify': (
     SetNodeParams, ARGS_ONE_NODE,
     [FORCE_OPT, SUBMIT_OPT, MC_OPT, DRAINED_OPT, OFFLINE_OPT,
-     AUTO_PROMOTE_OPT, DRY_RUN_OPT],
+     CAPAB_MASTER_OPT, CAPAB_VM_OPT, SECONDARY_IP_OPT,
+     AUTO_PROMOTE_OPT, DRY_RUN_OPT, PRIORITY_OPT],
     "<node_name>", "Alters the parameters of a node"),
   'powercycle': (
     PowercycleNode, ARGS_ONE_NODE,
-    [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT],
+    [FORCE_OPT, CONFIRM_OPT, DRY_RUN_OPT, PRIORITY_OPT],
     "<node_name>", "Tries to forcefully powercycle a node"),
   'remove': (
-    RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT],
+    RemoveNode, ARGS_ONE_NODE, [DRY_RUN_OPT, PRIORITY_OPT],
     "<node_name>", "Removes a node from the cluster"),
   'volumes': (
     ListVolumes, [ArgNode()],
-    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT],
+    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, PRIORITY_OPT],
     "[<node_name>...]", "List logical volumes on node(s)"),
   'list-storage': (
     ListStorage, ARGS_MANY_NODES,
-    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT],
+    [NOHDR_OPT, SEP_OPT, USEUNITS_OPT, FIELDS_OPT, _STORAGE_TYPE_OPT,
+     PRIORITY_OPT],
     "[<node_name>...]", "List physical volumes on node(s). The available"
     " fields are (see the man page for details): %s." %
     (utils.CommaJoin(_LIST_STOR_HEADERS))),
@@ -693,27 +747,28 @@ commands = {
     [ArgNode(min=1, max=1),
      ArgChoice(min=1, max=1, choices=_MODIFIABLE_STORAGE_TYPES),
      ArgFile(min=1, max=1)],
-    [ALLOCATABLE_OPT, DRY_RUN_OPT],
+    [ALLOCATABLE_OPT, DRY_RUN_OPT, PRIORITY_OPT],
     "<node_name> <storage_type> <name>", "Modify storage volume on a node"),
   'repair-storage': (
     RepairStorage,
     [ArgNode(min=1, max=1),
      ArgChoice(min=1, max=1, choices=_REPAIRABLE_STORAGE_TYPES),
      ArgFile(min=1, max=1)],
-    [IGNORE_CONSIST_OPT, DRY_RUN_OPT],
+    [IGNORE_CONSIST_OPT, DRY_RUN_OPT, PRIORITY_OPT],
     "<node_name> <storage_type> <name>",
     "Repairs a storage volume on a node"),
   'list-tags': (
     ListTags, ARGS_ONE_NODE, [],
     "<node_name>", "List the tags of the given node"),
   'add-tags': (
-    AddTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT],
+    AddTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT, PRIORITY_OPT],
     "<node_name> tag...", "Add tags to the given node"),
   'remove-tags': (
-    RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()], [TAG_SRC_OPT],
+    RemoveTags, [ArgNode(min=1, max=1), ArgUnknown()],
+    [TAG_SRC_OPT, PRIORITY_OPT],
     "<node_name> tag...", "Remove tags from the given node"),
   }
 
 
-if __name__ == '__main__':
-  sys.exit(GenericMain(commands, override={"tag_type": constants.TAG_NODE}))
+def Main():
+  return GenericMain(commands, override={"tag_type": constants.TAG_NODE})
old mode 100755 (executable)
new mode 100644 (file)
similarity index 94%
rename from scripts/gnt-os
rename to lib/client/gnt_os.py
index 2c25065..9808d5e
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#
 #
 
 # Copyright (C) 2006, 2007, 2010 Google Inc.
@@ -26,8 +26,6 @@
 # W0614: Unused import %s from wildcard import (since we need cli)
 # C0103: Invalid name gnt-os
 
-import sys
-
 from ganeti.cli import *
 from ganeti import constants
 from ganeti import opcodes
@@ -282,18 +280,22 @@ def ModifyOS(opts, args):
 
 commands = {
   'list': (
-    ListOS, ARGS_NONE, [NOHDR_OPT], "", "Lists all valid operating systems"
-    " on the cluster"),
+    ListOS, ARGS_NONE, [NOHDR_OPT, PRIORITY_OPT],
+    "", "Lists all valid operating systems on the cluster"),
   'diagnose': (
-    DiagnoseOS, ARGS_NONE, [], "", "Diagnose all operating systems"),
+    DiagnoseOS, ARGS_NONE, [PRIORITY_OPT],
+    "", "Diagnose all operating systems"),
   'info': (
-    ShowOSInfo, [ArgOs()], [], "", "Show detailed information about "
+    ShowOSInfo, [ArgOs()], [PRIORITY_OPT],
+    "", "Show detailed information about "
     "operating systems"),
   'modify': (
-    ModifyOS, ARGS_ONE_OS, [HVLIST_OPT, OSPARAMS_OPT, DRY_RUN_OPT,
-                            HID_OS_OPT, BLK_OS_OPT], "",
-    "Modify the OS parameters"),
+    ModifyOS, ARGS_ONE_OS,
+    [HVLIST_OPT, OSPARAMS_OPT, DRY_RUN_OPT, PRIORITY_OPT,
+     HID_OS_OPT, BLK_OS_OPT],
+    "", "Modify the OS parameters"),
   }
 
-if __name__ == '__main__':
-  sys.exit(GenericMain(commands))
+
+def Main():
+  return GenericMain(commands)
index 1a30b33..8c175d6 100644 (file)
@@ -53,205 +53,38 @@ from ganeti import uidpool
 from ganeti import compat
 from ganeti import masterd
 from ganeti import netutils
+from ganeti import ht
 
 import ganeti.masterd.instance # pylint: disable-msg=W0611
 
-
-# Modifiable default values; need to define these here before the
-# actual LUs
-
-def _EmptyList():
-  """Returns an empty list.
-
-  """
-  return []
-
-
-def _EmptyDict():
-  """Returns an empty dict.
-
-  """
-  return {}
-
-
-#: The without-default default value
-_NoDefault = object()
-
-
-#: The no-type (value to complex to check it in the type system)
-_NoType = object()
-
-
-# Some basic types
-def _TNotNone(val):
-  """Checks if the given value is not None.
-
-  """
-  return val is not None
-
-
-def _TNone(val):
-  """Checks if the given value is None.
-
-  """
-  return val is None
-
-
-def _TBool(val):
-  """Checks if the given value is a boolean.
-
-  """
-  return isinstance(val, bool)
-
-
-def _TInt(val):
-  """Checks if the given value is an integer.
-
-  """
-  return isinstance(val, int)
-
-
-def _TFloat(val):
-  """Checks if the given value is a float.
-
-  """
-  return isinstance(val, float)
-
-
-def _TString(val):
-  """Checks if the given value is a string.
-
-  """
-  return isinstance(val, basestring)
-
-
-def _TTrue(val):
-  """Checks if a given value evaluates to a boolean True value.
-
-  """
-  return bool(val)
-
-
-def _TElemOf(target_list):
-  """Builds a function that checks if a given value is a member of a list.
-
-  """
-  return lambda val: val in target_list
-
-
-# Container types
-def _TList(val):
-  """Checks if the given value is a list.
-
-  """
-  return isinstance(val, list)
-
-
-def _TDict(val):
-  """Checks if the given value is a dictionary.
-
-  """
-  return isinstance(val, dict)
-
-
-def _TIsLength(size):
-  """Check is the given container is of the given size.
-
-  """
-  return lambda container: len(container) == size
-
-
-# Combinator types
-def _TAnd(*args):
-  """Combine multiple functions using an AND operation.
-
-  """
-  def fn(val):
-    return compat.all(t(val) for t in args)
-  return fn
-
-
-def _TOr(*args):
-  """Combine multiple functions using an AND operation.
-
-  """
-  def fn(val):
-    return compat.any(t(val) for t in args)
-  return fn
-
-
-def _TMap(fn, test):
-  """Checks that a modified version of the argument passes the given test.
-
-  """
-  return lambda val: test(fn(val))
-
-
-# Type aliases
-
-#: a non-empty string
-_TNonEmptyString = _TAnd(_TString, _TTrue)
-
-
-#: a maybe non-empty string
-_TMaybeString = _TOr(_TNonEmptyString, _TNone)
-
-
-#: a maybe boolean (bool or none)
-_TMaybeBool = _TOr(_TBool, _TNone)
-
-
-#: a positive integer
-_TPositiveInt = _TAnd(_TInt, lambda v: v >= 0)
-
-#: a strictly positive integer
-_TStrictPositiveInt = _TAnd(_TInt, lambda v: v > 0)
-
-
-def _TListOf(my_type):
-  """Checks if a given value is a list with all elements of the same type.
-
-  """
-  return _TAnd(_TList,
-               lambda lst: compat.all(my_type(v) for v in lst))
-
-
-def _TDictOf(key_type, val_type):
-  """Checks a dict type for the type of its key/values.
-
-  """
-  return _TAnd(_TDict,
-               lambda my_dict: (compat.all(key_type(v) for v in my_dict.keys())
-                                and compat.all(val_type(v)
-                                               for v in my_dict.values())))
-
-
 # Common opcode attributes
 
 #: output fields for a query operation
-_POutputFields = ("output_fields", _NoDefault, _TListOf(_TNonEmptyString))
+_POutputFields = ("output_fields", ht.NoDefault, ht.TListOf(ht.TNonEmptyString))
 
 
 #: the shutdown timeout
 _PShutdownTimeout = ("shutdown_timeout", constants.DEFAULT_SHUTDOWN_TIMEOUT,
-                     _TPositiveInt)
+                     ht.TPositiveInt)
 
 #: the force parameter
-_PForce = ("force", False, _TBool)
+_PForce = ("force", False, ht.TBool)
 
 #: a required instance name (for single-instance LUs)
-_PInstanceName = ("instance_name", _NoDefault, _TNonEmptyString)
+_PInstanceName = ("instance_name", ht.NoDefault, ht.TNonEmptyString)
 
+#: Whether to ignore offline nodes
+_PIgnoreOfflineNodes = ("ignore_offline_nodes", False, ht.TBool)
 
 #: a required node name (for single-node LUs)
-_PNodeName = ("node_name", _NoDefault, _TNonEmptyString)
+_PNodeName = ("node_name", ht.NoDefault, ht.TNonEmptyString)
 
 #: the migration type (live/non-live)
-_PMigrationMode = ("mode", None, _TOr(_TNone,
-                                      _TElemOf(constants.HT_MIGRATION_MODES)))
+_PMigrationMode = ("mode", None,
+                   ht.TOr(ht.TNone, ht.TElemOf(constants.HT_MIGRATION_MODES)))
 
 #: the obsolete 'live' mode (boolean)
-_PMigrationLive = ("live", None, _TMaybeBool)
+_PMigrationLive = ("live", None, ht.TMaybeBool)
 
 
 # End types
@@ -320,7 +153,7 @@ class LogicalUnit(object):
     op_id = self.op.OP_ID
     for attr_name, aval, test in self._OP_PARAMS:
       if not hasattr(op, attr_name):
-        if aval == _NoDefault:
+        if aval == ht.NoDefault:
           raise errors.OpPrereqError("Required parameter '%s.%s' missing" %
                                      (op_id, attr_name), errors.ECODE_INVAL)
         else:
@@ -330,7 +163,7 @@ class LogicalUnit(object):
             dval = aval
           setattr(self.op, attr_name, dval)
       attr_val = getattr(op, attr_name)
-      if test == _NoType:
+      if test == ht.NoType:
         # no tests here
         continue
       if not callable(test):
@@ -760,17 +593,19 @@ def _CheckGlobalHvParams(params):
     raise errors.OpPrereqError(msg, errors.ECODE_INVAL)
 
 
-def _CheckNodeOnline(lu, node):
+def _CheckNodeOnline(lu, node, msg=None):
   """Ensure that a given node is online.
 
   @param lu: the LU on behalf of which we make the check
   @param node: the node to check
+  @param msg: if passed, should be a message to replace the default one
   @raise errors.OpPrereqError: if the node is offline
 
   """
+  if msg is None:
+    msg = "Can't use offline node"
   if lu.cfg.GetNodeInfo(node).offline:
-    raise errors.OpPrereqError("Can't use offline node %s" % node,
-                               errors.ECODE_INVAL)
+    raise errors.OpPrereqError("%s: %s" % (msg, node), errors.ECODE_STATE)
 
 
 def _CheckNodeNotDrained(lu, node):
@@ -783,7 +618,20 @@ def _CheckNodeNotDrained(lu, node):
   """
   if lu.cfg.GetNodeInfo(node).drained:
     raise errors.OpPrereqError("Can't use drained node %s" % node,
-                               errors.ECODE_INVAL)
+                               errors.ECODE_STATE)
+
+
+def _CheckNodeVmCapable(lu, node):
+  """Ensure that a given node is vm capable.
+
+  @param lu: the LU on behalf of which we make the check
+  @param node: the node to check
+  @raise errors.OpPrereqError: if the node is not vm capable
+
+  """
+  if not lu.cfg.GetNodeInfo(node).vm_capable:
+    raise errors.OpPrereqError("Can't use non-vm_capable node %s" % node,
+                               errors.ECODE_STATE)
 
 
 def _CheckNodeHasOS(lu, node, os_name, force_variant):
@@ -804,6 +652,33 @@ def _CheckNodeHasOS(lu, node, os_name, force_variant):
     _CheckOSVariant(result.payload, os_name)
 
 
+def _CheckNodeHasSecondaryIP(lu, node, secondary_ip, prereq):
+  """Ensure that a node has the given secondary ip.
+
+  @type lu: L{LogicalUnit}
+  @param lu: the LU on behalf of which we make the check
+  @type node: string
+  @param node: the node to check
+  @type secondary_ip: string
+  @param secondary_ip: the ip to check
+  @type prereq: boolean
+  @param prereq: whether to throw a prerequisite or an execute error
+  @raise errors.OpPrereqError: if the node doesn't have the ip, and prereq=True
+  @raise errors.OpExecError: if the node doesn't have the ip, and prereq=False
+
+  """
+  result = lu.rpc.call_node_has_ip_address(node, secondary_ip)
+  result.Raise("Failure checking secondary ip on node %s" % node,
+               prereq=prereq, ecode=errors.ECODE_ENVIRON)
+  if not result.payload:
+    msg = ("Node claims it doesn't have the secondary ip you gave (%s),"
+           " please fix and re-run this command" % secondary_ip)
+    if prereq:
+      raise errors.OpPrereqError(msg, errors.ECODE_ENVIRON)
+    else:
+      raise errors.OpExecError(msg)
+
+
 def _RequireFileStorage():
   """Checks that file storage is enabled.
 
@@ -1262,7 +1137,6 @@ class LUDestroyCluster(LogicalUnit):
 
     """
     master = self.cfg.GetMasterNode()
-    modify_ssh_setup = self.cfg.GetClusterInfo().modify_ssh_setup
 
     # Run post hooks on master node before it's removed
     hm = self.proc.hmclass(self.rpc.call_hooks_runner, self)
@@ -1275,11 +1149,6 @@ class LUDestroyCluster(LogicalUnit):
     result = self.rpc.call_node_stop_master(master, False)
     result.Raise("Could not disable the master role")
 
-    if modify_ssh_setup:
-      priv_key, pub_key, _ = ssh.GetUserFiles(constants.GANETI_RUNAS)
-      utils.CreateBackup(priv_key)
-      utils.CreateBackup(pub_key)
-
     return master
 
 
@@ -1323,11 +1192,11 @@ class LUVerifyCluster(LogicalUnit):
   HPATH = "cluster-verify"
   HTYPE = constants.HTYPE_CLUSTER
   _OP_PARAMS = [
-    ("skip_checks", _EmptyList,
-     _TListOf(_TElemOf(constants.VERIFY_OPTIONAL_CHECKS))),
-    ("verbose", False, _TBool),
-    ("error_codes", False, _TBool),
-    ("debug_simulate_errors", False, _TBool),
+    ("skip_checks", ht.EmptyList,
+     ht.TListOf(ht.TElemOf(constants.VERIFY_OPTIONAL_CHECKS))),
+    ("verbose", False, ht.TBool),
+    ("error_codes", False, ht.TBool),
+    ("debug_simulate_errors", False, ht.TBool),
     ]
   REQ_BGL = False
 
@@ -1341,7 +1210,7 @@ class LUVerifyCluster(LogicalUnit):
   EINSTANCEDOWN = (TINSTANCE, "EINSTANCEDOWN")
   EINSTANCELAYOUT = (TINSTANCE, "EINSTANCELAYOUT")
   EINSTANCEMISSINGDISK = (TINSTANCE, "EINSTANCEMISSINGDISK")
-  EINSTANCEMISSINGDISK = (TINSTANCE, "EINSTANCEMISSINGDISK")
+  EINSTANCEFAULTYDISK = (TINSTANCE, "EINSTANCEFAULTYDISK")
   EINSTANCEWRONGNODE = (TINSTANCE, "EINSTANCEWRONGNODE")
   ENODEDRBD = (TNODE, "ENODEDRBD")
   ENODEDRBDHELPER = (TNODE, "ENODEDRBDHELPER")
@@ -1392,9 +1261,11 @@ class LUVerifyCluster(LogicalUnit):
     @ivar os_fail: whether the RPC call didn't return valid OS data
     @type oslist: list
     @ivar oslist: list of OSes as diagnosed by DiagnoseOS
+    @type vm_capable: boolean
+    @ivar vm_capable: whether the node can host instances
 
     """
-    def __init__(self, offline=False, name=None):
+    def __init__(self, offline=False, name=None, vm_capable=True):
       self.name = name
       self.volumes = {}
       self.instances = []
@@ -1404,6 +1275,7 @@ class LUVerifyCluster(LogicalUnit):
       self.mfree = 0
       self.dfree = 0
       self.offline = offline
+      self.vm_capable = vm_capable
       self.rpc_fail = False
       self.lvm_fail = False
       self.hyp_fail = False
@@ -1508,13 +1380,12 @@ class LUVerifyCluster(LogicalUnit):
                   code=self.ETYPE_WARNING)
 
     hyp_result = nresult.get(constants.NV_HYPERVISOR, None)
-    if isinstance(hyp_result, dict):
+    if ninfo.vm_capable and isinstance(hyp_result, dict):
       for hv_name, hv_result in hyp_result.iteritems():
         test = hv_result is not None
         _ErrorIf(test, self.ENODEHV, node,
                  "hypervisor %s verify failure: '%s'", hv_name, hv_result)
 
-
     test = nresult.get(constants.NV_NODESETUP,
                            ["Missing NODESETUP results"])
     _ErrorIf(test, self.ENODESETUP, node, "node setup error: %s",
@@ -1633,8 +1504,8 @@ class LUVerifyCluster(LogicalUnit):
           msg = "cannot reach the master IP"
         _ErrorIf(True, self.ENODENET, node, msg)
 
-
-  def _VerifyInstance(self, instance, instanceconfig, node_image):
+  def _VerifyInstance(self, instance, instanceconfig, node_image,
+                      diskstatus):
     """Verify an instance.
 
     This function checks to see if the required block devices are
@@ -1670,6 +1541,20 @@ class LUVerifyCluster(LogicalUnit):
         _ErrorIf(test, self.EINSTANCEWRONGNODE, instance,
                  "instance should not run on node %s", node)
 
+    diskdata = [(nname, success, status, idx)
+                for (nname, disks) in diskstatus.items()
+                for idx, (success, status) in enumerate(disks)]
+
+    for nname, success, bdev_status, idx in diskdata:
+      _ErrorIf(instanceconfig.admin_up and not success,
+               self.EINSTANCEFAULTYDISK, instance,
+               "couldn't retrieve status for disk/%s on %s: %s",
+               idx, nname, bdev_status)
+      _ErrorIf((instanceconfig.admin_up and success and
+                bdev_status.ldisk_status == constants.LDS_FAULTY),
+               self.EINSTANCEFAULTYDISK, instance,
+               "disk/%s on %s is faulty", idx, nname)
+
   def _VerifyOrphanVolumes(self, node_vol_should, node_image, reserved):
     """Verify if there are any unknown volumes in the cluster.
 
@@ -2020,6 +1905,80 @@ class LUVerifyCluster(LogicalUnit):
           _ErrorIf(True, self.ENODERPC, node,
                    "node returned invalid LVM info, check LVM status")
 
+  def _CollectDiskInfo(self, nodelist, node_image, instanceinfo):
+    """Gets per-disk status information for all instances.
+
+    @type nodelist: list of strings
+    @param nodelist: Node names
+    @type node_image: dict of (name, L{objects.Node})
+    @param node_image: Node objects
+    @type instanceinfo: dict of (name, L{objects.Instance})
+    @param instanceinfo: Instance objects
+
+    """
+    _ErrorIf = self._ErrorIf # pylint: disable-msg=C0103
+
+    node_disks = {}
+    node_disks_devonly = {}
+
+    for nname in nodelist:
+      disks = [(inst, disk)
+               for instlist in [node_image[nname].pinst,
+                                node_image[nname].sinst]
+               for inst in instlist
+               for disk in instanceinfo[inst].disks]
+
+      if not disks:
+        # No need to collect data
+        continue
+
+      node_disks[nname] = disks
+
+      # Creating copies as SetDiskID below will modify the objects and that can
+      # lead to incorrect data returned from nodes
+      devonly = [dev.Copy() for (_, dev) in disks]
+
+      for dev in devonly:
+        self.cfg.SetDiskID(dev, nname)
+
+      node_disks_devonly[nname] = devonly
+
+    assert len(node_disks) == len(node_disks_devonly)
+
+    # Collect data from all nodes with disks
+    result = self.rpc.call_blockdev_getmirrorstatus_multi(node_disks.keys(),
+                                                          node_disks_devonly)
+
+    assert len(result) == len(node_disks)
+
+    instdisk = {}
+
+    for (nname, nres) in result.items():
+      if nres.offline:
+        # Ignore offline node
+        continue
+
+      disks = node_disks[nname]
+
+      msg = nres.fail_msg
+      _ErrorIf(msg, self.ENODERPC, nname,
+               "while getting disk information: %s", nres.fail_msg)
+      if msg:
+        # No data from this node
+        data = len(disks) * [None]
+      else:
+        data = nres.payload
+
+      for ((inst, _), status) in zip(disks, data):
+        instdisk.setdefault(inst, {}).setdefault(nname, []).append(status)
+
+    assert compat.all(len(statuses) == len(instanceinfo[inst].disks) and
+                      len(nnames) <= len(instanceinfo[inst].all_nodes)
+                      for inst, nnames in instdisk.items()
+                      for nname, statuses in nnames.items())
+
+    return instdisk
+
   def BuildHooksEnv(self):
     """Build hooks env.
 
@@ -2098,6 +2057,7 @@ class LUVerifyCluster(LogicalUnit):
       constants.NV_TIME: None,
       constants.NV_MASTERIP: (master_node, master_ip),
       constants.NV_OSLIST: None,
+      constants.NV_VMNODES: self.cfg.GetNonVmCapableNodeList(),
       }
 
     if vg_name is not None:
@@ -2111,7 +2071,8 @@ class LUVerifyCluster(LogicalUnit):
 
     # Build our expected cluster state
     node_image = dict((node.name, self.NodeImage(offline=node.offline,
-                                                 name=node.name))
+                                                 name=node.name,
+                                                 vm_capable=node.vm_capable))
                       for node in nodeinfo)
 
     for instance in instancelist:
@@ -2150,6 +2111,9 @@ class LUVerifyCluster(LogicalUnit):
 
     all_drbd_map = self.cfg.ComputeDRBDMap()
 
+    feedback_fn("* Gathering disk information (%s nodes)" % len(nodelist))
+    instdisk = self._CollectDiskInfo(nodelist, node_image, instanceinfo)
+
     feedback_fn("* Verifying node status")
 
     refos_img = None
@@ -2185,29 +2149,32 @@ class LUVerifyCluster(LogicalUnit):
       nresult = all_nvinfo[node].payload
 
       nimg.call_ok = self._VerifyNode(node_i, nresult)
+      self._VerifyNodeTime(node_i, nresult, nvinfo_starttime, nvinfo_endtime)
       self._VerifyNodeNetwork(node_i, nresult)
-      self._VerifyNodeLVM(node_i, nresult, vg_name)
       self._VerifyNodeFiles(node_i, nresult, file_names, local_checksums,
                             master_files)
-      self._VerifyNodeDrbd(node_i, nresult, instanceinfo, drbd_helper,
-                           all_drbd_map)
-      self._VerifyNodeTime(node_i, nresult, nvinfo_starttime, nvinfo_endtime)
 
-      self._UpdateNodeVolumes(node_i, nresult, nimg, vg_name)
-      self._UpdateNodeInstances(node_i, nresult, nimg)
-      self._UpdateNodeInfo(node_i, nresult, nimg, vg_name)
-      self._UpdateNodeOS(node_i, nresult, nimg)
-      if not nimg.os_fail:
-        if refos_img is None:
-          refos_img = nimg
-        self._VerifyNodeOS(node_i, nimg, refos_img)
+      if nimg.vm_capable:
+        self._VerifyNodeLVM(node_i, nresult, vg_name)
+        self._VerifyNodeDrbd(node_i, nresult, instanceinfo, drbd_helper,
+                             all_drbd_map)
+
+        self._UpdateNodeVolumes(node_i, nresult, nimg, vg_name)
+        self._UpdateNodeInstances(node_i, nresult, nimg)
+        self._UpdateNodeInfo(node_i, nresult, nimg, vg_name)
+        self._UpdateNodeOS(node_i, nresult, nimg)
+        if not nimg.os_fail:
+          if refos_img is None:
+            refos_img = nimg
+          self._VerifyNodeOS(node_i, nimg, refos_img)
 
     feedback_fn("* Verifying instance status")
     for instance in instancelist:
       if verbose:
         feedback_fn("* Verifying instance %s" % instance)
       inst_config = instanceinfo[instance]
-      self._VerifyInstance(instance, inst_config, node_image)
+      self._VerifyInstance(instance, inst_config, node_image,
+                           instdisk[instance])
       inst_nodes_offline = []
 
       pnode = inst_config.primary_node
@@ -2246,10 +2213,12 @@ class LUVerifyCluster(LogicalUnit):
       _ErrorIf(inst_nodes_offline, self.EINSTANCEBADNODE, instance,
                "instance lives on offline node(s) %s",
                utils.CommaJoin(inst_nodes_offline))
-      # ... or ghost nodes
+      # ... or ghost/non-vm_capable nodes
       for node in inst_config.all_nodes:
         _ErrorIf(node_image[node].ghost, self.EINSTANCEBADNODE, instance,
                  "instance lives on ghost node %s", node)
+        _ErrorIf(not node_image[node].vm_capable, self.EINSTANCEBADNODE,
+                 instance, "instance lives on non-vm_capable node %s", node)
 
     feedback_fn("* Verifying orphan volumes")
     reserved = utils.FieldSet(*cluster.reserved_lvs)
@@ -2404,7 +2373,7 @@ class LURepairDiskSizes(NoHooksLU):
   """Verifies the cluster disks sizes.
 
   """
-  _OP_PARAMS = [("instances", _EmptyList, _TListOf(_TNonEmptyString))]
+  _OP_PARAMS = [("instances", ht.EmptyList, ht.TListOf(ht.TNonEmptyString))]
   REQ_BGL = False
 
   def ExpandNames(self):
@@ -2522,7 +2491,7 @@ class LURenameCluster(LogicalUnit):
   """
   HPATH = "cluster-rename"
   HTYPE = constants.HTYPE_CLUSTER
-  _OP_PARAMS = [("name", _NoDefault, _TNonEmptyString)]
+  _OP_PARAMS = [("name", ht.NoDefault, ht.TNonEmptyString)]
 
   def BuildHooksEnv(self):
     """Build hooks env.
@@ -2540,7 +2509,8 @@ class LURenameCluster(LogicalUnit):
     """Verify that the passed name is a valid one.
 
     """
-    hostname = netutils.GetHostInfo(self.op.name)
+    hostname = netutils.GetHostname(name=self.op.name,
+                                    family=self.cfg.GetPrimaryIPFamily())
 
     new_name = hostname.name
     self.ip = new_ip = hostname.ip
@@ -2553,7 +2523,7 @@ class LURenameCluster(LogicalUnit):
     if new_ip != old_ip:
       if netutils.TcpPing(new_ip, constants.DEFAULT_NODED_PORT):
         raise errors.OpPrereqError("The given cluster IP address (%s) is"
-                                   " reachable on the network. Aborting." %
+                                   " reachable on the network" %
                                    new_ip, errors.ECODE_NOTUNIQUE)
 
     self.op.name = new_name
@@ -2583,15 +2553,7 @@ class LURenameCluster(LogicalUnit):
         node_list.remove(master)
       except ValueError:
         pass
-      result = self.rpc.call_upload_file(node_list,
-                                         constants.SSH_KNOWN_HOSTS_FILE)
-      for to_node, to_result in result.iteritems():
-        msg = to_result.fail_msg
-        if msg:
-          msg = ("Copy of file %s to node %s failed: %s" %
-                 (constants.SSH_KNOWN_HOSTS_FILE, to_node, msg))
-          self.proc.LogWarning(msg)
-
+      _UploadHelper(self, node_list, constants.SSH_KNOWN_HOSTS_FILE)
     finally:
       result = self.rpc.call_node_start_master(master, False, False)
       msg = result.fail_msg
@@ -2609,32 +2571,37 @@ class LUSetClusterParams(LogicalUnit):
   HPATH = "cluster-modify"
   HTYPE = constants.HTYPE_CLUSTER
   _OP_PARAMS = [
-    ("vg_name", None, _TMaybeString),
+    ("vg_name", None, ht.TMaybeString),
     ("enabled_hypervisors", None,
-     _TOr(_TAnd(_TListOf(_TElemOf(constants.HYPER_TYPES)), _TTrue), _TNone)),
-    ("hvparams", None, _TOr(_TDictOf(_TNonEmptyString, _TDict), _TNone)),
-    ("beparams", None, _TOr(_TDict, _TNone)),
-    ("os_hvp", None, _TOr(_TDictOf(_TNonEmptyString, _TDict), _TNone)),
-    ("osparams", None, _TOr(_TDictOf(_TNonEmptyString, _TDict), _TNone)),
-    ("candidate_pool_size", None, _TOr(_TStrictPositiveInt, _TNone)),
-    ("uid_pool", None, _NoType),
-    ("add_uids", None, _NoType),
-    ("remove_uids", None, _NoType),
-    ("maintain_node_health", None, _TMaybeBool),
-    ("nicparams", None, _TOr(_TDict, _TNone)),
-    ("drbd_helper", None, _TOr(_TString, _TNone)),
-    ("default_iallocator", None, _TMaybeString),
-    ("reserved_lvs", None, _TOr(_TListOf(_TNonEmptyString), _TNone)),
-    ("hidden_os", None, _TOr(_TListOf(\
-          _TAnd(_TList,
-                _TIsLength(2),
-                _TMap(lambda v: v[0], _TElemOf(constants.DDMS_VALUES)))),
-          _TNone)),
-    ("blacklisted_os", None, _TOr(_TListOf(\
-          _TAnd(_TList,
-                _TIsLength(2),
-                _TMap(lambda v: v[0], _TElemOf(constants.DDMS_VALUES)))),
-          _TNone)),
+     ht.TOr(ht.TAnd(ht.TListOf(ht.TElemOf(constants.HYPER_TYPES)), ht.TTrue),
+            ht.TNone)),
+    ("hvparams", None, ht.TOr(ht.TDictOf(ht.TNonEmptyString, ht.TDict),
+                              ht.TNone)),
+    ("beparams", None, ht.TOr(ht.TDict, ht.TNone)),
+    ("os_hvp", None, ht.TOr(ht.TDictOf(ht.TNonEmptyString, ht.TDict),
+                            ht.TNone)),
+    ("osparams", None, ht.TOr(ht.TDictOf(ht.TNonEmptyString, ht.TDict),
+                              ht.TNone)),
+    ("candidate_pool_size", None, ht.TOr(ht.TStrictPositiveInt, ht.TNone)),
+    ("uid_pool", None, ht.NoType),
+    ("add_uids", None, ht.NoType),
+    ("remove_uids", None, ht.NoType),
+    ("maintain_node_health", None, ht.TMaybeBool),
+    ("prealloc_wipe_disks", None, ht.TMaybeBool),
+    ("nicparams", None, ht.TOr(ht.TDict, ht.TNone)),
+    ("drbd_helper", None, ht.TOr(ht.TString, ht.TNone)),
+    ("default_iallocator", None, ht.TOr(ht.TString, ht.TNone)),
+    ("reserved_lvs", None, ht.TOr(ht.TListOf(ht.TNonEmptyString), ht.TNone)),
+    ("hidden_os", None, ht.TOr(ht.TListOf(\
+          ht.TAnd(ht.TList,
+                ht.TIsLength(2),
+                ht.TMap(lambda v: v[0], ht.TElemOf(constants.DDMS_VALUES)))),
+          ht.TNone)),
+    ("blacklisted_os", None, ht.TOr(ht.TListOf(\
+          ht.TAnd(ht.TList,
+                ht.TIsLength(2),
+                ht.TMap(lambda v: v[0], ht.TElemOf(constants.DDMS_VALUES)))),
+          ht.TNone)),
     ]
   REQ_BGL = False
 
@@ -2893,6 +2860,9 @@ class LUSetClusterParams(LogicalUnit):
     if self.op.maintain_node_health is not None:
       self.cluster.maintain_node_health = self.op.maintain_node_health
 
+    if self.op.prealloc_wipe_disks is not None:
+      self.cluster.prealloc_wipe_disks = self.op.prealloc_wipe_disks
+
     if self.op.add_uids is not None:
       uidpool.AddToUidPool(self.cluster.uid_pool, self.op.add_uids)
 
@@ -2914,14 +2884,14 @@ class LUSetClusterParams(LogicalUnit):
       for key, val in mods:
         if key == constants.DDM_ADD:
           if val in lst:
-            feedback_fn("OS %s already in %s, ignoring", val, desc)
+            feedback_fn("OS %s already in %s, ignoring" % (val, desc))
           else:
             lst.append(val)
         elif key == constants.DDM_REMOVE:
           if val in lst:
             lst.remove(val)
           else:
-            feedback_fn("OS %s not found in %s, ignoring", val, desc)
+            feedback_fn("OS %s not found in %s, ignoring" % (val, desc))
         else:
           raise errors.ProgrammerError("Invalid modification '%s'" % key)
 
@@ -2934,7 +2904,21 @@ class LUSetClusterParams(LogicalUnit):
     self.cfg.Update(self.cluster, feedback_fn)
 
 
-def _RedistributeAncillaryFiles(lu, additional_nodes=None):
+def _UploadHelper(lu, nodes, fname):
+  """Helper for uploading a file and showing warnings.
+
+  """
+  if os.path.exists(fname):
+    result = lu.rpc.call_upload_file(nodes, fname)
+    for to_node, to_result in result.items():
+      msg = to_result.fail_msg
+      if msg:
+        msg = ("Copy of file %s to node %s failed: %s" %
+               (fname, to_node, msg))
+        lu.proc.LogWarning(msg)
+
+
+def _RedistributeAncillaryFiles(lu, additional_nodes=None, additional_vm=True):
   """Distribute additional files which are part of the cluster configuration.
 
   ConfigWriter takes care of distributing the config and ssconf files, but
@@ -2943,15 +2927,23 @@ def _RedistributeAncillaryFiles(lu, additional_nodes=None):
 
   @param lu: calling logical unit
   @param additional_nodes: list of nodes not in the config to distribute to
+  @type additional_vm: boolean
+  @param additional_vm: whether the additional nodes are vm-capable or not
 
   """
   # 1. Gather target nodes
   myself = lu.cfg.GetNodeInfo(lu.cfg.GetMasterNode())
   dist_nodes = lu.cfg.GetOnlineNodeList()
+  nvm_nodes = lu.cfg.GetNonVmCapableNodeList()
+  vm_nodes = [name for name in dist_nodes if name not in nvm_nodes]
   if additional_nodes is not None:
     dist_nodes.extend(additional_nodes)
+    if additional_vm:
+      vm_nodes.extend(additional_nodes)
   if myself.name in dist_nodes:
     dist_nodes.remove(myself.name)
+  if myself.name in vm_nodes:
+    vm_nodes.remove(myself.name)
 
   # 2. Gather files to distribute
   dist_files = set([constants.ETC_HOSTS,
@@ -2962,21 +2954,17 @@ def _RedistributeAncillaryFiles(lu, additional_nodes=None):
                     constants.CLUSTER_DOMAIN_SECRET_FILE,
                    ])
 
+  vm_files = set()
   enabled_hypervisors = lu.cfg.GetClusterInfo().enabled_hypervisors
   for hv_name in enabled_hypervisors:
     hv_class = hypervisor.GetHypervisor(hv_name)
-    dist_files.update(hv_class.GetAncillaryFiles())
+    vm_files.update(hv_class.GetAncillaryFiles())
 
   # 3. Perform the files upload
   for fname in dist_files:
-    if os.path.exists(fname):
-      result = lu.rpc.call_upload_file(dist_nodes, fname)
-      for to_node, to_result in result.items():
-        msg = to_result.fail_msg
-        if msg:
-          msg = ("Copy of file %s to node %s failed: %s" %
-                 (fname, to_node, msg))
-          lu.proc.LogWarning(msg)
+    _UploadHelper(lu, dist_nodes, fname)
+  for fname in vm_files:
+    _UploadHelper(lu, vm_nodes, fname)
 
 
 class LURedistributeConfig(NoHooksLU):
@@ -3116,7 +3104,7 @@ class LUDiagnoseOS(NoHooksLU):
   """
   _OP_PARAMS = [
     _POutputFields,
-    ("names", _EmptyList, _TListOf(_TNonEmptyString)),
+    ("names", ht.EmptyList, ht.TListOf(ht.TNonEmptyString)),
     ]
   REQ_BGL = False
   _HID = "hidden"
@@ -3339,8 +3327,11 @@ class LURemoveNode(LogicalUnit):
 
     # Remove node from our /etc/hosts
     if self.cfg.GetClusterInfo().modify_etc_hosts:
-      # FIXME: this should be done via an rpc call to node daemon
-      utils.RemoveHostFromEtcHosts(node.name)
+      master_node = self.cfg.GetMasterNode()
+      result = self.rpc.call_etc_hosts_modify(master_node,
+                                              constants.ETC_HOSTS_REMOVE,
+                                              node.name, None)
+      result.Raise("Can't update hosts file with new host data")
       _RedistributeAncillaryFiles(self)
 
 
@@ -3351,13 +3342,14 @@ class LUQueryNodes(NoHooksLU):
   # pylint: disable-msg=W0142
   _OP_PARAMS = [
     _POutputFields,
-    ("names", _EmptyList, _TListOf(_TNonEmptyString)),
-    ("use_locking", False, _TBool),
+    ("names", ht.EmptyList, ht.TListOf(ht.TNonEmptyString)),
+    ("use_locking", False, ht.TBool),
     ]
   REQ_BGL = False
 
   _SIMPLE_FIELDS = ["name", "serial_no", "ctime", "mtime", "uuid",
-                    "master_candidate", "offline", "drained"]
+                    "master_candidate", "offline", "drained",
+                    "master_capable", "vm_capable"]
 
   _FIELDS_DYNAMIC = utils.FieldSet(
     "dtotal", "dfree",
@@ -3507,8 +3499,8 @@ class LUQueryNodeVolumes(NoHooksLU):
 
   """
   _OP_PARAMS = [
-    ("nodes", _EmptyList, _TListOf(_TNonEmptyString)),
-    ("output_fields", _NoDefault, _TListOf(_TNonEmptyString)),
+    ("nodes", ht.EmptyList, ht.TListOf(ht.TNonEmptyString)),
+    ("output_fields", ht.NoDefault, ht.TListOf(ht.TNonEmptyString)),
     ]
   REQ_BGL = False
   _FIELDS_DYNAMIC = utils.FieldSet("phys", "vg", "name", "size", "instance")
@@ -3590,10 +3582,10 @@ class LUQueryNodeStorage(NoHooksLU):
   """
   _FIELDS_STATIC = utils.FieldSet(constants.SF_NODE)
   _OP_PARAMS = [
-    ("nodes", _EmptyList, _TListOf(_TNonEmptyString)),
-    ("storage_type", _NoDefault, _CheckStorageType),
-    ("output_fields", _NoDefault, _TListOf(_TNonEmptyString)),
-    ("name", None, _TMaybeString),
+    ("nodes", ht.EmptyList, ht.TListOf(ht.TNonEmptyString)),
+    ("storage_type", ht.NoDefault, _CheckStorageType),
+    ("output_fields", ht.NoDefault, ht.TListOf(ht.TNonEmptyString)),
+    ("name", None, ht.TMaybeString),
     ]
   REQ_BGL = False
 
@@ -3679,9 +3671,9 @@ class LUModifyNodeStorage(NoHooksLU):
   """
   _OP_PARAMS = [
     _PNodeName,
-    ("storage_type", _NoDefault, _CheckStorageType),
-    ("name", _NoDefault, _TNonEmptyString),
-    ("changes", _NoDefault, _TDict),
+    ("storage_type", ht.NoDefault, _CheckStorageType),
+    ("name", ht.NoDefault, ht.TNonEmptyString),
+    ("changes", ht.NoDefault, ht.TDict),
     ]
   REQ_BGL = False
 
@@ -3729,14 +3721,24 @@ class LUAddNode(LogicalUnit):
   HTYPE = constants.HTYPE_NODE
   _OP_PARAMS = [
     _PNodeName,
-    ("primary_ip", None, _NoType),
-    ("secondary_ip", None, _TMaybeString),
-    ("readd", False, _TBool),
+    ("primary_ip", None, ht.NoType),
+    ("secondary_ip", None, ht.TMaybeString),
+    ("readd", False, ht.TBool),
+    ("group", None, ht.TMaybeString),
+    ("master_capable", None, ht.TMaybeBool),
+    ("vm_capable", None, ht.TMaybeBool),
     ]
+  _NFLAGS = ["master_capable", "vm_capable"]
 
   def CheckArguments(self):
+    self.primary_ip_family = self.cfg.GetPrimaryIPFamily()
     # validate/normalize the node name
-    self.op.node_name = netutils.HostInfo.NormalizeName(self.op.node_name)
+    self.hostname = netutils.GetHostname(name=self.op.node_name,
+                                         family=self.primary_ip_family)
+    self.op.node_name = self.hostname.name
+    if self.op.readd and self.op.group:
+      raise errors.OpPrereqError("Cannot pass a node group when a node is"
+                                 " being readded", errors.ECODE_INVAL)
 
   def BuildHooksEnv(self):
     """Build hooks env.
@@ -3749,6 +3751,8 @@ class LUAddNode(LogicalUnit):
       "NODE_NAME": self.op.node_name,
       "NODE_PIP": self.op.primary_ip,
       "NODE_SIP": self.op.secondary_ip,
+      "MASTER_CAPABLE": str(self.op.master_capable),
+      "VM_CAPABLE": str(self.op.vm_capable),
       }
     nodes_0 = self.cfg.GetNodeList()
     nodes_1 = nodes_0 + [self.op.node_name, ]
@@ -3765,19 +3769,21 @@ class LUAddNode(LogicalUnit):
     Any errors are signaled by raising errors.OpPrereqError.
 
     """
-    node_name = self.op.node_name
     cfg = self.cfg
-
-    dns_data = netutils.GetHostInfo(node_name)
-
-    node = dns_data.name
-    primary_ip = self.op.primary_ip = dns_data.ip
+    hostname = self.hostname
+    node = hostname.name
+    primary_ip = self.op.primary_ip = hostname.ip
     if self.op.secondary_ip is None:
+      if self.primary_ip_family == netutils.IP6Address.family:
+        raise errors.OpPrereqError("When using a IPv6 primary address, a valid"
+                                   " IPv4 address must be given as secondary",
+                                   errors.ECODE_INVAL)
       self.op.secondary_ip = primary_ip
-    if not netutils.IsValidIP4(self.op.secondary_ip):
-      raise errors.OpPrereqError("Invalid secondary IP given",
-                                 errors.ECODE_INVAL)
+
     secondary_ip = self.op.secondary_ip
+    if not netutils.IP4Address.IsValid(secondary_ip):
+      raise errors.OpPrereqError("Secondary IP (%s) needs to be a valid IPv4"
+                                 " address" % secondary_ip, errors.ECODE_INVAL)
 
     node_list = cfg.GetNodeList()
     if not self.op.readd and node in node_list:
@@ -3810,6 +3816,27 @@ class LUAddNode(LogicalUnit):
                                    " existing node %s" % existing_node.name,
                                    errors.ECODE_NOTUNIQUE)
 
+    # After this 'if' block, None is no longer a valid value for the
+    # _capable op attributes
+    if self.op.readd:
+      old_node = self.cfg.GetNodeInfo(node)
+      assert old_node is not None, "Can't retrieve locked node %s" % node
+      for attr in self._NFLAGS:
+        if getattr(self.op, attr) is None:
+          setattr(self.op, attr, getattr(old_node, attr))
+    else:
+      for attr in self._NFLAGS:
+        if getattr(self.op, attr) is None:
+          setattr(self.op, attr, True)
+
+    if self.op.readd and not self.op.vm_capable:
+      pri, sec = cfg.GetNodeInstances(node)
+      if pri or sec:
+        raise errors.OpPrereqError("Node %s being re-added with vm_capable"
+                                   " flag set to false, but it already holds"
+                                   " instances" % node,
+                                   errors.ECODE_STATE)
+
     # check that the type of the node (single versus dual homed) is the
     # same as for the master
     myself = cfg.GetNodeInfo(self.cfg.GetMasterNode())
@@ -3817,11 +3844,11 @@ class LUAddNode(LogicalUnit):
     newbie_singlehomed = secondary_ip == primary_ip
     if master_singlehomed != newbie_singlehomed:
       if master_singlehomed:
-        raise errors.OpPrereqError("The master has no private ip but the"
+        raise errors.OpPrereqError("The master has no secondary ip but the"
                                    " new node has one",
                                    errors.ECODE_INVAL)
       else:
-        raise errors.OpPrereqError("The master has a private ip but the"
+        raise errors.OpPrereqError("The master has a secondary ip but the"
                                    " new node doesn't have one",
                                    errors.ECODE_INVAL)
 
@@ -3835,7 +3862,7 @@ class LUAddNode(LogicalUnit):
       if not netutils.TcpPing(secondary_ip, constants.DEFAULT_NODED_PORT,
                            source=myself.secondary_ip):
         raise errors.OpPrereqError("Node secondary ip not reachable by TCP"
-                                   " based ping to noded port",
+                                   " based ping to node daemon port",
                                    errors.ECODE_ENVIRON)
 
     if self.op.readd:
@@ -3843,17 +3870,21 @@ class LUAddNode(LogicalUnit):
     else:
       exceptions = []
 
-    self.master_candidate = _DecideSelfPromotion(self, exceptions=exceptions)
+    if self.op.master_capable:
+      self.master_candidate = _DecideSelfPromotion(self, exceptions=exceptions)
+    else:
+      self.master_candidate = False
 
     if self.op.readd:
-      self.new_node = self.cfg.GetNodeInfo(node)
-      assert self.new_node is not None, "Can't retrieve locked node %s" % node
+      self.new_node = old_node
     else:
+      node_group = cfg.LookupNodeGroup(self.op.group)
       self.new_node = objects.Node(name=node,
                                    primary_ip=primary_ip,
                                    secondary_ip=secondary_ip,
                                    master_candidate=self.master_candidate,
-                                   offline=False, drained=False)
+                                   offline=False, drained=False,
+                                   group=node_group)
 
   def Exec(self, feedback_fn):
     """Adds the new node to the cluster.
@@ -3874,6 +3905,10 @@ class LUAddNode(LogicalUnit):
       if self.changed_primary_ip:
         new_node.primary_ip = self.op.primary_ip
 
+    # copy the master/vm_capable flags
+    for attr in self._NFLAGS:
+      setattr(new_node, attr, getattr(self.op, attr))
+
     # notify the user about any possible mc promotion
     if new_node.master_candidate:
       self.LogInfo("Node will be a master candidate")
@@ -3889,37 +3924,18 @@ class LUAddNode(LogicalUnit):
                                " node version %s" %
                                (constants.PROTOCOL_VERSION, result.payload))
 
-    # setup ssh on node
-    if self.cfg.GetClusterInfo().modify_ssh_setup:
-      logging.info("Copy ssh key to node %s", node)
-      priv_key, pub_key, _ = ssh.GetUserFiles(constants.GANETI_RUNAS)
-      keyarray = []
-      keyfiles = [constants.SSH_HOST_DSA_PRIV, constants.SSH_HOST_DSA_PUB,
-                  constants.SSH_HOST_RSA_PRIV, constants.SSH_HOST_RSA_PUB,
-                  priv_key, pub_key]
-
-      for i in keyfiles:
-        keyarray.append(utils.ReadFile(i))
-
-      result = self.rpc.call_node_add(node, keyarray[0], keyarray[1],
-                                      keyarray[2], keyarray[3], keyarray[4],
-                                      keyarray[5])
-      result.Raise("Cannot transfer ssh keys to the new node")
-
     # Add node to our /etc/hosts, and add key to known_hosts
     if self.cfg.GetClusterInfo().modify_etc_hosts:
-      # FIXME: this should be done via an rpc call to node daemon
-      utils.AddHostToEtcHosts(new_node.name)
+      master_node = self.cfg.GetMasterNode()
+      result = self.rpc.call_etc_hosts_modify(master_node,
+                                              constants.ETC_HOSTS_ADD,
+                                              self.hostname.name,
+                                              self.hostname.ip)
+      result.Raise("Can't update hosts file with new host data")
 
     if new_node.secondary_ip != new_node.primary_ip:
-      result = self.rpc.call_node_has_ip_address(new_node.name,
-                                                 new_node.secondary_ip)
-      result.Raise("Failure checking secondary ip on node %s" % new_node.name,
-                   prereq=True, ecode=errors.ECODE_ENVIRON)
-      if not result.payload:
-        raise errors.OpExecError("Node claims it doesn't have the secondary ip"
-                                 " you gave (%s). Please fix and re-run this"
-                                 " command." % new_node.secondary_ip)
+      _CheckNodeHasSecondaryIP(self, new_node.name, new_node.secondary_ip,
+                               False)
 
     node_verify_list = [self.cfg.GetMasterNode()]
     node_verify_param = {
@@ -3952,30 +3968,50 @@ class LUAddNode(LogicalUnit):
           self.LogWarning("Node failed to demote itself from master"
                           " candidate status: %s" % msg)
     else:
-      _RedistributeAncillaryFiles(self, additional_nodes=[node])
+      _RedistributeAncillaryFiles(self, additional_nodes=[node],
+                                  additional_vm=self.op.vm_capable)
       self.context.AddNode(new_node, self.proc.GetECId())
 
 
 class LUSetNodeParams(LogicalUnit):
   """Modifies the parameters of a node.
 
+  @cvar _F2R: a dictionary from tuples of flags (mc, drained, offline)
+      to the node role (as _ROLE_*)
+  @cvar _R2F: a dictionary from node role to tuples of flags
+  @cvar _FLAGS: a list of attribute names corresponding to the flags
+
   """
   HPATH = "node-modify"
   HTYPE = constants.HTYPE_NODE
   _OP_PARAMS = [
     _PNodeName,
-    ("master_candidate", None, _TMaybeBool),
-    ("offline", None, _TMaybeBool),
-    ("drained", None, _TMaybeBool),
-    ("auto_promote", False, _TBool),
+    ("master_candidate", None, ht.TMaybeBool),
+    ("offline", None, ht.TMaybeBool),
+    ("drained", None, ht.TMaybeBool),
+    ("auto_promote", False, ht.TBool),
+    ("master_capable", None, ht.TMaybeBool),
+    ("vm_capable", None, ht.TMaybeBool),
+    ("secondary_ip", None, ht.TMaybeString),
     _PForce,
     ]
   REQ_BGL = False
+  (_ROLE_CANDIDATE, _ROLE_DRAINED, _ROLE_OFFLINE, _ROLE_REGULAR) = range(4)
+  _F2R = {
+    (True, False, False): _ROLE_CANDIDATE,
+    (False, True, False): _ROLE_DRAINED,
+    (False, False, True): _ROLE_OFFLINE,
+    (False, False, False): _ROLE_REGULAR,
+    }
+  _R2F = dict((v, k) for k, v in _F2R.items())
+  _FLAGS = ["master_candidate", "drained", "offline"]
 
   def CheckArguments(self):
     self.op.node_name = _ExpandNodeName(self.cfg, self.op.node_name)
-    all_mods = [self.op.offline, self.op.master_candidate, self.op.drained]
-    if all_mods.count(None) == 3:
+    all_mods = [self.op.offline, self.op.master_candidate, self.op.drained,
+                self.op.master_capable, self.op.vm_capable,
+                self.op.secondary_ip]
+    if all_mods.count(None) == len(all_mods):
       raise errors.OpPrereqError("Please pass at least one modification",
                                  errors.ECODE_INVAL)
     if all_mods.count(True) > 1:
@@ -3983,16 +4019,20 @@ class LUSetNodeParams(LogicalUnit):
                                  " state at the same time",
                                  errors.ECODE_INVAL)
 
-    # Boolean value that tells us whether we're offlining or draining the node
-    self.offline_or_drain = (self.op.offline == True or
-                             self.op.drained == True)
-    self.deoffline_or_drain = (self.op.offline == False or
-                               self.op.drained == False)
+    # Boolean value that tells us whether we might be demoting from MC
     self.might_demote = (self.op.master_candidate == False or
-                         self.offline_or_drain)
+                         self.op.offline == True or
+                         self.op.drained == True or
+                         self.op.master_capable == False)
+
+    if self.op.secondary_ip:
+      if not netutils.IP4Address.IsValid(self.op.secondary_ip):
+        raise errors.OpPrereqError("Secondary IP (%s) needs to be a valid IPv4"
+                                   " address" % self.op.secondary_ip,
+                                   errors.ECODE_INVAL)
 
     self.lock_all = self.op.auto_promote and self.might_demote
-
+    self.lock_instances = self.op.secondary_ip is not None
 
   def ExpandNames(self):
     if self.lock_all:
@@ -4000,6 +4040,29 @@ class LUSetNodeParams(LogicalUnit):
     else:
       self.needed_locks = {locking.LEVEL_NODE: self.op.node_name}
 
+    if self.lock_instances:
+      self.needed_locks[locking.LEVEL_INSTANCE] = locking.ALL_SET
+
+  def DeclareLocks(self, level):
+    # If we have locked all instances, before waiting to lock nodes, release
+    # all the ones living on nodes unrelated to the current operation.
+    if level == locking.LEVEL_NODE and self.lock_instances:
+      instances_release = []
+      instances_keep = []
+      self.affected_instances = []
+      if self.needed_locks[locking.LEVEL_NODE] is not locking.ALL_SET:
+        for instance_name in self.acquired_locks[locking.LEVEL_INSTANCE]:
+          instance = self.context.cfg.GetInstanceInfo(instance_name)
+          i_mirrored = instance.disk_template in constants.DTS_NET_MIRROR
+          if i_mirrored and self.op.node_name in instance.all_nodes:
+            instances_keep.append(instance_name)
+            self.affected_instances.append(instance)
+          else:
+            instances_release.append(instance_name)
+        if instances_release:
+          self.context.glm.release(locking.LEVEL_INSTANCE, instances_release)
+          self.acquired_locks[locking.LEVEL_INSTANCE] = instances_keep
+
   def BuildHooksEnv(self):
     """Build hooks env.
 
@@ -4011,6 +4074,8 @@ class LUSetNodeParams(LogicalUnit):
       "MASTER_CANDIDATE": str(self.op.master_candidate),
       "OFFLINE": str(self.op.offline),
       "DRAINED": str(self.op.drained),
+      "MASTER_CAPABLE": str(self.op.master_capable),
+      "VM_CAPABLE": str(self.op.vm_capable),
       }
     nl = [self.cfg.GetMasterNode(),
           self.op.node_name]
@@ -4033,6 +4098,17 @@ class LUSetNodeParams(LogicalUnit):
                                    " only via master-failover",
                                    errors.ECODE_INVAL)
 
+    if self.op.master_candidate and not node.master_capable:
+      raise errors.OpPrereqError("Node %s is not master capable, cannot make"
+                                 " it a master candidate" % node.name,
+                                 errors.ECODE_STATE)
+
+    if self.op.vm_capable == False:
+      (ipri, isec) = self.cfg.GetNodeInstances(self.op.node_name)
+      if ipri or isec:
+        raise errors.OpPrereqError("Node %s hosts instances, cannot unset"
+                                   " the vm_capable flag" % node.name,
+                                   errors.ECODE_STATE)
 
     if node.master_candidate and self.might_demote and not self.lock_all:
       assert not self.op.auto_promote, "auto-promote set but lock_all not"
@@ -4043,80 +4119,136 @@ class LUSetNodeParams(LogicalUnit):
       if mc_remaining < mc_should:
         raise errors.OpPrereqError("Not enough master candidates, please"
                                    " pass auto_promote to allow promotion",
-                                   errors.ECODE_INVAL)
+                                   errors.ECODE_STATE)
 
-    if (self.op.master_candidate == True and
-        ((node.offline and not self.op.offline == False) or
-         (node.drained and not self.op.drained == False))):
-      raise errors.OpPrereqError("Node '%s' is offline or drained, can't set"
-                                 " to master_candidate" % node.name,
-                                 errors.ECODE_INVAL)
+    self.old_flags = old_flags = (node.master_candidate,
+                                  node.drained, node.offline)
+    assert old_flags in self._F2R, "Un-handled old flags  %s" % str(old_flags)
+    self.old_role = old_role = self._F2R[old_flags]
 
-    # If we're being deofflined/drained, we'll MC ourself if needed
-    if (self.deoffline_or_drain and not self.offline_or_drain and not
-        self.op.master_candidate == True and not node.master_candidate):
-      self.op.master_candidate = _DecideSelfPromotion(self)
-      if self.op.master_candidate:
-        self.LogInfo("Autopromoting node to master candidate")
+    # Check for ineffective changes
+    for attr in self._FLAGS:
+      if (getattr(self.op, attr) == False and getattr(node, attr) == False):
+        self.LogInfo("Ignoring request to unset flag %s, already unset", attr)
+        setattr(self.op, attr, None)
 
-    return
+    # Past this point, any flag change to False means a transition
+    # away from the respective state, as only real changes are kept
+
+    # If we're being deofflined/drained, we'll MC ourself if needed
+    if (self.op.drained == False or self.op.offline == False or
+        (self.op.master_capable and not node.master_capable)):
+      if _DecideSelfPromotion(self):
+        self.op.master_candidate = True
+        self.LogInfo("Auto-promoting node to master candidate")
+
+    # If we're no longer master capable, we'll demote ourselves from MC
+    if self.op.master_capable == False and node.master_candidate:
+      self.LogInfo("Demoting from master candidate")
+      self.op.master_candidate = False
+
+    # Compute new role
+    assert [getattr(self.op, attr) for attr in self._FLAGS].count(True) <= 1
+    if self.op.master_candidate:
+      new_role = self._ROLE_CANDIDATE
+    elif self.op.drained:
+      new_role = self._ROLE_DRAINED
+    elif self.op.offline:
+      new_role = self._ROLE_OFFLINE
+    elif False in [self.op.master_candidate, self.op.drained, self.op.offline]:
+      # False is still in new flags, which means we're un-setting (the
+      # only) True flag
+      new_role = self._ROLE_REGULAR
+    else: # no new flags, nothing, keep old role
+      new_role = old_role
+
+    self.new_role = new_role
+
+    if old_role == self._ROLE_OFFLINE and new_role != old_role:
+      # Trying to transition out of offline status
+      result = self.rpc.call_version([node.name])[node.name]
+      if result.fail_msg:
+        raise errors.OpPrereqError("Node %s is being de-offlined but fails"
+                                   " to report its version: %s" %
+                                   (node.name, result.fail_msg),
+                                   errors.ECODE_STATE)
+      else:
+        self.LogWarning("Transitioning node from offline to online state"
+                        " without using re-add. Please make sure the node"
+                        " is healthy!")
+
+    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_singlehomed = master.secondary_ip == master.primary_ip
+      if master_singlehomed and self.op.secondary_ip:
+        raise errors.OpPrereqError("Cannot change the secondary ip on a single"
+                                   " homed cluster", errors.ECODE_INVAL)
+
+      if node.offline:
+        if self.affected_instances:
+          raise errors.OpPrereqError("Cannot change secondary ip: offline"
+                                     " node has instances (%s) configured"
+                                     " to use it" % self.affected_instances)
+      else:
+        # On online nodes, check that no instances are running, and that
+        # the node has the new ip and we can reach it.
+        for instance in self.affected_instances:
+          _CheckInstanceDown(self, instance, "cannot change secondary ip")
+
+        _CheckNodeHasSecondaryIP(self, node.name, self.op.secondary_ip, True)
+        if master.name != node.name:
+          # check reachability from master secondary ip to new secondary ip
+          if not netutils.TcpPing(self.op.secondary_ip,
+                                  constants.DEFAULT_NODED_PORT,
+                                  source=master.secondary_ip):
+            raise errors.OpPrereqError("Node secondary ip not reachable by TCP"
+                                       " based ping to node daemon port",
+                                       errors.ECODE_ENVIRON)
 
   def Exec(self, feedback_fn):
     """Modifies a node.
 
     """
     node = self.node
+    old_role = self.old_role
+    new_role = self.new_role
 
     result = []
-    changed_mc = False
-
-    if self.op.offline is not None:
-      node.offline = self.op.offline
-      result.append(("offline", str(self.op.offline)))
-      if self.op.offline == True:
-        if node.master_candidate:
-          node.master_candidate = False
-          changed_mc = True
-          result.append(("master_candidate", "auto-demotion due to offline"))
-        if node.drained:
-          node.drained = False
-          result.append(("drained", "clear drained status due to offline"))
-
-    if self.op.master_candidate is not None:
-      node.master_candidate = self.op.master_candidate
-      changed_mc = True
-      result.append(("master_candidate", str(self.op.master_candidate)))
-      if self.op.master_candidate == False:
-        rrc = self.rpc.call_node_demote_from_mc(node.name)
-        msg = rrc.fail_msg
+
+    for attr in ["master_capable", "vm_capable"]:
+      val = getattr(self.op, attr)
+      if val is not None:
+        setattr(node, attr, val)
+        result.append((attr, str(val)))
+
+    if new_role != old_role:
+      # Tell the node to demote itself, if no longer MC and not offline
+      if old_role == self._ROLE_CANDIDATE and new_role != self._ROLE_OFFLINE:
+        msg = self.rpc.call_node_demote_from_mc(node.name).fail_msg
         if msg:
-          self.LogWarning("Node failed to demote itself: %s" % msg)
-
-    if self.op.drained is not None:
-      node.drained = self.op.drained
-      result.append(("drained", str(self.op.drained)))
-      if self.op.drained == True:
-        if node.master_candidate:
-          node.master_candidate = False
-          changed_mc = True
-          result.append(("master_candidate", "auto-demotion due to drain"))
-          rrc = self.rpc.call_node_demote_from_mc(node.name)
-          msg = rrc.fail_msg
-          if msg:
-            self.LogWarning("Node failed to demote itself: %s" % msg)
-        if node.offline:
-          node.offline = False
-          result.append(("offline", "clear offline status due to drain"))
+          self.LogWarning("Node failed to demote itself: %s", msg)
 
-    # we locked all nodes, we adjust the CP before updating this node
-    if self.lock_all:
-      _AdjustCandidatePool(self, [node.name])
+      new_flags = self._R2F[new_role]
+      for of, nf, desc in zip(self.old_flags, new_flags, self._FLAGS):
+        if of != nf:
+          result.append((desc, str(nf)))
+      (node.master_candidate, node.drained, node.offline) = new_flags
+
+      # we locked all nodes, we adjust the CP before updating this node
+      if self.lock_all:
+        _AdjustCandidatePool(self, [node.name])
+
+    if self.op.secondary_ip:
+      node.secondary_ip = self.op.secondary_ip
+      result.append(("secondary_ip", self.op.secondary_ip))
 
     # this will trigger configuration file update, if needed
     self.cfg.Update(node, feedback_fn)
 
-    # this will trigger job queue propagation or cleanup
-    if changed_mc:
+    # this will trigger job queue propagation or cleanup if the mc
+    # flag changed
+    if [old_role, new_role].count(self._ROLE_CANDIDATE) == 1:
       self.context.ReaddNode(node)
 
     return result
@@ -4181,6 +4313,11 @@ class LUQueryClusterInfo(NoHooksLU):
         if hv_name in cluster.enabled_hypervisors:
           os_hvp[os_name][hv_name] = hv_params
 
+    # Convert ip_family to ip_version
+    primary_ip_version = constants.IP4_VERSION
+    if cluster.primary_ip_family == netutils.IP6Address.family:
+      primary_ip_version = constants.IP6_VERSION
+
     result = {
       "software_version": constants.RELEASE_VERSION,
       "protocol_version": constants.PROTOCOL_VERSION,
@@ -4211,6 +4348,8 @@ class LUQueryClusterInfo(NoHooksLU):
       "uid_pool": cluster.uid_pool,
       "default_iallocator": cluster.default_iallocator,
       "reserved_lvs": cluster.reserved_lvs,
+      "primary_ip_version": primary_ip_version,
+      "prealloc_wipe_disks": cluster.prealloc_wipe_disks,
       }
 
     return result
@@ -4262,7 +4401,7 @@ class LUActivateInstanceDisks(NoHooksLU):
   """
   _OP_PARAMS = [
     _PInstanceName,
-    ("ignore_size", False, _TBool),
+    ("ignore_size", False, ht.TBool),
     ]
   REQ_BGL = False
 
@@ -4574,8 +4713,9 @@ class LUStartupInstance(LogicalUnit):
   _OP_PARAMS = [
     _PInstanceName,
     _PForce,
-    ("hvparams", _EmptyDict, _TDict),
-    ("beparams", _EmptyDict, _TDict),
+    _PIgnoreOfflineNodes,
+    ("hvparams", ht.EmptyDict, ht.TDict),
+    ("beparams", ht.EmptyDict, ht.TDict),
     ]
   REQ_BGL = False
 
@@ -4622,21 +4762,30 @@ class LUStartupInstance(LogicalUnit):
       hv_type.CheckParameterSyntax(filled_hvp)
       _CheckHVParams(self, instance.all_nodes, instance.hypervisor, filled_hvp)
 
-    _CheckNodeOnline(self, instance.primary_node)
+    self.primary_offline = self.cfg.GetNodeInfo(instance.primary_node).offline
 
-    bep = self.cfg.GetClusterInfo().FillBE(instance)
-    # check bridges existence
-    _CheckInstanceBridgesExist(self, instance)
+    if self.primary_offline and self.op.ignore_offline_nodes:
+      self.proc.LogWarning("Ignoring offline primary node")
+
+      if self.op.hvparams or self.op.beparams:
+        self.proc.LogWarning("Overridden parameters are ignored")
+    else:
+      _CheckNodeOnline(self, instance.primary_node)
+
+      bep = self.cfg.GetClusterInfo().FillBE(instance)
+
+      # check bridges existence
+      _CheckInstanceBridgesExist(self, instance)
 
-    remote_info = self.rpc.call_instance_info(instance.primary_node,
-                                              instance.name,
-                                              instance.hypervisor)
-    remote_info.Raise("Error checking node %s" % instance.primary_node,
-                      prereq=True, ecode=errors.ECODE_ENVIRON)
-    if not remote_info.payload: # not running already
-      _CheckNodeFreeMemory(self, instance.primary_node,
-                           "starting instance %s" % instance.name,
-                           bep[constants.BE_MEMORY], instance.hypervisor)
+      remote_info = self.rpc.call_instance_info(instance.primary_node,
+                                                instance.name,
+                                                instance.hypervisor)
+      remote_info.Raise("Error checking node %s" % instance.primary_node,
+                        prereq=True, ecode=errors.ECODE_ENVIRON)
+      if not remote_info.payload: # not running already
+        _CheckNodeFreeMemory(self, instance.primary_node,
+                             "starting instance %s" % instance.name,
+                             bep[constants.BE_MEMORY], instance.hypervisor)
 
   def Exec(self, feedback_fn):
     """Start the instance.
@@ -4647,16 +4796,20 @@ class LUStartupInstance(LogicalUnit):
 
     self.cfg.MarkInstanceUp(instance.name)
 
-    node_current = instance.primary_node
+    if self.primary_offline:
+      assert self.op.ignore_offline_nodes
+      self.proc.LogInfo("Primary node offline, marked instance as started")
+    else:
+      node_current = instance.primary_node
 
-    _StartInstanceDisks(self, instance, force)
+      _StartInstanceDisks(self, instance, force)
 
-    result = self.rpc.call_instance_start(node_current, instance,
-                                          self.op.hvparams, self.op.beparams)
-    msg = result.fail_msg
-    if msg:
-      _ShutdownInstanceDisks(self, instance)
-      raise errors.OpExecError("Could not start instance: %s" % msg)
+      result = self.rpc.call_instance_start(node_current, instance,
+                                            self.op.hvparams, self.op.beparams)
+      msg = result.fail_msg
+      if msg:
+        _ShutdownInstanceDisks(self, instance)
+        raise errors.OpExecError("Could not start instance: %s" % msg)
 
 
 class LURebootInstance(LogicalUnit):
@@ -4667,8 +4820,8 @@ class LURebootInstance(LogicalUnit):
   HTYPE = constants.HTYPE_INSTANCE
   _OP_PARAMS = [
     _PInstanceName,
-    ("ignore_secondaries", False, _TBool),
-    ("reboot_type", _NoDefault, _TElemOf(constants.REBOOT_TYPES)),
+    ("ignore_secondaries", False, ht.TBool),
+    ("reboot_type", ht.NoDefault, ht.TElemOf(constants.REBOOT_TYPES)),
     _PShutdownTimeout,
     ]
   REQ_BGL = False
@@ -4748,7 +4901,8 @@ class LUShutdownInstance(LogicalUnit):
   HTYPE = constants.HTYPE_INSTANCE
   _OP_PARAMS = [
     _PInstanceName,
-    ("timeout", constants.DEFAULT_SHUTDOWN_TIMEOUT, _TPositiveInt),
+    _PIgnoreOfflineNodes,
+    ("timeout", constants.DEFAULT_SHUTDOWN_TIMEOUT, ht.TPositiveInt),
     ]
   REQ_BGL = False
 
@@ -4775,7 +4929,14 @@ class LUShutdownInstance(LogicalUnit):
     self.instance = self.cfg.GetInstanceInfo(self.op.instance_name)
     assert self.instance is not None, \
       "Cannot retrieve locked instance %s" % self.op.instance_name
-    _CheckNodeOnline(self, self.instance.primary_node)
+
+    self.primary_offline = \
+      self.cfg.GetNodeInfo(self.instance.primary_node).offline
+
+    if self.primary_offline and self.op.ignore_offline_nodes:
+      self.proc.LogWarning("Ignoring offline primary node")
+    else:
+      _CheckNodeOnline(self, self.instance.primary_node)
 
   def Exec(self, feedback_fn):
     """Shutdown the instance.
@@ -4784,13 +4945,19 @@ class LUShutdownInstance(LogicalUnit):
     instance = self.instance
     node_current = instance.primary_node
     timeout = self.op.timeout
+
     self.cfg.MarkInstanceDown(instance.name)
-    result = self.rpc.call_instance_shutdown(node_current, instance, timeout)
-    msg = result.fail_msg
-    if msg:
-      self.proc.LogWarning("Could not shutdown instance: %s" % msg)
 
-    _ShutdownInstanceDisks(self, instance)
+    if self.primary_offline:
+      assert self.op.ignore_offline_nodes
+      self.proc.LogInfo("Primary node offline, marked instance as stopped")
+    else:
+      result = self.rpc.call_instance_shutdown(node_current, instance, timeout)
+      msg = result.fail_msg
+      if msg:
+        self.proc.LogWarning("Could not shutdown instance: %s" % msg)
+
+      _ShutdownInstanceDisks(self, instance)
 
 
 class LUReinstallInstance(LogicalUnit):
@@ -4801,8 +4968,9 @@ class LUReinstallInstance(LogicalUnit):
   HTYPE = constants.HTYPE_INSTANCE
   _OP_PARAMS = [
     _PInstanceName,
-    ("os_type", None, _TMaybeString),
-    ("force_variant", False, _TBool),
+    ("os_type", None, ht.TMaybeString),
+    ("force_variant", False, ht.TBool),
+    ("osparams", None, ht.TOr(ht.TDict, ht.TNone)),
     ]
   REQ_BGL = False
 
@@ -4828,7 +4996,11 @@ class LUReinstallInstance(LogicalUnit):
     instance = self.cfg.GetInstanceInfo(self.op.instance_name)
     assert instance is not None, \
       "Cannot retrieve locked instance %s" % self.op.instance_name
-    _CheckNodeOnline(self, instance.primary_node)
+    _CheckNodeOnline(self, instance.primary_node, "Instance primary node"
+                     " offline, cannot reinstall")
+    for node in instance.secondary_nodes:
+      _CheckNodeOnline(self, node, "Instance secondary node offline,"
+                       " cannot reinstall")
 
     if instance.disk_template == constants.DT_DISKLESS:
       raise errors.OpPrereqError("Instance '%s' has no disks" %
@@ -4840,6 +5012,18 @@ class LUReinstallInstance(LogicalUnit):
       # OS verification
       pnode = _ExpandNodeName(self.cfg, instance.primary_node)
       _CheckNodeHasOS(self, pnode, self.op.os_type, self.op.force_variant)
+      instance_os = self.op.os_type
+    else:
+      instance_os = instance.os
+
+    nodelist = list(instance.all_nodes)
+
+    if self.op.osparams:
+      i_osdict = _GetUpdatedParams(instance.osparams, self.op.osparams)
+      _CheckOSParams(self, True, nodelist, instance_os, i_osdict)
+      self.os_inst = i_osdict # the new dict (without defaults)
+    else:
+      self.os_inst = None
 
     self.instance = instance
 
@@ -4852,6 +5036,7 @@ class LUReinstallInstance(LogicalUnit):
     if self.op.os_type is not None:
       feedback_fn("Changing OS to '%s'..." % self.op.os_type)
       inst.os = self.op.os_type
+      # Write to configuration
       self.cfg.Update(inst, feedback_fn)
 
     _StartInstanceDisks(self, inst, None)
@@ -4859,7 +5044,8 @@ class LUReinstallInstance(LogicalUnit):
       feedback_fn("Running the instance OS create scripts...")
       # FIXME: pass debug option from opcode to backend
       result = self.rpc.call_instance_os_add(inst.primary_node, inst, True,
-                                             self.op.debug_level)
+                                             self.op.debug_level,
+                                             osparams=self.os_inst)
       result.Raise("Could not install OS for instance %s on node %s" %
                    (inst.name, inst.primary_node))
     finally:
@@ -4874,7 +5060,7 @@ class LURecreateInstanceDisks(LogicalUnit):
   HTYPE = constants.HTYPE_INSTANCE
   _OP_PARAMS = [
     _PInstanceName,
-    ("disks", _EmptyList, _TListOf(_TPositiveInt)),
+    ("disks", ht.EmptyList, ht.TListOf(ht.TPositiveInt)),
     ]
   REQ_BGL = False
 
@@ -4938,9 +5124,9 @@ class LURenameInstance(LogicalUnit):
   HTYPE = constants.HTYPE_INSTANCE
   _OP_PARAMS = [
     _PInstanceName,
-    ("new_name", _NoDefault, _TNonEmptyString),
-    ("ip_check", False, _TBool),
-    ("name_check", True, _TBool),
+    ("new_name", ht.NoDefault, ht.TNonEmptyString),
+    ("ip_check", False, ht.TBool),
+    ("name_check", True, ht.TBool),
     ]
 
   def CheckArguments(self):
@@ -4979,12 +5165,12 @@ class LURenameInstance(LogicalUnit):
 
     new_name = self.op.new_name
     if self.op.name_check:
-      hostinfo = netutils.HostInfo(netutils.HostInfo.NormalizeName(new_name))
-      new_name = self.op.new_name = hostinfo.name
+      hostname = netutils.GetHostname(name=new_name)
+      new_name = self.op.new_name = hostname.name
       if (self.op.ip_check and
-          netutils.TcpPing(hostinfo.ip, constants.DEFAULT_NODED_PORT)):
+          netutils.TcpPing(hostname.ip, constants.DEFAULT_NODED_PORT)):
         raise errors.OpPrereqError("IP %s of instance %s already in use" %
-                                   (hostinfo.ip, new_name),
+                                   (hostname.ip, new_name),
                                    errors.ECODE_NOTUNIQUE)
 
     instance_list = self.cfg.GetInstanceList()
@@ -4992,7 +5178,6 @@ class LURenameInstance(LogicalUnit):
       raise errors.OpPrereqError("Instance '%s' is already in the cluster" %
                                  new_name, errors.ECODE_EXISTS)
 
-
   def Exec(self, feedback_fn):
     """Reinstall the instance.
 
@@ -5045,7 +5230,7 @@ class LURemoveInstance(LogicalUnit):
   HTYPE = constants.HTYPE_INSTANCE
   _OP_PARAMS = [
     _PInstanceName,
-    ("ignore_failures", False, _TBool),
+    ("ignore_failures", False, ht.TBool),
     _PShutdownTimeout,
     ]
   REQ_BGL = False
@@ -5131,9 +5316,9 @@ class LUQueryInstances(NoHooksLU):
   """
   # pylint: disable-msg=W0142
   _OP_PARAMS = [
-    ("output_fields", _NoDefault, _TListOf(_TNonEmptyString)),
-    ("names", _EmptyList, _TListOf(_TNonEmptyString)),
-    ("use_locking", False, _TBool),
+    ("output_fields", ht.NoDefault, ht.TListOf(ht.TNonEmptyString)),
+    ("names", ht.EmptyList, ht.TListOf(ht.TNonEmptyString)),
+    ("use_locking", False, ht.TBool),
     ]
   REQ_BGL = False
   _SIMPLE_FIELDS = ["name", "os", "network_port", "hypervisor",
@@ -5150,7 +5335,8 @@ class LUQueryInstances(NoHooksLU):
                                     r"(nic)\.(bridge)/([0-9]+)",
                                     r"(nic)\.(macs|ips|modes|links|bridges)",
                                     r"(disk|nic)\.(count)",
-                                    "hvparams",
+                                    "hvparams", "custom_hvparams",
+                                    "custom_beparams", "custom_nicparams",
                                     ] + _SIMPLE_FIELDS +
                                   ["hv/%s" % name
                                    for name in constants.HVS_PARAMETERS
@@ -5328,6 +5514,8 @@ class LUQueryInstances(NoHooksLU):
             val = instance.nics[0].mac
           else:
             val = None
+        elif field == "custom_nicparams":
+          val = [nic.nicparams for nic in instance.nics]
         elif field == "sda_size" or field == "sdb_size":
           idx = ord(field[2]) - ord('a')
           try:
@@ -5339,12 +5527,16 @@ class LUQueryInstances(NoHooksLU):
           val = _ComputeDiskSize(instance.disk_template, disk_sizes)
         elif field == "tags":
           val = list(instance.GetTags())
+        elif field == "custom_hvparams":
+          val = instance.hvparams # not filled!
         elif field == "hvparams":
           val = i_hv
         elif (field.startswith(HVPREFIX) and
               field[len(HVPREFIX):] in constants.HVS_PARAMETERS and
               field[len(HVPREFIX):] not in constants.HVC_GLOBALS):
           val = i_hv.get(field[len(HVPREFIX):], None)
+        elif field == "custom_beparams":
+          val = instance.beparams
         elif field == "beparams":
           val = i_be
         elif (field.startswith(BEPREFIX) and
@@ -5424,7 +5616,7 @@ class LUFailoverInstance(LogicalUnit):
   HTYPE = constants.HTYPE_INSTANCE
   _OP_PARAMS = [
     _PInstanceName,
-    ("ignore_consistency", False, _TBool),
+    ("ignore_consistency", False, ht.TBool),
     _PShutdownTimeout,
     ]
   REQ_BGL = False
@@ -5505,6 +5697,7 @@ class LUFailoverInstance(LogicalUnit):
 
     """
     instance = self.instance
+    primary_node = self.cfg.GetNodeInfo(instance.primary_node)
 
     source_node = instance.primary_node
     target_node = instance.secondary_nodes[0]
@@ -5528,7 +5721,7 @@ class LUFailoverInstance(LogicalUnit):
                                              self.op.shutdown_timeout)
     msg = result.fail_msg
     if msg:
-      if self.op.ignore_consistency:
+      if self.op.ignore_consistency or primary_node.offline:
         self.proc.LogWarning("Could not shutdown instance %s on node %s."
                              " Proceeding anyway. Please make sure node"
                              " %s is down. Error details: %s",
@@ -5580,7 +5773,7 @@ class LUMigrateInstance(LogicalUnit):
     _PInstanceName,
     _PMigrationMode,
     _PMigrationLive,
-    ("cleanup", False, _TBool),
+    ("cleanup", False, ht.TBool),
     ]
 
   REQ_BGL = False
@@ -5631,7 +5824,7 @@ class LUMoveInstance(LogicalUnit):
   HTYPE = constants.HTYPE_INSTANCE
   _OP_PARAMS = [
     _PInstanceName,
-    ("target_node", _NoDefault, _TNonEmptyString),
+    ("target_node", ht.NoDefault, ht.TNonEmptyString),
     _PShutdownTimeout,
     ]
   REQ_BGL = False
@@ -5692,6 +5885,7 @@ class LUMoveInstance(LogicalUnit):
 
     _CheckNodeOnline(self, target_node)
     _CheckNodeNotDrained(self, target_node)
+    _CheckNodeVmCapable(self, target_node)
 
     if instance.admin_up:
       # check memory requirements on the secondary node
@@ -6396,6 +6590,58 @@ def _GetInstanceInfoText(instance):
   return "originstname+%s" % instance.name
 
 
+def _CalcEta(time_taken, written, total_size):
+  """Calculates the ETA based on size written and total size.
+
+  @param time_taken: The time taken so far
+  @param written: amount written so far
+  @param total_size: The total size of data to be written
+  @return: The remaining time in seconds
+
+  """
+  avg_time = time_taken / float(written)
+  return (total_size - written) * avg_time
+
+
+def _WipeDisks(lu, instance):
+  """Wipes instance disks.
+
+  @type lu: L{LogicalUnit}
+  @param lu: the logical unit on whose behalf we execute
+  @type instance: L{objects.Instance}
+  @param instance: the instance whose disks we should create
+  @return: the success of the wipe
+
+  """
+  node = instance.primary_node
+  for idx, device in enumerate(instance.disks):
+    lu.LogInfo("* Wiping disk %d", idx)
+    logging.info("Wiping disk %d for instance %s", idx, instance.name)
+
+    # The wipe size is MIN_WIPE_CHUNK_PERCENT % of the instance disk but
+    # MAX_WIPE_CHUNK at max
+    wipe_chunk_size = min(constants.MAX_WIPE_CHUNK, device.size / 100.0 *
+                          constants.MIN_WIPE_CHUNK_PERCENT)
+
+    offset = 0
+    size = device.size
+    last_output = 0
+    start_time = time.time()
+
+    while offset < size:
+      wipe_size = min(wipe_chunk_size, size - offset)
+      result = lu.rpc.call_blockdev_wipe(node, device, offset, wipe_size)
+      result.Raise("Could not wipe disk %d at offset %d for size %d" %
+                   (idx, offset, wipe_size))
+      now = time.time()
+      offset += wipe_size
+      if now - last_output >= 60:
+        eta = _CalcEta(now - start_time, offset, size)
+        lu.LogInfo(" - done: %.1f%% ETA: %s" %
+                   (offset / float(size) * 100, utils.FormatSeconds(eta)))
+        last_output = now
+
+
 def _CreateDisks(lu, instance, to_skip=None, target_node=None):
   """Create all disks for an instance.
 
@@ -6574,32 +6820,32 @@ class LUCreateInstance(LogicalUnit):
   HTYPE = constants.HTYPE_INSTANCE
   _OP_PARAMS = [
     _PInstanceName,
-    ("mode", _NoDefault, _TElemOf(constants.INSTANCE_CREATE_MODES)),
-    ("start", True, _TBool),
-    ("wait_for_sync", True, _TBool),
-    ("ip_check", True, _TBool),
-    ("name_check", True, _TBool),
-    ("disks", _NoDefault, _TListOf(_TDict)),
-    ("nics", _NoDefault, _TListOf(_TDict)),
-    ("hvparams", _EmptyDict, _TDict),
-    ("beparams", _EmptyDict, _TDict),
-    ("osparams", _EmptyDict, _TDict),
-    ("no_install", None, _TMaybeBool),
-    ("os_type", None, _TMaybeString),
-    ("force_variant", False, _TBool),
-    ("source_handshake", None, _TOr(_TList, _TNone)),
-    ("source_x509_ca", None, _TMaybeString),
-    ("source_instance_name", None, _TMaybeString),
-    ("src_node", None, _TMaybeString),
-    ("src_path", None, _TMaybeString),
-    ("pnode", None, _TMaybeString),
-    ("snode", None, _TMaybeString),
-    ("iallocator", None, _TMaybeString),
-    ("hypervisor", None, _TMaybeString),
-    ("disk_template", _NoDefault, _CheckDiskTemplate),
-    ("identify_defaults", False, _TBool),
-    ("file_driver", None, _TOr(_TNone, _TElemOf(constants.FILE_DRIVER))),
-    ("file_storage_dir", None, _TMaybeString),
+    ("mode", ht.NoDefault, ht.TElemOf(constants.INSTANCE_CREATE_MODES)),
+    ("start", True, ht.TBool),
+    ("wait_for_sync", True, ht.TBool),
+    ("ip_check", True, ht.TBool),
+    ("name_check", True, ht.TBool),
+    ("disks", ht.NoDefault, ht.TListOf(ht.TDict)),
+    ("nics", ht.NoDefault, ht.TListOf(ht.TDict)),
+    ("hvparams", ht.EmptyDict, ht.TDict),
+    ("beparams", ht.EmptyDict, ht.TDict),
+    ("osparams", ht.EmptyDict, ht.TDict),
+    ("no_install", None, ht.TMaybeBool),
+    ("os_type", None, ht.TMaybeString),
+    ("force_variant", False, ht.TBool),
+    ("source_handshake", None, ht.TOr(ht.TList, ht.TNone)),
+    ("source_x509_ca", None, ht.TMaybeString),
+    ("source_instance_name", None, ht.TMaybeString),
+    ("src_node", None, ht.TMaybeString),
+    ("src_path", None, ht.TMaybeString),
+    ("pnode", None, ht.TMaybeString),
+    ("snode", None, ht.TMaybeString),
+    ("iallocator", None, ht.TMaybeString),
+    ("hypervisor", None, ht.TMaybeString),
+    ("disk_template", ht.NoDefault, _CheckDiskTemplate),
+    ("identify_defaults", False, ht.TBool),
+    ("file_driver", None, ht.TOr(ht.TNone, ht.TElemOf(constants.FILE_DRIVER))),
+    ("file_storage_dir", None, ht.TMaybeString),
     ]
   REQ_BGL = False
 
@@ -6614,7 +6860,7 @@ class LUCreateInstance(LogicalUnit):
       self.op.start = False
     # validate/normalize the instance name
     self.op.instance_name = \
-      netutils.HostInfo.NormalizeName(self.op.instance_name)
+      netutils.Hostname.GetNormalizedName(self.op.instance_name)
 
     if self.op.ip_check and not self.op.name_check:
       # TODO: make the ip check more flexible and not depend on the name check
@@ -6653,7 +6899,7 @@ class LUCreateInstance(LogicalUnit):
 
     # instance name verification
     if self.op.name_check:
-      self.hostname1 = netutils.GetHostInfo(self.op.instance_name)
+      self.hostname1 = netutils.GetHostname(name=self.op.instance_name)
       self.op.instance_name = self.hostname1.name
       # used in CheckPrereq for ip ping check
       self.check_ip = self.hostname1.ip
@@ -6744,8 +6990,8 @@ class LUCreateInstance(LogicalUnit):
         raise errors.OpPrereqError("Missing source instance name",
                                    errors.ECODE_INVAL)
 
-      norm_name = netutils.HostInfo.NormalizeName(src_instance_name)
-      self.source_instance_name = netutils.GetHostInfo(norm_name).name
+      self.source_instance_name = \
+          netutils.GetHostname(name=src_instance_name).name
 
     else:
       raise errors.OpPrereqError("Invalid instance creation mode %r" %
@@ -7085,13 +7331,12 @@ class LUCreateInstance(LogicalUnit):
       elif ip.lower() == constants.VALUE_AUTO:
         if not self.op.name_check:
           raise errors.OpPrereqError("IP address set to auto but name checks"
-                                     " have been skipped. Aborting.",
+                                     " have been skipped",
                                      errors.ECODE_INVAL)
         nic_ip = self.hostname1.ip
       else:
-        if not netutils.IsValidIP4(ip):
-          raise errors.OpPrereqError("Given IP address '%s' doesn't look"
-                                     " like a valid IP" % ip,
+        if not netutils.IPAddress.IsValid(ip):
+          raise errors.OpPrereqError("Invalid IP address '%s'" % ip,
                                      errors.ECODE_INVAL)
         nic_ip = ip
 
@@ -7229,6 +7474,9 @@ class LUCreateInstance(LogicalUnit):
     if pnode.drained:
       raise errors.OpPrereqError("Cannot use drained primary node '%s'" %
                                  pnode.name, errors.ECODE_STATE)
+    if not pnode.vm_capable:
+      raise errors.OpPrereqError("Cannot use non-vm_capable primary node"
+                                 " '%s'" % pnode.name, errors.ECODE_STATE)
 
     self.secondaries = []
 
@@ -7239,6 +7487,7 @@ class LUCreateInstance(LogicalUnit):
                                    " primary node.", errors.ECODE_INVAL)
       _CheckNodeOnline(self, self.op.snode)
       _CheckNodeNotDrained(self, self.op.snode)
+      _CheckNodeVmCapable(self, self.op.snode)
       self.secondaries.append(self.op.snode)
 
     nodenames = [pnode.name] + self.secondaries
@@ -7368,6 +7617,18 @@ class LUCreateInstance(LogicalUnit):
           self.cfg.ReleaseDRBDMinors(instance)
           raise
 
+      if self.cfg.GetClusterInfo().prealloc_wipe_disks:
+        feedback_fn("* wiping instance disks...")
+        try:
+          _WipeDisks(self, iobj)
+        except errors.OpExecError:
+          self.LogWarning("Device wiping failed, reverting...")
+          try:
+            _RemoveDisks(self, iobj)
+          finally:
+            self.cfg.ReleaseDRBDMinors(instance)
+            raise
+
     feedback_fn("adding instance %s to cluster config" % instance)
 
     self.cfg.AddInstance(iobj, self.proc.GetECId())
@@ -7519,7 +7780,12 @@ class LUConnectConsole(NoHooksLU):
     node_insts.Raise("Can't get node information from %s" % node)
 
     if instance.name not in node_insts.payload:
-      raise errors.OpExecError("Instance %s is not running." % instance.name)
+      if instance.admin_up:
+        state = "ERROR_down"
+      else:
+        state = "ADMIN_down"
+      raise errors.OpExecError("Instance %s is not running (state %s)" %
+                               (instance.name, state))
 
     logging.debug("Connecting to console of %s on %s", instance.name, node)
 
@@ -7543,11 +7809,11 @@ class LUReplaceDisks(LogicalUnit):
   HTYPE = constants.HTYPE_INSTANCE
   _OP_PARAMS = [
     _PInstanceName,
-    ("mode", _NoDefault, _TElemOf(constants.REPLACE_MODES)),
-    ("disks", _EmptyList, _TListOf(_TPositiveInt)),
-    ("remote_node", None, _TMaybeString),
-    ("iallocator", None, _TMaybeString),
-    ("early_release", False, _TBool),
+    ("mode", ht.NoDefault, ht.TElemOf(constants.REPLACE_MODES)),
+    ("disks", ht.EmptyList, ht.TListOf(ht.TPositiveInt)),
+    ("remote_node", None, ht.TMaybeString),
+    ("iallocator", None, ht.TMaybeString),
+    ("early_release", False, ht.TBool),
     ]
   REQ_BGL = False
 
@@ -7803,6 +8069,7 @@ class TLReplaceDisks(Tasklet):
         check_nodes = [self.new_node, self.other_node]
 
         _CheckNodeNotDrained(self.lu, remote_node)
+        _CheckNodeVmCapable(self.lu, remote_node)
 
         old_node_info = self.cfg.GetNodeInfo(secondary_node)
         assert old_node_info is not None
@@ -8286,9 +8553,9 @@ class LURepairNodeStorage(NoHooksLU):
   """
   _OP_PARAMS = [
     _PNodeName,
-    ("storage_type", _NoDefault, _CheckStorageType),
-    ("name", _NoDefault, _TNonEmptyString),
-    ("ignore_consistency", False, _TBool),
+    ("storage_type", ht.NoDefault, _CheckStorageType),
+    ("name", ht.NoDefault, ht.TNonEmptyString),
+    ("ignore_consistency", False, ht.TBool),
     ]
   REQ_BGL = False
 
@@ -8353,9 +8620,9 @@ class LUNodeEvacuationStrategy(NoHooksLU):
 
   """
   _OP_PARAMS = [
-    ("nodes", _NoDefault, _TListOf(_TNonEmptyString)),
-    ("remote_node", None, _TMaybeString),
-    ("iallocator", None, _TMaybeString),
+    ("nodes", ht.NoDefault, ht.TListOf(ht.TNonEmptyString)),
+    ("remote_node", None, ht.TMaybeString),
+    ("iallocator", None, ht.TMaybeString),
     ]
   REQ_BGL = False
 
@@ -8405,9 +8672,9 @@ class LUGrowDisk(LogicalUnit):
   HTYPE = constants.HTYPE_INSTANCE
   _OP_PARAMS = [
     _PInstanceName,
-    ("disk", _NoDefault, _TInt),
-    ("amount", _NoDefault, _TInt),
-    ("wait_for_sync", True, _TBool),
+    ("disk", ht.NoDefault, ht.TInt),
+    ("amount", ht.NoDefault, ht.TInt),
+    ("wait_for_sync", True, ht.TBool),
     ]
   REQ_BGL = False
 
@@ -8503,8 +8770,8 @@ class LUQueryInstanceData(NoHooksLU):
 
   """
   _OP_PARAMS = [
-    ("instances", _EmptyList, _TListOf(_TNonEmptyString)),
-    ("static", False, _TBool),
+    ("instances", ht.EmptyList, ht.TListOf(ht.TNonEmptyString)),
+    ("static", False, ht.TBool),
     ]
   REQ_BGL = False
 
@@ -8664,15 +8931,15 @@ class LUSetInstanceParams(LogicalUnit):
   HTYPE = constants.HTYPE_INSTANCE
   _OP_PARAMS = [
     _PInstanceName,
-    ("nics", _EmptyList, _TList),
-    ("disks", _EmptyList, _TList),
-    ("beparams", _EmptyDict, _TDict),
-    ("hvparams", _EmptyDict, _TDict),
-    ("disk_template", None, _TMaybeString),
-    ("remote_node", None, _TMaybeString),
-    ("os_name", None, _TMaybeString),
-    ("force_variant", False, _TBool),
-    ("osparams", None, _TOr(_TDict, _TNone)),
+    ("nics", ht.EmptyList, ht.TList),
+    ("disks", ht.EmptyList, ht.TList),
+    ("beparams", ht.EmptyDict, ht.TDict),
+    ("hvparams", ht.EmptyDict, ht.TDict),
+    ("disk_template", None, ht.TMaybeString),
+    ("remote_node", None, ht.TMaybeString),
+    ("os_name", None, ht.TMaybeString),
+    ("force_variant", False, ht.TBool),
+    ("osparams", None, ht.TOr(ht.TDict, ht.TNone)),
     _PForce,
     ]
   REQ_BGL = False
@@ -8761,7 +9028,7 @@ class LUSetInstanceParams(LogicalUnit):
         if nic_ip.lower() == constants.VALUE_NONE:
           nic_dict['ip'] = None
         else:
-          if not netutils.IsValidIP4(nic_ip):
+          if not netutils.IPAddress.IsValid(nic_ip):
             raise errors.OpPrereqError("Invalid IP address '%s'" % nic_ip,
                                        errors.ECODE_INVAL)
 
@@ -8934,10 +9201,9 @@ class LUSetInstanceParams(LogicalUnit):
     if self.op.osparams:
       i_osdict = _GetUpdatedParams(instance.osparams, self.op.osparams)
       _CheckOSParams(self, True, nodelist, instance_os, i_osdict)
-      self.os_new = cluster.SimpleFillOS(instance_os, i_osdict)
       self.os_inst = i_osdict # the new dict (without defaults)
     else:
-      self.os_new = self.os_inst = {}
+      self.os_inst = {}
 
     self.warn = []
 
@@ -9329,8 +9595,8 @@ class LUQueryExports(NoHooksLU):
 
   """
   _OP_PARAMS = [
-    ("nodes", _EmptyList, _TListOf(_TNonEmptyString)),
-    ("use_locking", False, _TBool),
+    ("nodes", ht.EmptyList, ht.TListOf(ht.TNonEmptyString)),
+    ("use_locking", False, ht.TBool),
     ]
   REQ_BGL = False
 
@@ -9370,7 +9636,7 @@ class LUPrepareExport(NoHooksLU):
   """
   _OP_PARAMS = [
     _PInstanceName,
-    ("mode", _NoDefault, _TElemOf(constants.EXPORT_MODES)),
+    ("mode", ht.NoDefault, ht.TElemOf(constants.EXPORT_MODES)),
     ]
   REQ_BGL = False
 
@@ -9427,14 +9693,14 @@ class LUExportInstance(LogicalUnit):
   HTYPE = constants.HTYPE_INSTANCE
   _OP_PARAMS = [
     _PInstanceName,
-    ("target_node", _NoDefault, _TOr(_TNonEmptyString, _TList)),
-    ("shutdown", True, _TBool),
+    ("target_node", ht.NoDefault, ht.TOr(ht.TNonEmptyString, ht.TList)),
+    ("shutdown", True, ht.TBool),
     _PShutdownTimeout,
-    ("remove_instance", False, _TBool),
-    ("ignore_remove_failures", False, _TBool),
-    ("mode", constants.EXPORT_MODE_LOCAL, _TElemOf(constants.EXPORT_MODES)),
-    ("x509_key_name", None, _TOr(_TList, _TNone)),
-    ("destination_x509_ca", None, _TMaybeString),
+    ("remove_instance", False, ht.TBool),
+    ("ignore_remove_failures", False, ht.TBool),
+    ("mode", constants.EXPORT_MODE_LOCAL, ht.TElemOf(constants.EXPORT_MODES)),
+    ("x509_key_name", None, ht.TOr(ht.TList, ht.TNone)),
+    ("destination_x509_ca", None, ht.TMaybeString),
     ]
   REQ_BGL = False
 
@@ -9445,10 +9711,6 @@ class LUExportInstance(LogicalUnit):
     self.x509_key_name = self.op.x509_key_name
     self.dest_x509_ca_pem = self.op.destination_x509_ca
 
-    if self.op.remove_instance and not self.op.shutdown:
-      raise errors.OpPrereqError("Can not remove instance without shutting it"
-                                 " down before")
-
     if self.op.mode == constants.EXPORT_MODE_REMOTE:
       if not self.x509_key_name:
         raise errors.OpPrereqError("Missing X509 key name for encryption",
@@ -9514,6 +9776,11 @@ class LUExportInstance(LogicalUnit):
           "Cannot retrieve locked instance %s" % self.op.instance_name
     _CheckNodeOnline(self, self.instance.primary_node)
 
+    if (self.op.remove_instance and self.instance.admin_up and
+        not self.op.shutdown):
+      raise errors.OpPrereqError("Can not remove instance without shutting it"
+                                 " down before")
+
     if self.op.mode == constants.EXPORT_MODE_LOCAL:
       self.op.target_node = _ExpandNodeName(self.cfg, self.op.target_node)
       self.dst_node = self.cfg.GetNodeInfo(self.op.target_node)
@@ -9806,9 +10073,9 @@ class LUGetTags(TagsLU):
 
   """
   _OP_PARAMS = [
-    ("kind", _NoDefault, _TElemOf(constants.VALID_TAG_TYPES)),
+    ("kind", ht.NoDefault, ht.TElemOf(constants.VALID_TAG_TYPES)),
     # Name is only meaningful for nodes and instances
-    ("name", _NoDefault, _TMaybeString),
+    ("name", ht.NoDefault, ht.TMaybeString),
     ]
   REQ_BGL = False
 
@@ -9830,7 +10097,7 @@ class LUSearchTags(NoHooksLU):
 
   """
   _OP_PARAMS = [
-    ("pattern", _NoDefault, _TNonEmptyString),
+    ("pattern", ht.NoDefault, ht.TNonEmptyString),
     ]
   REQ_BGL = False
 
@@ -9872,10 +10139,10 @@ class LUAddTags(TagsLU):
 
   """
   _OP_PARAMS = [
-    ("kind", _NoDefault, _TElemOf(constants.VALID_TAG_TYPES)),
+    ("kind", ht.NoDefault, ht.TElemOf(constants.VALID_TAG_TYPES)),
     # Name is only meaningful for nodes and instances
-    ("name", _NoDefault, _TMaybeString),
-    ("tags", _NoDefault, _TListOf(_TNonEmptyString)),
+    ("name", ht.NoDefault, ht.TMaybeString),
+    ("tags", ht.NoDefault, ht.TListOf(ht.TNonEmptyString)),
     ]
   REQ_BGL = False
 
@@ -9906,10 +10173,10 @@ class LUDelTags(TagsLU):
 
   """
   _OP_PARAMS = [
-    ("kind", _NoDefault, _TElemOf(constants.VALID_TAG_TYPES)),
+    ("kind", ht.NoDefault, ht.TElemOf(constants.VALID_TAG_TYPES)),
     # Name is only meaningful for nodes and instances
-    ("name", _NoDefault, _TMaybeString),
-    ("tags", _NoDefault, _TListOf(_TNonEmptyString)),
+    ("name", ht.NoDefault, ht.TMaybeString),
+    ("tags", ht.NoDefault, ht.TListOf(ht.TNonEmptyString)),
     ]
   REQ_BGL = False
 
@@ -9949,10 +10216,10 @@ class LUTestDelay(NoHooksLU):
 
   """
   _OP_PARAMS = [
-    ("duration", _NoDefault, _TFloat),
-    ("on_master", True, _TBool),
-    ("on_nodes", _EmptyList, _TListOf(_TNonEmptyString)),
-    ("repeat", 0, _TPositiveInt)
+    ("duration", ht.NoDefault, ht.TFloat),
+    ("on_master", True, ht.TBool),
+    ("on_nodes", ht.EmptyList, ht.TListOf(ht.TNonEmptyString)),
+    ("repeat", 0, ht.TPositiveInt)
     ]
   REQ_BGL = False
 
@@ -10000,10 +10267,10 @@ class LUTestJobqueue(NoHooksLU):
 
   """
   _OP_PARAMS = [
-    ("notify_waitlock", False, _TBool),
-    ("notify_exec", False, _TBool),
-    ("log_messages", _EmptyList, _TListOf(_TString)),
-    ("fail", False, _TBool),
+    ("notify_waitlock", False, ht.TBool),
+    ("notify_exec", False, ht.TBool),
+    ("log_messages", ht.EmptyList, ht.TListOf(ht.TString)),
+    ("fail", False, ht.TBool),
     ]
   REQ_BGL = False
 
@@ -10224,7 +10491,6 @@ class IAllocator(object):
     i_list = [(inst, cluster_info.FillBE(inst)) for inst in iinfo]
 
     # node data
-    node_results = {}
     node_list = cfg.GetNodeList()
 
     if self.mode == constants.IALLOCATOR_MODE_ALLOC:
@@ -10239,6 +10505,31 @@ class IAllocator(object):
     node_iinfo = \
       self.rpc.call_all_instances_info(node_list,
                                        cluster_info.enabled_hypervisors)
+
+    data["nodegroups"] = self._ComputeNodeGroupData(cfg)
+
+    data["nodes"] = self._ComputeNodeData(cfg, node_data, node_iinfo, i_list)
+
+    data["instances"] = self._ComputeInstanceData(cluster_info, i_list)
+
+    self.in_data = data
+
+  @staticmethod
+  def _ComputeNodeGroupData(cfg):
+    """Compute node groups data.
+
+    """
+    ng = {}
+    for guuid, gdata in cfg.GetAllNodeGroupsInfo().items():
+      ng[guuid] = { "name": gdata.name }
+    return ng
+
+  @staticmethod
+  def _ComputeNodeData(cfg, node_data, node_iinfo, i_list):
+    """Compute global node data.
+
+    """
+    node_results = {}
     for nname, nresult in node_data.items():
       # first fill in static (config-based) values
       ninfo = cfg.GetNodeInfo(nname)
@@ -10249,6 +10540,9 @@ class IAllocator(object):
         "offline": ninfo.offline,
         "drained": ninfo.drained,
         "master_candidate": ninfo.master_candidate,
+        "group": ninfo.group,
+        "master_capable": ninfo.master_capable,
+        "vm_capable": ninfo.vm_capable,
         }
 
       if not (ninfo.offline or ninfo.drained):
@@ -10295,9 +10589,14 @@ class IAllocator(object):
         pnr.update(pnr_dyn)
 
       node_results[nname] = pnr
-    data["nodes"] = node_results
 
-    # instance data
+    return node_results
+
+  @staticmethod
+  def _ComputeInstanceData(cluster_info, i_list):
+    """Compute global instance data.
+
+    """
     instance_data = {}
     for iinfo, beinfo in i_list:
       nic_data = []
@@ -10327,9 +10626,7 @@ class IAllocator(object):
                                                  pir["disks"])
       instance_data[iinfo.name] = pir
 
-    data["instances"] = instance_data
-
-    self.in_data = data
+    return instance_data
 
   def _AddNewInstance(self):
     """Add new instance data to allocator structure.
@@ -10470,21 +10767,22 @@ class LUTestAllocator(NoHooksLU):
 
   """
   _OP_PARAMS = [
-    ("direction", _NoDefault, _TElemOf(constants.VALID_IALLOCATOR_DIRECTIONS)),
-    ("mode", _NoDefault, _TElemOf(constants.VALID_IALLOCATOR_MODES)),
-    ("name", _NoDefault, _TNonEmptyString),
-    ("nics", _NoDefault, _TOr(_TNone, _TListOf(
-      _TDictOf(_TElemOf(["mac", "ip", "bridge"]),
-               _TOr(_TNone, _TNonEmptyString))))),
-    ("disks", _NoDefault, _TOr(_TNone, _TList)),
-    ("hypervisor", None, _TMaybeString),
-    ("allocator", None, _TMaybeString),
-    ("tags", _EmptyList, _TListOf(_TNonEmptyString)),
-    ("mem_size", None, _TOr(_TNone, _TPositiveInt)),
-    ("vcpus", None, _TOr(_TNone, _TPositiveInt)),
-    ("os", None, _TMaybeString),
-    ("disk_template", None, _TMaybeString),
-    ("evac_nodes", None, _TOr(_TNone, _TListOf(_TNonEmptyString))),
+    ("direction", ht.NoDefault,
+     ht.TElemOf(constants.VALID_IALLOCATOR_DIRECTIONS)),
+    ("mode", ht.NoDefault, ht.TElemOf(constants.VALID_IALLOCATOR_MODES)),
+    ("name", ht.NoDefault, ht.TNonEmptyString),
+    ("nics", ht.NoDefault, ht.TOr(ht.TNone, ht.TListOf(
+      ht.TDictOf(ht.TElemOf(["mac", "ip", "bridge"]),
+               ht.TOr(ht.TNone, ht.TNonEmptyString))))),
+    ("disks", ht.NoDefault, ht.TOr(ht.TNone, ht.TList)),
+    ("hypervisor", None, ht.TMaybeString),
+    ("allocator", None, ht.TMaybeString),
+    ("tags", ht.EmptyList, ht.TListOf(ht.TNonEmptyString)),
+    ("mem_size", None, ht.TOr(ht.TNone, ht.TPositiveInt)),
+    ("vcpus", None, ht.TOr(ht.TNone, ht.TPositiveInt)),
+    ("os", None, ht.TMaybeString),
+    ("disk_template", None, ht.TMaybeString),
+    ("evac_nodes", None, ht.TOr(ht.TNone, ht.TListOf(ht.TNonEmptyString))),
     ]
 
   def CheckPrereq(self):
index af25c68..2ca2ed8 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2009 Google Inc.
+# Copyright (C) 2009, 2010 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
@@ -391,11 +391,11 @@ class ConfdClient:
       raise errors.ConfdClientError("Peer list empty")
     try:
       peer = self._peers[0]
-      self._family = netutils.GetAddressFamily(peer)
+      self._family = netutils.IPAddress.GetAddressFamily(peer)
       for peer in self._peers[1:]:
-        if netutils.GetAddressFamily(peer) != self._family:
+        if netutils.IPAddress.GetAddressFamily(peer) != self._family:
           raise errors.ConfdClientError("Peers must be of same address family")
-    except errors.GenericError:
+    except errors.IPAddressError:
       raise errors.ConfdClientError("Peer address %s invalid" % peer)
 
 
index 1435ffa..eb57cae 100644 (file)
@@ -31,6 +31,9 @@ much memory.
 
 """
 
+# pylint: disable-msg=R0904
+# R0904: Too many public methods
+
 import os
 import random
 import logging
@@ -45,6 +48,7 @@ from ganeti import objects
 from ganeti import serializer
 from ganeti import uidpool
 from ganeti import netutils
+from ganeti import runtime
 
 
 _config_lock = locking.SharedLock("ConfigWriter")
@@ -63,10 +67,7 @@ def _ValidateConfig(data):
 
   """
   if data.version != constants.CONFIG_VERSION:
-    raise errors.ConfigurationError("Cluster configuration version"
-                                    " mismatch, got %s instead of %s" %
-                                    (data.version,
-                                     constants.CONFIG_VERSION))
+    raise errors.ConfigVersionMismatch(constants.CONFIG_VERSION, data.version)
 
 
 class TemporaryReservationManager:
@@ -80,15 +81,15 @@ class TemporaryReservationManager:
     self._ec_reserved = {}
 
   def Reserved(self, resource):
-    for holder_reserved in self._ec_reserved.items():
+    for holder_reserved in self._ec_reserved.values():
       if resource in holder_reserved:
         return True
     return False
 
   def Reserve(self, ec_id, resource):
     if self.Reserved(resource):
-      raise errors.ReservationError("Duplicate reservation for resource: %s." %
-                                    (resource))
+      raise errors.ReservationError("Duplicate reservation for resource '%s'"
+                                    % str(resource))
     if ec_id not in self._ec_reserved:
       self._ec_reserved[ec_id] = set([resource])
     else:
@@ -131,7 +132,8 @@ class ConfigWriter:
   @ivar _all_rms: a list of all temporary reservation managers
 
   """
-  def __init__(self, cfg_file=None, offline=False):
+  def __init__(self, cfg_file=None, offline=False, _getents=runtime.GetEnts,
+               accept_foreign=False):
     self.write_count = 0
     self._lock = _config_lock
     self._config_data = None
@@ -140,6 +142,7 @@ class ConfigWriter:
       self._cfg_file = constants.CLUSTER_CONF_FILE
     else:
       self._cfg_file = cfg_file
+    self._getents = _getents
     self._temporary_ids = TemporaryReservationManager()
     self._temporary_drbds = {}
     self._temporary_macs = TemporaryReservationManager()
@@ -151,9 +154,10 @@ class ConfigWriter:
     # _DistributeConfig, we compute it here once and reuse it; it's
     # better to raise an error before starting to modify the config
     # file than after it was modified
-    self._my_hostname = netutils.HostInfo().name
+    self._my_hostname = netutils.Hostname.GetSysName()
     self._last_cluster_serial = -1
-    self._OpenConfig()
+    self._cfg_id = None
+    self._OpenConfig(accept_foreign)
 
   # this method needs to be static, so that we can call it on the class
   @staticmethod
@@ -456,6 +460,21 @@ class ConfigWriter:
                       (node.name, node.master_candidate, node.drained,
                        node.offline))
 
+    # nodegroups checks
+    nodegroups_names = set()
+    for nodegroup_uuid in data.nodegroups:
+      nodegroup = data.nodegroups[nodegroup_uuid]
+      if nodegroup.uuid != nodegroup_uuid:
+        result.append("nodegroup '%s' (uuid: '%s') indexed by wrong uuid '%s'"
+                      % (nodegroup.name, nodegroup.uuid, nodegroup_uuid))
+      if utils.UUID_RE.match(nodegroup.name.lower()):
+        result.append("nodegroup '%s' (uuid: '%s') has uuid-like name" %
+                      (nodegroup.name, nodegroup.uuid))
+      if nodegroup.name in nodegroups_names:
+        result.append("duplicate nodegroup name '%s'" % nodegroup.name)
+      else:
+        nodegroups_names.add(nodegroup.name)
+
     # drbd minors check
     _, duplicates = self._UnlockedComputeDRBDMap()
     for node, minor, instance_a, instance_b in duplicates:
@@ -824,6 +843,68 @@ class ConfigWriter:
     """
     return self._config_data.cluster.default_iallocator
 
+  @locking.ssynchronized(_config_lock, shared=1)
+  def GetPrimaryIPFamily(self):
+    """Get cluster primary ip family.
+
+    @return: primary ip family
+
+    """
+    return self._config_data.cluster.primary_ip_family
+
+  @locking.ssynchronized(_config_lock, shared=1)
+  def LookupNodeGroup(self, target):
+    """Lookup a node group's UUID.
+
+    @type target: string or None
+    @param target: group name or UUID or None to look for the default
+    @rtype: string
+    @return: nodegroup UUID
+    @raises errors.OpPrereqError: when the target group cannot be found
+
+    """
+    if target is None:
+      if len(self._config_data.nodegroups) != 1:
+        raise errors.OpPrereqError("More than one nodegroup exists. Target"
+                                   " group must be specified explicitely.")
+      else:
+        return self._config_data.nodegroups.keys()[0]
+    if target in self._config_data.nodegroups:
+      return target
+    for nodegroup in self._config_data.nodegroups.values():
+      if nodegroup.name == target:
+        return nodegroup.uuid
+    raise errors.OpPrereqError("Nodegroup '%s' not found" % target)
+
+  @locking.ssynchronized(_config_lock, shared=1)
+  def GetNodeGroup(self, uuid):
+    """Lookup a node group.
+
+    @type uuid: string
+    @param uuid: group UUID
+    @rtype: L{objects.NodeGroup} or None
+    @return: nodegroup object, or None if not found
+
+    """
+    if uuid not in self._config_data.nodegroups:
+      return None
+
+    return self._config_data.nodegroups[uuid]
+
+  @locking.ssynchronized(_config_lock, shared=1)
+  def GetAllNodeGroupsInfo(self):
+    """Get the configuration of all node groups.
+
+    """
+    return dict(self._config_data.nodegroups)
+
+  @locking.ssynchronized(_config_lock, shared=1)
+  def GetNodeGroupList(self):
+    """Get a list of node groups.
+
+    """
+    return self._config_data.nodegroups.keys()
+
   @locking.ssynchronized(_config_lock)
   def AddInstance(self, instance, ec_id):
     """Add an instance to the config.
@@ -929,6 +1010,9 @@ class ConfigWriter:
                                                              inst.name,
                                                              disk.iv_name))
 
+    # Force update of ssconf files
+    self._config_data.cluster.serial_no += 1
+
     self._config_data.instances[inst.name] = inst
     self._WriteConfig()
 
@@ -1020,6 +1104,7 @@ class ConfigWriter:
 
     node.serial_no = 1
     node.ctime = node.mtime = time.time()
+    self._UnlockedAddNodeToGroup(node.name, node.group)
     self._config_data.nodes[node.name] = node
     self._config_data.cluster.serial_no += 1
     self._WriteConfig()
@@ -1034,6 +1119,7 @@ class ConfigWriter:
     if node_name not in self._config_data.nodes:
       raise errors.ConfigurationError("Unknown node '%s'" % node_name)
 
+    self._UnlockedRemoveNodeFromGroup(self._config_data.nodes[node_name])
     del self._config_data.nodes[node_name]
     self._config_data.cluster.serial_no += 1
     self._WriteConfig()
@@ -1078,6 +1164,25 @@ class ConfigWriter:
     """
     return self._UnlockedGetNodeInfo(node_name)
 
+  @locking.ssynchronized(_config_lock, shared=1)
+  def GetNodeInstances(self, node_name):
+    """Get the instances of a node, as stored in the config.
+
+    @param node_name: the node name, e.g. I{node1.example.com}
+
+    @rtype: (list, list)
+    @return: a tuple with two lists: the primary and the secondary instances
+
+    """
+    pri = []
+    sec = []
+    for inst in self._config_data.instances.values():
+      if inst.primary_node == node_name:
+        pri.append(inst.name)
+      if node_name in inst.secondary_nodes:
+        sec.append(inst.name)
+    return (pri, sec)
+
   def _UnlockedGetNodeList(self):
     """Return the list of nodes which are in the configuration.
 
@@ -1112,6 +1217,15 @@ class ConfigWriter:
     return self._UnlockedGetOnlineNodeList()
 
   @locking.ssynchronized(_config_lock, shared=1)
+  def GetNonVmCapableNodeList(self):
+    """Return the list of nodes which are not vm capable.
+
+    """
+    all_nodes = [self._UnlockedGetNodeInfo(node)
+                 for node in self._UnlockedGetNodeList()]
+    return [node.name for node in all_nodes if not node.vm_capable]
+
+  @locking.ssynchronized(_config_lock, shared=1)
   def GetAllNodesInfo(self):
     """Get the configuration of all nodes.
 
@@ -1137,7 +1251,7 @@ class ConfigWriter:
     for node in self._config_data.nodes.values():
       if exceptions and node.name in exceptions:
         continue
-      if not (node.offline or node.drained):
+      if not (node.offline or node.drained) and node.master_capable:
         mc_max += 1
       if node.master_candidate:
         mc_now += 1
@@ -1178,7 +1292,7 @@ class ConfigWriter:
           break
         node = self._config_data.nodes[name]
         if (node.master_candidate or node.offline or node.drained or
-            node.name in exceptions):
+            node.name in exceptions or not node.master_capable):
           continue
         mod_list.append(node)
         node.master_candidate = True
@@ -1194,6 +1308,34 @@ class ConfigWriter:
 
     return mod_list
 
+  def _UnlockedAddNodeToGroup(self, node_name, nodegroup_uuid):
+    """Add a given node to the specified group.
+
+    """
+    if nodegroup_uuid not in self._config_data.nodegroups:
+      # This can happen if a node group gets deleted between its lookup and
+      # when we're adding the first node to it, since we don't keep a lock in
+      # the meantime. It's ok though, as we'll fail cleanly if the node group
+      # is not found anymore.
+      raise errors.OpExecError("Unknown node group: %s" % nodegroup_uuid)
+    if node_name not in self._config_data.nodegroups[nodegroup_uuid].members:
+      self._config_data.nodegroups[nodegroup_uuid].members.append(node_name)
+
+  def _UnlockedRemoveNodeFromGroup(self, node):
+    """Remove a given node from its group.
+
+    """
+    nodegroup = node.group
+    if nodegroup not in self._config_data.nodegroups:
+      logging.warning("Warning: node '%s' has unknown node group '%s'"
+                      " (while being removed from it)", node.name, nodegroup)
+    nodegroup_obj = self._config_data.nodegroups[nodegroup]
+    if node.name not in nodegroup_obj.members:
+      logging.warning("Warning: node '%s' not a member of its node group '%s'"
+                      " (while being removed from it)", node.name, nodegroup)
+    else:
+      nodegroup_obj.members.remove(node.name)
+
   def _BumpSerialNo(self):
     """Bump up the serial number of the config.
 
@@ -1207,9 +1349,10 @@ class ConfigWriter:
     """
     return (self._config_data.instances.values() +
             self._config_data.nodes.values() +
+            self._config_data.nodegroups.values() +
             [self._config_data.cluster])
 
-  def _OpenConfig(self):
+  def _OpenConfig(self, accept_foreign):
     """Read the config data from disk.
 
     """
@@ -1228,6 +1371,13 @@ class ConfigWriter:
       raise errors.ConfigurationError("Incomplete configuration"
                                       " (missing cluster.rsahostkeypub)")
 
+    if data.cluster.master_node != self._my_hostname and not accept_foreign:
+      msg = ("The configuration denotes node %s as master, while my"
+             " hostname is %s; opening a foreign configuration is only"
+             " possible in accept_foreign mode" %
+             (data.cluster.master_node, self._my_hostname))
+      raise errors.ConfigurationError(msg)
+
     # Upgrade configuration if needed
     data.UpgradeConfig()
 
@@ -1239,6 +1389,8 @@ class ConfigWriter:
     # And finally run our (custom) config upgrade sequence
     self._UpgradeConfig()
 
+    self._cfg_id = utils.GetFileID(path=self._cfg_file)
+
   def _UpgradeConfig(self):
     """Run upgrade steps that cannot be done purely in the objects.
 
@@ -1257,6 +1409,24 @@ class ConfigWriter:
       if item.uuid is None:
         item.uuid = self._GenerateUniqueID(_UPGRADE_CONFIG_JID)
         modified = True
+    if not self._config_data.nodegroups:
+      default_nodegroup_uuid = self._GenerateUniqueID(_UPGRADE_CONFIG_JID)
+      default_nodegroup = objects.NodeGroup(
+          uuid=default_nodegroup_uuid,
+          name="default",
+          members=[],
+          )
+      self._config_data.nodegroups[default_nodegroup_uuid] = default_nodegroup
+      modified = True
+    for node in self._config_data.nodes.values():
+      if not node.group:
+        node.group = self.LookupNodeGroup(None)
+        modified = True
+      # This is technically *not* an upgrade, but needs to be done both when
+      # nodegroups are being added, and upon normally loading the config,
+      # because the members list of a node group is discarded upon
+      # serializing/deserializing the object.
+      self._UnlockedAddNodeToGroup(node.name, node.group)
     if modified:
       self._WriteConfig()
       # This is ok even if it acquires the internal lock, as _UpgradeConfig is
@@ -1330,7 +1500,18 @@ class ConfigWriter:
     self._BumpSerialNo()
     txt = serializer.Dump(self._config_data.ToDict())
 
-    utils.WriteFile(destination, data=txt)
+    getents = self._getents()
+    try:
+      fd = utils.SafeWriteFile(destination, self._cfg_id, data=txt,
+                               close=False, gid=getents.confd_gid, mode=0640)
+    except errors.LockError:
+      raise errors.ConfigurationError("The configuration file has been"
+                                      " modified since the last write, cannot"
+                                      " update")
+    try:
+      self._cfg_id = utils.GetFileID(fd=fd)
+    finally:
+      os.close(fd)
 
     self.write_count += 1
 
@@ -1390,6 +1571,10 @@ class ConfigWriter:
 
     uid_pool = uidpool.FormatUidPool(cluster.uid_pool, separator="\n")
 
+    nodegroups = ["%s %s" % (nodegroup.uuid, nodegroup.name) for nodegroup in
+                  self._config_data.nodegroups.values()]
+    nodegroups_data = fn(utils.NiceSort(nodegroups))
+
     return {
       constants.SS_CLUSTER_NAME: cluster.cluster_name,
       constants.SS_CLUSTER_TAGS: cluster_tags,
@@ -1404,14 +1589,23 @@ class ConfigWriter:
       constants.SS_NODE_SECONDARY_IPS: node_snd_ips_data,
       constants.SS_OFFLINE_NODES: off_data,
       constants.SS_ONLINE_NODES: on_data,
+      constants.SS_PRIMARY_IP_FAMILY: str(cluster.primary_ip_family),
       constants.SS_INSTANCE_LIST: instance_data,
       constants.SS_RELEASE_VERSION: constants.RELEASE_VERSION,
       constants.SS_HYPERVISOR_LIST: hypervisor_list,
       constants.SS_MAINTAIN_NODE_HEALTH: str(cluster.maintain_node_health),
       constants.SS_UID_POOL: uid_pool,
+      constants.SS_NODEGROUPS: nodegroups_data,
       }
 
   @locking.ssynchronized(_config_lock, shared=1)
+  def GetSsconfValues(self):
+    """Wrapper using lock around _UnlockedGetSsconf().
+
+    """
+    return self._UnlockedGetSsconfValues()
+
+  @locking.ssynchronized(_config_lock, shared=1)
   def GetVGName(self):
     """Return the volume group name.
 
index 2941c8a..1935064 100644 (file)
@@ -86,8 +86,22 @@ CONFIG_VERSION = BuildVersion(CONFIG_MAJOR, CONFIG_MINOR, CONFIG_REVISION)
 
 # 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
+NODED_USER = _autoconf.NODED_USER
+
+
+# Wipe
+DD_CMD = "dd"
+WIPE_BLOCK_SIZE = 1024**2
+MAX_WIPE_CHUNK = 1024 # 1GB
+MIN_WIPE_CHUNK_PERCENT = 10
+
 
 # file paths
 DATA_DIR = _autoconf.LOCALSTATEDIR + "/lib/ganeti"
@@ -124,6 +138,7 @@ SSH_KNOWN_HOSTS_FILE = DATA_DIR + "/known_hosts"
 RAPI_USERS_FILE = DATA_DIR + "/rapi_users"
 QUEUE_DIR = DATA_DIR + "/queue"
 DAEMON_UTIL = _autoconf.PKGLIBDIR + "/daemon-util"
+SETUP_SSH = _autoconf.TOOLSDIR + "/setup-ssh"
 ETC_HOSTS = "/etc/hosts"
 DEFAULT_FILE_STORAGE_DIR = _autoconf.FILE_STORAGE_DIR
 ENABLE_FILE_STORAGE = _autoconf.ENABLE_FILE_STORAGE
@@ -184,6 +199,7 @@ PROC_MOUNTS = "/proc/mounts"
 
 # luxi related constants
 LUXI_EOM = "\3"
+LUXI_VERSION = CONFIG_VERSION
 
 # one of 'no', 'yes', 'only'
 SYSLOG_USAGE = _autoconf.SYSLOG_USAGE
@@ -451,6 +467,8 @@ 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
 TCP_PING_TIMEOUT = 10
 GANETI_RUNAS = "root"
 DEFAULT_VG = "xenvg"
@@ -768,6 +786,7 @@ NV_PVLIST = "pvlist"
 NV_TIME = "time"
 NV_VERSION = "version"
 NV_VGLIST = "vglist"
+NV_VMNODES = "vmnodes"
 
 # SSL certificate check constants (in days)
 SSL_CERT_EXPIRATION_WARN = 30
@@ -820,6 +839,12 @@ JOBS_FINALIZED = frozenset([
   JOB_STATUS_SUCCESS,
   JOB_STATUS_ERROR,
   ])
+JOB_STATUS_ALL = frozenset([
+  JOB_STATUS_QUEUED,
+  JOB_STATUS_WAITLOCK,
+  JOB_STATUS_CANCELING,
+  JOB_STATUS_RUNNING,
+  ]) | JOBS_FINALIZED
 
 # OpCode status
 # not yet finalized
@@ -835,12 +860,32 @@ OPS_FINALIZED = frozenset([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 = frozenset([
+  OP_PRIO_LOW,
+  OP_PRIO_NORMAL,
+  OP_PRIO_HIGH,
+  ])
+
+OP_PRIO_DEFAULT = OP_PRIO_NORMAL
+
 # Execution log types
 ELOG_MESSAGE = "message"
 ELOG_PROGRESS = "progress"
 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"
@@ -872,11 +917,13 @@ 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"
 
 # cluster wide default parameters
 DEFAULT_ENABLED_HYPERVISOR = HT_XEN_PVM
index 1fd1b74..c3c4761 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007, 2008 Google Inc.
+# Copyright (C) 2006, 2007, 2008, 2010 Google Inc.
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -25,9 +25,7 @@
 import asyncore
 import asynchat
 import collections
-import grp
 import os
-import pwd
 import signal
 import logging
 import sched
@@ -40,10 +38,8 @@ from ganeti import utils
 from ganeti import constants
 from ganeti import errors
 from ganeti import netutils
-
-
-_DEFAULT_RUN_USER = "root"
-_DEFAULT_RUN_GROUP = "root"
+from ganeti import ssconf
+from ganeti import runtime
 
 
 class SchedulerBreakout(Exception):
@@ -100,23 +96,6 @@ class GanetiBaseAsyncoreDispatcher(asyncore.dispatcher):
     return False
 
 
-def FormatAddress(family, address):
-  """Format a client's address
-
-  @type family: integer
-  @param family: socket family (one of socket.AF_*)
-  @type address: family specific (usually tuple)
-  @param address: address, as reported by this class
-
-  """
-  if family == socket.AF_INET and len(address) == 2:
-    return "%s:%d" % address
-  elif family == socket.AF_UNIX and len(address) == 3:
-    return "pid=%s, uid=%s, gid=%s" % address
-  else:
-    return str(address)
-
-
 class AsyncStreamServer(GanetiBaseAsyncoreDispatcher):
   """A stream server to use with asyncore.
 
@@ -159,7 +138,7 @@ class AsyncStreamServer(GanetiBaseAsyncoreDispatcher):
         # is passed in from accept anyway
         client_address = netutils.GetSocketCredentials(connected_socket)
       logging.info("Accepted connection from %s",
-                   FormatAddress(self.family, client_address))
+                   netutils.FormatAddress(client_address, family=self.family))
       self.handle_connection(connected_socket, client_address)
 
   def handle_connection(self, connected_socket, client_address):
@@ -290,7 +269,7 @@ class AsyncTerminatedMessageStream(asynchat.async_chat):
 
   def close_log(self):
     logging.info("Closing connection from %s",
-                 FormatAddress(self.family, self.peer_address))
+                 netutils.FormatAddress(self.peer_address, family=self.family))
     self.close()
 
   # this method is overriding an asyncore.dispatcher method
@@ -509,10 +488,61 @@ class Mainloop(object):
     self._signal_wait.append(owner)
 
 
-def GenericMain(daemon_name, optionparser, dirs, check_fn, exec_fn,
+def _VerifyDaemonUser(daemon_name):
+  """Verifies the process uid matches the configured uid.
+
+  This method verifies that a daemon is started as the user it is
+  intended to be run
+
+  @param daemon_name: The name of daemon to be started
+  @return: A tuple with the first item indicating success or not,
+           the second item current uid and third with expected uid
+
+  """
+  getents = runtime.GetEnts()
+  running_uid = os.getuid()
+  daemon_uids = {
+    constants.MASTERD: getents.masterd_uid,
+    constants.RAPI: getents.rapi_uid,
+    constants.NODED: getents.noded_uid,
+    constants.CONFD: getents.confd_uid,
+    }
+
+  return (daemon_uids[daemon_name] == running_uid, running_uid,
+          daemon_uids[daemon_name])
+
+
+def _BeautifyError(err):
+  """Try to format an error better.
+
+  Since we're dealing with daemon startup errors, in many cases this
+  will be due to socket error and such, so we try to format these cases better.
+
+  @param err: an exception object
+  @rtype: string
+  @return: the formatted error description
+
+  """
+  try:
+    if isinstance(err, socket.error):
+      return "Socket-related error: %s (errno=%s)" % (err.args[1], err.args[0])
+    elif isinstance(err, EnvironmentError):
+      if err.filename is None:
+        return "%s (errno=%s)" % (err.strerror, err.errno)
+      else:
+        return "%s (file %s) (errno=%s)" % (err.strerror, err.filename,
+                                            err.errno)
+    else:
+      return str(err)
+  except Exception: # pylint: disable-msg=W0703
+    logging.exception("Error while handling existing error %s", err)
+    return "%s" % str(err)
+
+
+def GenericMain(daemon_name, optionparser,
+                check_fn, prepare_fn, exec_fn,
                 multithreaded=False, console_logging=False,
-                default_ssl_cert=None, default_ssl_key=None,
-                user=_DEFAULT_RUN_USER, group=_DEFAULT_RUN_GROUP):
+                default_ssl_cert=None, default_ssl_key=None):
   """Shared main function for daemons.
 
   @type daemon_name: string
@@ -520,13 +550,14 @@ def GenericMain(daemon_name, optionparser, dirs, check_fn, exec_fn,
   @type optionparser: optparse.OptionParser
   @param optionparser: initialized optionparser with daemon-specific options
                        (common -f -d options will be handled by this module)
-  @type dirs: list of (string, integer)
-  @param dirs: list of directories that must be created if they don't exist,
-               and the permissions to be used to create them
   @type check_fn: function which accepts (options, args)
   @param check_fn: function that checks start conditions and exits if they're
                    not met
-  @type exec_fn: function which accepts (options, args)
+  @type prepare_fn: function which accepts (options, args)
+  @param prepare_fn: function that is run before forking, or None;
+      it's result will be passed as the third parameter to exec_fn, or
+      if None was passed in, we will just pass None to exec_fn
+  @type exec_fn: function which accepts (options, args, prepare_results)
   @param exec_fn: function that's executed with the daemon's pid file held, and
                   runs the daemon itself.
   @type multithreaded: bool
@@ -538,10 +569,6 @@ def GenericMain(daemon_name, optionparser, dirs, check_fn, exec_fn,
   @param default_ssl_cert: Default SSL certificate path
   @type default_ssl_key: string
   @param default_ssl_key: Default SSL key path
-  @param user: Default user to run as
-  @type user: string
-  @param group: Default group to run as
-  @type group: string
 
   """
   optionparser.add_option("-f", "--foreground", dest="fork",
@@ -559,6 +586,13 @@ def GenericMain(daemon_name, optionparser, dirs, check_fn, exec_fn,
 
   if daemon_name in constants.DAEMONS_PORTS:
     default_bind_address = constants.IP4_ADDRESS_ANY
+    family = ssconf.SimpleStore().GetPrimaryIPFamily()
+    # family will default to AF_INET if there is no ssconf file (e.g. when
+    # upgrading a cluster from 2.2 -> 2.3. This is intended, as Ganeti clusters
+    # <= 2.2 can not be AF_INET6
+    if family == netutils.IP6Address.family:
+      default_bind_address = constants.IP6_ADDRESS_ANY
+
     default_port = netutils.GetDaemonPort(daemon_name)
 
     # For networked daemons we allow choosing the port and bind address
@@ -566,7 +600,7 @@ def GenericMain(daemon_name, optionparser, dirs, check_fn, exec_fn,
                             help="Network port (default: %s)" % default_port,
                             default=default_port, type="int")
     optionparser.add_option("-b", "--bind", dest="bind_address",
-                            help=("Bind address (default: %s)" %
+                            help=("Bind address (default: '%s')" %
                                   default_bind_address),
                             default=default_bind_address, metavar="ADDRESS")
 
@@ -605,31 +639,46 @@ def GenericMain(daemon_name, optionparser, dirs, check_fn, exec_fn,
     # once and have a proper validation (isfile returns False on directories)
     # at the same time.
 
+  result, running_uid, expected_uid = _VerifyDaemonUser(daemon_name)
+  if not result:
+    msg = ("%s started using wrong user ID (%d), expected %d" %
+           (daemon_name, running_uid, expected_uid))
+    print >> sys.stderr, msg
+    sys.exit(constants.EXIT_FAILURE)
+
   if check_fn is not None:
     check_fn(options, args)
 
-  utils.EnsureDirs(dirs)
-
   if options.fork:
-    try:
-      uid = pwd.getpwnam(user).pw_uid
-      gid = grp.getgrnam(group).gr_gid
-    except KeyError:
-      raise errors.ConfigurationError("User or group not existing on system:"
-                                      " %s:%s" % (user, group))
     utils.CloseFDs()
-    utils.Daemonize(constants.DAEMONS_LOGFILES[daemon_name], uid, gid)
+    wpipe = utils.Daemonize(logfile=constants.DAEMONS_LOGFILES[daemon_name])
+  else:
+    wpipe = None
 
-  utils.WritePidFile(daemon_name)
+  utils.WritePidFile(utils.DaemonPidFileName(daemon_name))
   try:
-    utils.SetupLogging(logfile=constants.DAEMONS_LOGFILES[daemon_name],
-                       debug=options.debug,
-                       stderr_logging=not options.fork,
-                       multithreaded=multithreaded,
-                       program=daemon_name,
-                       syslog=options.syslog,
-                       console_logging=console_logging)
-    logging.info("%s daemon startup", daemon_name)
-    exec_fn(options, args)
+    try:
+      utils.SetupLogging(logfile=constants.DAEMONS_LOGFILES[daemon_name],
+                         debug=options.debug,
+                         stderr_logging=not options.fork,
+                         multithreaded=multithreaded,
+                         program=daemon_name,
+                         syslog=options.syslog,
+                         console_logging=console_logging)
+      if callable(prepare_fn):
+        prep_results = prepare_fn(options, args)
+      else:
+        prep_results = None
+      logging.info("%s daemon startup", daemon_name)
+    except Exception, err:
+      utils.WriteErrorToFD(wpipe, _BeautifyError(err))
+      raise
+
+    if wpipe is not None:
+      # we're done with the preparation phase, we close the pipe to
+      # let the parent know it's safe to exit
+      os.close(wpipe)
+
+    exec_fn(options, args, prep_results)
   finally:
     utils.RemovePidFile(daemon_name)
index 9374978..7df2285 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007 Google Inc.
+# Copyright (C) 2006, 2007, 2008, 2009, 2010 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
@@ -110,6 +110,16 @@ class ConfigurationError(GenericError):
   pass
 
 
+class ConfigVersionMismatch(ConfigurationError):
+  """Version mismatch in the configuration file.
+
+  The error has two arguments: the expected and the actual found
+  version.
+
+  """
+  pass
+
+
 class ReservationError(GenericError):
   """Errors reserving a resource.
 
@@ -356,6 +366,18 @@ class NoCtypesError(GenericError):
   """
 
 
+class IPAddressError(GenericError):
+  """Generic IP address error.
+
+  """
+
+
+class LuxiError(GenericError):
+  """LUXI error.
+
+  """
+
+
 # errors should be added above
 
 
diff --git a/lib/ht.py b/lib/ht.py
new file mode 100644 (file)
index 0000000..9609063
--- /dev/null
+++ b/lib/ht.py
@@ -0,0 +1,192 @@
+#
+#
+
+# Copyright (C) 2010 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 implementing the parameter types code."""
+
+from ganeti import compat
+
+# Modifiable default values; need to define these here before the
+# actual LUs
+
+def EmptyList():
+  """Returns an empty list.
+
+  """
+  return []
+
+
+def EmptyDict():
+  """Returns an empty dict.
+
+  """
+  return {}
+
+
+#: The without-default default value
+NoDefault = object()
+
+
+#: The no-type (value to complex to check it in the type system)
+NoType = object()
+
+
+# Some basic types
+def TNotNone(val):
+  """Checks if the given value is not None.
+
+  """
+  return val is not None
+
+
+def TNone(val):
+  """Checks if the given value is None.
+
+  """
+  return val is None
+
+
+def TBool(val):
+  """Checks if the given value is a boolean.
+
+  """
+  return isinstance(val, bool)
+
+
+def TInt(val):
+  """Checks if the given value is an integer.
+
+  """
+  return isinstance(val, int)
+
+
+def TFloat(val):
+  """Checks if the given value is a float.
+
+  """
+  return isinstance(val, float)
+
+
+def TString(val):
+  """Checks if the given value is a string.
+
+  """
+  return isinstance(val, basestring)
+
+
+def TTrue(val):
+  """Checks if a given value evaluates to a boolean True value.
+
+  """
+  return bool(val)
+
+
+def TElemOf(target_list):
+  """Builds a function that checks if a given value is a member of a list.
+
+  """
+  return lambda val: val in target_list
+
+
+# Container types
+def TList(val):
+  """Checks if the given value is a list.
+
+  """
+  return isinstance(val, list)
+
+
+def TDict(val):
+  """Checks if the given value is a dictionary.
+
+  """
+  return isinstance(val, dict)
+
+
+def TIsLength(size):
+  """Check is the given container is of the given size.
+
+  """
+  return lambda container: len(container) == size
+
+
+# Combinator types
+def TAnd(*args):
+  """Combine multiple functions using an AND operation.
+
+  """
+  def fn(val):
+    return compat.all(t(val) for t in args)
+  return fn
+
+
+def TOr(*args):
+  """Combine multiple functions using an AND operation.
+
+  """
+  def fn(val):
+    return compat.any(t(val) for t in args)
+  return fn
+
+
+def TMap(fn, test):
+  """Checks that a modified version of the argument passes the given test.
+
+  """
+  return lambda val: test(fn(val))
+
+
+# Type aliases
+
+#: a non-empty string
+TNonEmptyString = TAnd(TString, TTrue)
+
+
+#: a maybe non-empty string
+TMaybeString = TOr(TNonEmptyString, TNone)
+
+
+#: a maybe boolean (bool or none)
+TMaybeBool = TOr(TBool, TNone)
+
+
+#: a positive integer
+TPositiveInt = TAnd(TInt, lambda v: v >= 0)
+
+#: a strictly positive integer
+TStrictPositiveInt = TAnd(TInt, lambda v: v > 0)
+
+
+def TListOf(my_type):
+  """Checks if a given value is a list with all elements of the same type.
+
+  """
+  return TAnd(TList,
+               lambda lst: compat.all(my_type(v) for v in lst))
+
+
+def TDictOf(key_type, val_type):
+  """Checks a dict type for the type of its key/values.
+
+  """
+  return TAnd(TDict,
+              lambda my_dict: (compat.all(key_type(v) for v in my_dict.keys())
+                               and compat.all(val_type(v)
+                                              for v in my_dict.values())))
index 036c13f..9049829 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2007, 2008 Google Inc.
+# Copyright (C) 2007, 2008, 2010 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
@@ -571,7 +571,7 @@ class HttpBase(object):
     self._ssl_key = None
     self._ssl_cert = None
 
-  def _CreateSocket(self, ssl_params, ssl_verify_peer):
+  def _CreateSocket(self, ssl_params, ssl_verify_peer, family):
     """Creates a TCP socket and initializes SSL if needed.
 
     @type ssl_params: HttpSslParams
@@ -579,11 +579,14 @@ class HttpBase(object):
     @type ssl_verify_peer: bool
     @param ssl_verify_peer: Whether to require client certificate
         and compare it with our certificate
+    @type family: int
+    @param family: socket.AF_INET | socket.AF_INET6
 
     """
-    self._ssl_params = ssl_params
+    assert family in (socket.AF_INET, socket.AF_INET6)
 
-    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    self._ssl_params = ssl_params
+    sock = socket.socket(family, socket.SOCK_STREAM)
 
     # Should we enable SSL?
     self.using_ssl = ssl_params is not None
index f66f54b..6bfd9e0 100644 (file)
@@ -27,7 +27,6 @@ import re
 import base64
 import binascii
 
-from ganeti import utils
 from ganeti import compat
 from ganeti import http
 
@@ -287,8 +286,8 @@ class PasswordFileUser(object):
     self.options = options
 
 
-def ReadPasswordFile(file_name):
-  """Reads a password file.
+def ParsePasswordFile(contents):
+  """Parses the contents of a password file.
 
   Lines in the password file are of the following format::
 
@@ -298,15 +297,15 @@ def ReadPasswordFile(file_name):
   options are optional and separated by comma (','). Empty lines and comments
   ('#') are ignored.
 
-  @type file_name: str
-  @param file_name: Path to password file
+  @type contents: str
+  @param contents: Contents of password file
   @rtype: dict
   @return: Dictionary containing L{PasswordFileUser} instances
 
   """
   users = {}
 
-  for line in utils.ReadFile(file_name).splitlines():
+  for line in contents.splitlines():
     line = line.strip()
 
     # Ignore empty lines and comments
index dc502ab..8cc4744 100644 (file)
@@ -28,6 +28,7 @@ from cStringIO import StringIO
 
 from ganeti import http
 from ganeti import compat
+from ganeti import netutils
 
 
 class HttpClientRequest(object):
@@ -104,8 +105,12 @@ class HttpClientRequest(object):
     """Returns the full URL for this requests.
 
     """
+    if netutils.IPAddress.IsValid(self.host):
+      address = netutils.FormatAddress((self.host, self.port))
+    else:
+      address = "%s:%s" % (self.host, self.port)
     # TODO: Support for non-SSL requests
-    return "https://%s:%s%s" % (self.host, self.port, self.path)
+    return "https://%s%s" % (address, self.path)
 
   @property
   def identity(self):
index 2e444dc..7a46af6 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2007, 2008 Google Inc.
+# Copyright (C) 2007, 2008, 2010 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
@@ -33,6 +33,7 @@ import asyncore
 
 from ganeti import http
 from ganeti import utils
+from ganeti import netutils
 
 
 WEEKDAYNAME = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
@@ -478,8 +479,8 @@ class HttpServer(http.HttpBase, asyncore.dispatcher):
     self.mainloop = mainloop
     self.local_address = local_address
     self.port = port
-
-    self.socket = self._CreateSocket(ssl_params, ssl_verify_peer)
+    family = netutils.IPAddress.GetAddressFamily(local_address)
+    self.socket = self._CreateSocket(ssl_params, ssl_verify_peer, family)
 
     # Allow port to be reused
     self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
index 042aef5..9816b35 100644 (file)
@@ -175,7 +175,8 @@ class KVMHypervisor(hv_base.BaseHypervisor):
     constants.HV_ACPI: hv_base.NO_CHECK,
     constants.HV_SERIAL_CONSOLE: hv_base.NO_CHECK,
     constants.HV_VNC_BIND_ADDRESS:
-      (False, lambda x: (netutils.IsValidIP4(x) or utils.IsNormAbsPath(x)),
+      (False, lambda x: (netutils.IP4Address.IsValid(x) or
+                         utils.IsNormAbsPath(x)),
        "the VNC bind address must be either a valid IP address or an absolute"
        " pathname", None, None),
     constants.HV_VNC_TLS: hv_base.NO_CHECK,
@@ -582,7 +583,7 @@ class KVMHypervisor(hv_base.BaseHypervisor):
       kvm_cmd.extend(['-usbdevice', constants.HT_MOUSE_TABLET])
 
     if vnc_bind_address:
-      if netutils.IsValidIP4(vnc_bind_address):
+      if netutils.IP4Address.IsValid(vnc_bind_address):
         if instance.network_port > constants.VNC_BASE_PORT:
           display = instance.network_port - constants.VNC_BASE_PORT
           if vnc_bind_address == constants.IP4_ADDRESS_ANY:
index 64c231d..bdf38a4 100644 (file)
@@ -551,7 +551,7 @@ class XenHvmHypervisor(XenHypervisor):
       hv_base.ParamInSet(True, constants.HT_HVM_VALID_NIC_TYPES),
     constants.HV_PAE: hv_base.NO_CHECK,
     constants.HV_VNC_BIND_ADDRESS:
-      (False, netutils.IsValidIP4,
+      (False, netutils.IP4Address.IsValid,
        "VNC bind address is not a valid IP address", None, None),
     constants.HV_KERNEL_PATH: hv_base.REQ_FILE_CHECK,
     constants.HV_DEVICE_MODEL: hv_base.REQ_FILE_CHECK,
index b1e3ee0..1a0c20d 100644 (file)
@@ -53,6 +53,7 @@ from ganeti import mcpu
 from ganeti import utils
 from ganeti import jstore
 from ganeti import rpc
+from ganeti import runtime
 from ganeti import netutils
 from ganeti import compat
 
@@ -94,7 +95,7 @@ class _QueuedOpCode(object):
   @ivar stop_timestamp: timestamp for the end of the execution
 
   """
-  __slots__ = ["input", "status", "result", "log",
+  __slots__ = ["input", "status", "result", "log", "priority",
                "start_timestamp", "exec_timestamp", "end_timestamp",
                "__weakref__"]
 
@@ -113,6 +114,9 @@ class _QueuedOpCode(object):
     self.exec_timestamp = None
     self.end_timestamp = None
 
+    # Get initial priority (it might change during the lifetime of this opcode)
+    self.priority = getattr(op, "priority", constants.OP_PRIO_DEFAULT)
+
   @classmethod
   def Restore(cls, state):
     """Restore the _QueuedOpCode from the serialized form.
@@ -131,6 +135,7 @@ class _QueuedOpCode(object):
     obj.start_timestamp = state.get("start_timestamp", None)
     obj.exec_timestamp = state.get("exec_timestamp", None)
     obj.end_timestamp = state.get("end_timestamp", None)
+    obj.priority = state.get("priority", constants.OP_PRIO_DEFAULT)
     return obj
 
   def Serialize(self):
@@ -148,6 +153,7 @@ class _QueuedOpCode(object):
       "start_timestamp": self.start_timestamp,
       "exec_timestamp": self.exec_timestamp,
       "end_timestamp": self.end_timestamp,
+      "priority": self.priority,
       }
 
 
@@ -170,7 +176,7 @@ class _QueuedJob(object):
 
   """
   # pylint: disable-msg=W0212
-  __slots__ = ["queue", "id", "ops", "log_serial",
+  __slots__ = ["queue", "id", "ops", "log_serial", "ops_iter", "cur_opctx",
                "received_timestamp", "start_timestamp", "end_timestamp",
                "__weakref__"]
 
@@ -197,6 +203,16 @@ class _QueuedJob(object):
     self.start_timestamp = None
     self.end_timestamp = None
 
+    self._InitInMemory(self)
+
+  @staticmethod
+  def _InitInMemory(obj):
+    """Initializes in-memory variables.
+
+    """
+    obj.ops_iter = None
+    obj.cur_opctx = None
+
   def __repr__(self):
     status = ["%s.%s" % (self.__class__.__module__, self.__class__.__name__),
               "id=%s" % self.id,
@@ -231,6 +247,8 @@ class _QueuedJob(object):
         obj.log_serial = max(obj.log_serial, log_entry[0])
       obj.ops.append(op)
 
+    cls._InitInMemory(obj)
+
     return obj
 
   def Serialize(self):
@@ -301,6 +319,24 @@ class _QueuedJob(object):
 
     return status
 
+  def CalcPriority(self):
+    """Gets the current priority for this job.
+
+    Only unfinished opcodes are considered. When all are done, the default
+    priority is used.
+
+    @rtype: int
+
+    """
+    priorities = [op.priority for op in self.ops
+                  if op.status not in constants.OPS_FINALIZED]
+
+    if not priorities:
+      # All opcodes are done, assume default priority
+      return constants.OP_PRIO_DEFAULT
+
+    return min(priorities)
+
   def GetLogEntries(self, newer_than):
     """Selectively returns the log entries.
 
@@ -340,6 +376,8 @@ class _QueuedJob(object):
         row.append(self.id)
       elif fname == "status":
         row.append(self.CalcStatus())
+      elif fname == "priority":
+        row.append(self.CalcPriority())
       elif fname == "ops":
         row.append([op.input.__getstate__() for op in self.ops])
       elif fname == "opresult":
@@ -354,6 +392,8 @@ class _QueuedJob(object):
         row.append([op.exec_timestamp for op in self.ops])
       elif fname == "opend":
         row.append([op.end_timestamp for op in self.ops])
+      elif fname == "oppriority":
+        row.append([op.priority for op in self.ops])
       elif fname == "received_ts":
         row.append(self.received_timestamp)
       elif fname == "start_ts":
@@ -386,6 +426,30 @@ class _QueuedJob(object):
       op.result = result
       not_marked = False
 
+  def Cancel(self):
+    """Marks job as canceled/-ing if possible.
+
+    @rtype: tuple; (bool, string)
+    @return: Boolean describing whether job was successfully canceled or marked
+      as canceling and a text message
+
+    """
+    status = self.CalcStatus()
+
+    if status == constants.JOB_STATUS_QUEUED:
+      self.MarkUnfinishedOps(constants.OP_STATUS_CANCELED,
+                             "Job canceled by request")
+      return (True, "Job %s canceled" % self.id)
+
+    elif status == constants.JOB_STATUS_WAITLOCK:
+      # The worker will notice the new status and cancel the job
+      self.MarkUnfinishedOps(constants.OP_STATUS_CANCELING, None)
+      return (True, "Job %s will be canceled" % self.id)
+
+    else:
+      logging.debug("Job %s is no longer waiting in the queue", self.id)
+      return (False, "Job %s is no longer waiting in the queue" % self.id)
+
 
 class _OpExecCallbacks(mcpu.OpExecCbBase):
   def __init__(self, queue, job, op):
@@ -683,6 +747,348 @@ def _EncodeOpError(err):
   return errors.EncodeException(to_encode)
 
 
+class _TimeoutStrategyWrapper:
+  def __init__(self, fn):
+    """Initializes this class.
+
+    """
+    self._fn = fn
+    self._next = None
+
+  def _Advance(self):
+    """Gets the next timeout if necessary.
+
+    """
+    if self._next is None:
+      self._next = self._fn()
+
+  def Peek(self):
+    """Returns the next timeout.
+
+    """
+    self._Advance()
+    return self._next
+
+  def Next(self):
+    """Returns the current timeout and advances the internal state.
+
+    """
+    self._Advance()
+    result = self._next
+    self._next = None
+    return result
+
+
+class _OpExecContext:
+  def __init__(self, op, index, log_prefix, timeout_strategy_factory):
+    """Initializes this class.
+
+    """
+    self.op = op
+    self.index = index
+    self.log_prefix = log_prefix
+    self.summary = op.input.Summary()
+
+    self._timeout_strategy_factory = timeout_strategy_factory
+    self._ResetTimeoutStrategy()
+
+  def _ResetTimeoutStrategy(self):
+    """Creates a new timeout strategy.
+
+    """
+    self._timeout_strategy = \
+      _TimeoutStrategyWrapper(self._timeout_strategy_factory().NextAttempt)
+
+  def CheckPriorityIncrease(self):
+    """Checks whether priority can and should be increased.
+
+    Called when locks couldn't be acquired.
+
+    """
+    op = self.op
+
+    # Exhausted all retries and next round should not use blocking acquire
+    # for locks?
+    if (self._timeout_strategy.Peek() is None and
+        op.priority > constants.OP_PRIO_HIGHEST):
+      logging.debug("Increasing priority")
+      op.priority -= 1
+      self._ResetTimeoutStrategy()
+      return True
+
+    return False
+
+  def GetNextLockTimeout(self):
+    """Returns the next lock acquire timeout.
+
+    """
+    return self._timeout_strategy.Next()
+
+
+class _JobProcessor(object):
+  def __init__(self, queue, opexec_fn, job,
+               _timeout_strategy_factory=mcpu.LockAttemptTimeoutStrategy):
+    """Initializes this class.
+
+    """
+    self.queue = queue
+    self.opexec_fn = opexec_fn
+    self.job = job
+    self._timeout_strategy_factory = _timeout_strategy_factory
+
+  @staticmethod
+  def _FindNextOpcode(job, timeout_strategy_factory):
+    """Locates the next opcode to run.
+
+    @type job: L{_QueuedJob}
+    @param job: Job object
+    @param timeout_strategy_factory: Callable to create new timeout strategy
+
+    """
+    # Create some sort of a cache to speed up locating next opcode for future
+    # lookups
+    # TODO: Consider splitting _QueuedJob.ops into two separate lists, one for
+    # pending and one for processed ops.
+    if job.ops_iter is None:
+      job.ops_iter = enumerate(job.ops)
+
+    # Find next opcode to run
+    while True:
+      try:
+        (idx, op) = job.ops_iter.next()
+      except StopIteration:
+        raise errors.ProgrammerError("Called for a finished job")
+
+      if op.status == constants.OP_STATUS_RUNNING:
+        # Found an opcode already marked as running
+        raise errors.ProgrammerError("Called for job marked as running")
+
+      opctx = _OpExecContext(op, idx, "Op %s/%s" % (idx + 1, len(job.ops)),
+                             timeout_strategy_factory)
+
+      if op.status == constants.OP_STATUS_CANCELED:
+        # Cancelled jobs are handled by the caller
+        assert not compat.any(i.status != constants.OP_STATUS_CANCELED
+                              for i in job.ops[idx:])
+
+      elif op.status in constants.OPS_FINALIZED:
+        # This is a job that was partially completed before master daemon
+        # shutdown, so it can be expected that some opcodes are already
+        # completed successfully (if any did error out, then the whole job
+        # should have been aborted and not resubmitted for processing).
+        logging.info("%s: opcode %s already processed, skipping",
+                     opctx.log_prefix, opctx.summary)
+        continue
+
+      return opctx
+
+  @staticmethod
+  def _MarkWaitlock(job, op):
+    """Marks an opcode as waiting for locks.
+
+    The job's start timestamp is also set if necessary.
+
+    @type job: L{_QueuedJob}
+    @param job: Job object
+    @type op: L{_QueuedOpCode}
+    @param op: Opcode object
+
+    """
+    assert op in job.ops
+
+    op.status = constants.OP_STATUS_WAITLOCK
+    op.result = None
+    op.start_timestamp = TimeStampNow()
+
+    if job.start_timestamp is None:
+      job.start_timestamp = op.start_timestamp
+
+  def _ExecOpCodeUnlocked(self, opctx):
+    """Processes one opcode and returns the result.
+
+    """
+    op = opctx.op
+
+    assert op.status == constants.OP_STATUS_WAITLOCK
+
+    timeout = opctx.GetNextLockTimeout()
+
+    try:
+      # Make sure not to hold queue lock while calling ExecOpCode
+      result = self.opexec_fn(op.input,
+                              _OpExecCallbacks(self.queue, self.job, op),
+                              timeout=timeout, priority=op.priority)
+    except mcpu.LockAcquireTimeout:
+      assert timeout is not None, "Received timeout for blocking acquire"
+      logging.debug("Couldn't acquire locks in %0.6fs", timeout)
+
+      assert op.status in (constants.OP_STATUS_WAITLOCK,
+                           constants.OP_STATUS_CANCELING)
+
+      # Was job cancelled while we were waiting for the lock?
+      if op.status == constants.OP_STATUS_CANCELING:
+        return (constants.OP_STATUS_CANCELING, None)
+
+      return (constants.OP_STATUS_QUEUED, None)
+    except CancelJob:
+      logging.exception("%s: Canceling job", opctx.log_prefix)
+      assert op.status == constants.OP_STATUS_CANCELING
+      return (constants.OP_STATUS_CANCELING, None)
+    except Exception, err: # pylint: disable-msg=W0703
+      logging.exception("%s: Caught exception in %s",
+                        opctx.log_prefix, opctx.summary)
+      return (constants.OP_STATUS_ERROR, _EncodeOpError(err))
+    else:
+      logging.debug("%s: %s successful",
+                    opctx.log_prefix, opctx.summary)
+      return (constants.OP_STATUS_SUCCESS, result)
+
+  def __call__(self, _nextop_fn=None):
+    """Continues execution of a job.
+
+    @param _nextop_fn: Callback function for tests
+    @rtype: bool
+    @return: True if job is finished, False if processor needs to be called
+             again
+
+    """
+    queue = self.queue
+    job = self.job
+
+    logging.debug("Processing job %s", job.id)
+
+    queue.acquire(shared=1)
+    try:
+      opcount = len(job.ops)
+
+      # Is a previous opcode still pending?
+      if job.cur_opctx:
+        opctx = job.cur_opctx
+      else:
+        if __debug__ and _nextop_fn:
+          _nextop_fn()
+        opctx = self._FindNextOpcode(job, self._timeout_strategy_factory)
+
+      op = opctx.op
+
+      # Consistency check
+      assert compat.all(i.status in (constants.OP_STATUS_QUEUED,
+                                     constants.OP_STATUS_CANCELED)
+                        for i in job.ops[opctx.index:])
+
+      assert op.status in (constants.OP_STATUS_QUEUED,
+                           constants.OP_STATUS_WAITLOCK,
+                           constants.OP_STATUS_CANCELED)
+
+      assert (op.priority <= constants.OP_PRIO_LOWEST and
+              op.priority >= constants.OP_PRIO_HIGHEST)
+
+      if op.status != constants.OP_STATUS_CANCELED:
+        # Prepare to start opcode
+        self._MarkWaitlock(job, op)
+
+        assert op.status == constants.OP_STATUS_WAITLOCK
+        assert job.CalcStatus() == constants.JOB_STATUS_WAITLOCK
+
+        # Write to disk
+        queue.UpdateJobUnlocked(job)
+
+        logging.info("%s: opcode %s waiting for locks",
+                     opctx.log_prefix, opctx.summary)
+
+        queue.release()
+        try:
+          (op_status, op_result) = self._ExecOpCodeUnlocked(opctx)
+        finally:
+          queue.acquire(shared=1)
+
+        op.status = op_status
+        op.result = op_result
+
+        if op.status == constants.OP_STATUS_QUEUED:
+          # Couldn't get locks in time
+          assert not op.end_timestamp
+        else:
+          # Finalize opcode
+          op.end_timestamp = TimeStampNow()
+
+          if op.status == constants.OP_STATUS_CANCELING:
+            assert not compat.any(i.status != constants.OP_STATUS_CANCELING
+                                  for i in job.ops[opctx.index:])
+          else:
+            assert op.status in constants.OPS_FINALIZED
+
+      if op.status == constants.OP_STATUS_QUEUED:
+        finalize = False
+
+        opctx.CheckPriorityIncrease()
+
+        # Keep around for another round
+        job.cur_opctx = opctx
+
+        assert (op.priority <= constants.OP_PRIO_LOWEST and
+                op.priority >= constants.OP_PRIO_HIGHEST)
+
+        # In no case must the status be finalized here
+        assert job.CalcStatus() == constants.JOB_STATUS_QUEUED
+
+        queue.UpdateJobUnlocked(job)
+
+      else:
+        # Ensure all opcodes so far have been successful
+        assert (opctx.index == 0 or
+                compat.all(i.status == constants.OP_STATUS_SUCCESS
+                           for i in job.ops[:opctx.index]))
+
+        # Reset context
+        job.cur_opctx = None
+
+        if op.status == constants.OP_STATUS_SUCCESS:
+          finalize = False
+
+        elif op.status == constants.OP_STATUS_ERROR:
+          # Ensure failed opcode has an exception as its result
+          assert errors.GetEncodedError(job.ops[opctx.index].result)
+
+          to_encode = errors.OpExecError("Preceding opcode failed")
+          job.MarkUnfinishedOps(constants.OP_STATUS_ERROR,
+                                _EncodeOpError(to_encode))
+          finalize = True
+
+          # Consistency check
+          assert compat.all(i.status == constants.OP_STATUS_ERROR and
+                            errors.GetEncodedError(i.result)
+                            for i in job.ops[opctx.index:])
+
+        elif op.status == constants.OP_STATUS_CANCELING:
+          job.MarkUnfinishedOps(constants.OP_STATUS_CANCELED,
+                                "Job canceled by request")
+          finalize = True
+
+        elif op.status == constants.OP_STATUS_CANCELED:
+          finalize = True
+
+        else:
+          raise errors.ProgrammerError("Unknown status '%s'" % op.status)
+
+        # Finalizing or last opcode?
+        if finalize or opctx.index == (opcount - 1):
+          # All opcodes have been run, finalize job
+          job.end_timestamp = TimeStampNow()
+
+        # Write to disk. If the job status is final, this is the final write
+        # allowed. Once the file has been written, it can be archived anytime.
+        queue.UpdateJobUnlocked(job)
+
+        if finalize or opctx.index == (opcount - 1):
+          logging.info("Finished job %s, status = %s", job.id, job.CalcStatus())
+          return True
+
+      return False
+    finally:
+      queue.release()
+
+
 class _JobQueueWorker(workerpool.BaseWorker):
   """The actual job workers.
 
@@ -690,125 +1096,23 @@ class _JobQueueWorker(workerpool.BaseWorker):
   def RunTask(self, job): # pylint: disable-msg=W0221
     """Job executor.
 
-    This functions processes a job. It is closely tied to the _QueuedJob and
-    _QueuedOpCode classes.
+    This functions processes a job. It is closely tied to the L{_QueuedJob} and
+    L{_QueuedOpCode} classes.
 
     @type job: L{_QueuedJob}
     @param job: the job to be processed
 
     """
+    queue = job.queue
+    assert queue == self.pool.queue
+
     self.SetTaskName("Job%s" % job.id)
 
-    logging.info("Processing job %s", job.id)
-    proc = mcpu.Processor(self.pool.queue.context, job.id)
-    queue = job.queue
-    try:
-      try:
-        count = len(job.ops)
-        for idx, op in enumerate(job.ops):
-          op_summary = op.input.Summary()
-          if op.status == constants.OP_STATUS_SUCCESS:
-            # this is a job that was partially completed before master
-            # daemon shutdown, so it can be expected that some opcodes
-            # are already completed successfully (if any did error
-            # out, then the whole job should have been aborted and not
-            # resubmitted for processing)
-            logging.info("Op %s/%s: opcode %s already processed, skipping",
-                         idx + 1, count, op_summary)
-            continue
-          try:
-            logging.info("Op %s/%s: Starting opcode %s", idx + 1, count,
-                         op_summary)
-
-            queue.acquire(shared=1)
-            try:
-              if op.status == constants.OP_STATUS_CANCELED:
-                logging.debug("Canceling opcode")
-                raise CancelJob()
-              assert op.status == constants.OP_STATUS_QUEUED
-              logging.debug("Opcode %s/%s waiting for locks",
-                            idx + 1, count)
-              op.status = constants.OP_STATUS_WAITLOCK
-              op.result = None
-              op.start_timestamp = TimeStampNow()
-              if idx == 0: # first opcode
-                job.start_timestamp = op.start_timestamp
-              queue.UpdateJobUnlocked(job)
-
-              input_opcode = op.input
-            finally:
-              queue.release()
-
-            # Make sure not to hold queue lock while calling ExecOpCode
-            result = proc.ExecOpCode(input_opcode,
-                                     _OpExecCallbacks(queue, job, op))
-
-            queue.acquire(shared=1)
-            try:
-              logging.debug("Opcode %s/%s succeeded", idx + 1, count)
-              op.status = constants.OP_STATUS_SUCCESS
-              op.result = result
-              op.end_timestamp = TimeStampNow()
-              if idx == count - 1:
-                job.end_timestamp = TimeStampNow()
-
-                # Consistency check
-                assert compat.all(i.status == constants.OP_STATUS_SUCCESS
-                                  for i in job.ops)
-
-              queue.UpdateJobUnlocked(job)
-            finally:
-              queue.release()
-
-            logging.info("Op %s/%s: Successfully finished opcode %s",
-                         idx + 1, count, op_summary)
-          except CancelJob:
-            # Will be handled further up
-            raise
-          except Exception, err:
-            queue.acquire(shared=1)
-            try:
-              try:
-                logging.debug("Opcode %s/%s failed", idx + 1, count)
-                op.status = constants.OP_STATUS_ERROR
-                op.result = _EncodeOpError(err)
-                op.end_timestamp = TimeStampNow()
-                logging.info("Op %s/%s: Error in opcode %s: %s",
-                             idx + 1, count, op_summary, err)
-
-                to_encode = errors.OpExecError("Preceding opcode failed")
-                job.MarkUnfinishedOps(constants.OP_STATUS_ERROR,
-                                      _EncodeOpError(to_encode))
-
-                # Consistency check
-                assert compat.all(i.status == constants.OP_STATUS_SUCCESS
-                                  for i in job.ops[:idx])
-                assert compat.all(i.status == constants.OP_STATUS_ERROR and
-                                  errors.GetEncodedError(i.result)
-                                  for i in job.ops[idx:])
-              finally:
-                job.end_timestamp = TimeStampNow()
-                queue.UpdateJobUnlocked(job)
-            finally:
-              queue.release()
-            raise
-
-      except CancelJob:
-        queue.acquire(shared=1)
-        try:
-          job.MarkUnfinishedOps(constants.OP_STATUS_CANCELED,
-                                "Job canceled by request")
-          job.end_timestamp = TimeStampNow()
-          queue.UpdateJobUnlocked(job)
-        finally:
-          queue.release()
-      except errors.GenericError, err:
-        logging.exception("Ganeti exception")
-      except:
-        logging.exception("Unhandled exception")
-    finally:
-      status = job.CalcStatus()
-      logging.info("Finished job %s, status = %s", job.id, status)
+    proc = mcpu.Processor(queue.context, job.id)
+
+    if not _JobProcessor(queue, proc.ExecOpCode, job)():
+      # Schedule again
+      raise workerpool.DeferTask(priority=job.CalcPriority())
 
 
 class _JobQueueWorkerPool(workerpool.WorkerPool):
@@ -870,7 +1174,7 @@ class JobQueue(object):
     """
     self.context = context
     self._memcache = weakref.WeakValueDictionary()
-    self._my_hostname = netutils.HostInfo().name
+    self._my_hostname = netutils.Hostname.GetSysName()
 
     # The Big JobQueue lock. If a code block or method acquires it in shared
     # mode safe it must guarantee concurrency with all the code acquiring it in
@@ -924,6 +1228,8 @@ class JobQueue(object):
     """
     logging.info("Inspecting job queue")
 
+    restartjobs = []
+
     all_job_ids = self._GetJobIDsUnlocked()
     jobs_count = len(all_job_ids)
     lastinfo = time.time()
@@ -943,17 +1249,28 @@ class JobQueue(object):
 
       status = job.CalcStatus()
 
-      if status in (constants.JOB_STATUS_QUEUED, ):
-        self._wpool.AddTask((job, ))
+      if status == constants.JOB_STATUS_QUEUED:
+        restartjobs.append(job)
 
       elif status in (constants.JOB_STATUS_RUNNING,
                       constants.JOB_STATUS_WAITLOCK,
                       constants.JOB_STATUS_CANCELING):
         logging.warning("Unfinished job %s found: %s", job.id, job)
-        job.MarkUnfinishedOps(constants.OP_STATUS_ERROR,
-                              "Unclean master daemon shutdown")
+
+        if status == constants.JOB_STATUS_WAITLOCK:
+          # Restart job
+          job.MarkUnfinishedOps(constants.OP_STATUS_QUEUED, None)
+          restartjobs.append(job)
+        else:
+          job.MarkUnfinishedOps(constants.OP_STATUS_ERROR,
+                                "Unclean master daemon shutdown")
+
         self.UpdateJobUnlocked(job)
 
+    if restartjobs:
+      logging.info("Restarting %s jobs", len(restartjobs))
+      self._EnqueueJobs(restartjobs)
+
     logging.info("Job queue inspection finished")
 
   @locking.ssynchronized(_LOCK)
@@ -1071,7 +1388,9 @@ class JobQueue(object):
     @param replicate: whether to spread the changes to the remote nodes
 
     """
-    utils.WriteFile(file_name, data=data)
+    getents = runtime.GetEnts()
+    utils.WriteFile(file_name, data=data, uid=getents.masterd_uid,
+                    gid=getents.masterd_gid)
 
     if replicate:
       names, addrs = self._GetNodeIp()
@@ -1315,8 +1634,11 @@ class JobQueue(object):
     @param drain_flag: Whether to set or unset the drain flag
 
     """
+    getents = runtime.GetEnts()
+
     if drain_flag:
-      utils.WriteFile(constants.JOB_QUEUE_DRAIN_FILE, data="", close=True)
+      utils.WriteFile(constants.JOB_QUEUE_DRAIN_FILE, data="", close=True,
+                      uid=getents.masterd_uid, gid=getents.masterd_gid)
     else:
       utils.RemoveFile(constants.JOB_QUEUE_DRAIN_FILE)
 
@@ -1339,6 +1661,7 @@ class JobQueue(object):
     @return: the job object to be queued
     @raise errors.JobQueueDrainError: if the job queue is marked for draining
     @raise errors.JobQueueFull: if the job queue has too many jobs in it
+    @raise errors.GenericError: If an opcode is not valid
 
     """
     # Ok when sharing the big job queue lock, as the drain file is created when
@@ -1351,6 +1674,13 @@ class JobQueue(object):
 
     job = _QueuedJob(self, job_id, ops)
 
+    # Check priority
+    for idx, op in enumerate(job.ops):
+      if op.priority not in constants.OP_PRIO_SUBMIT_VALID:
+        allowed = utils.CommaJoin(constants.OP_PRIO_SUBMIT_VALID)
+        raise errors.GenericError("Opcode %s has invalid priority %s, allowed"
+                                  " are %s" % (idx, op.priority, allowed))
+
     # Write to disk
     self.UpdateJobUnlocked(job)
 
@@ -1370,7 +1700,7 @@ class JobQueue(object):
 
     """
     job_id = self._NewSerialsUnlocked(1)[0]
-    self._wpool.AddTask((self._SubmitJobUnlocked(job_id, ops), ))
+    self._EnqueueJobs([self._SubmitJobUnlocked(job_id, ops)])
     return job_id
 
   @locking.ssynchronized(_LOCK)
@@ -1382,21 +1712,32 @@ class JobQueue(object):
 
     """
     results = []
-    tasks = []
+    added_jobs = []
     all_job_ids = self._NewSerialsUnlocked(len(jobs))
     for job_id, ops in zip(all_job_ids, jobs):
       try:
-        tasks.append((self._SubmitJobUnlocked(job_id, ops), ))
+        added_jobs.append(self._SubmitJobUnlocked(job_id, ops))
         status = True
         data = job_id
       except errors.GenericError, err:
         data = str(err)
         status = False
       results.append((status, data))
-    self._wpool.AddManyTasks(tasks)
+
+    self._EnqueueJobs(added_jobs)
 
     return results
 
+  def _EnqueueJobs(self, jobs):
+    """Helper function to add jobs to worker pool's queue.
+
+    @type jobs: list
+    @param jobs: List of all jobs
+
+    """
+    self._wpool.AddManyTasks([(job, ) for job in jobs],
+                             priority=[job.CalcPriority() for job in jobs])
+
   @_RequireOpenQueue
   def UpdateJobUnlocked(self, job, replicate=True):
     """Update a job's on disk storage.
@@ -1465,26 +1806,12 @@ class JobQueue(object):
       logging.debug("Job %s not found", job_id)
       return (False, "Job %s not found" % job_id)
 
-    job_status = job.CalcStatus()
+    (success, msg) = job.Cancel()
 
-    if job_status not in (constants.JOB_STATUS_QUEUED,
-                          constants.JOB_STATUS_WAITLOCK):
-      logging.debug("Job %s is no longer waiting in the queue", job.id)
-      return (False, "Job %s is no longer waiting in the queue" % job.id)
-
-    if job_status == constants.JOB_STATUS_QUEUED:
-      job.MarkUnfinishedOps(constants.OP_STATUS_CANCELED,
-                            "Job canceled by request")
-      msg = "Job %s canceled" % job.id
-
-    elif job_status == constants.JOB_STATUS_WAITLOCK:
-      # The worker will notice the new status and cancel the job
-      job.MarkUnfinishedOps(constants.OP_STATUS_CANCELING, None)
-      msg = "Job %s will be canceled" % job.id
-
-    self.UpdateJobUnlocked(job)
+    if success:
+      self.UpdateJobUnlocked(job)
 
-    return (True, msg)
+    return (success, msg)
 
   @_RequireOpenQueue
   def _ArchiveJobsUnlocked(self, jobs):
index f61a79c..1570cb9 100644 (file)
@@ -25,6 +25,7 @@ import errno
 
 from ganeti import constants
 from ganeti import errors
+from ganeti import runtime
 from ganeti import utils
 
 
@@ -73,8 +74,7 @@ def InitAndVerifyQueue(must_lock):
            locking mode.
 
   """
-  dirs = [(d, constants.JOB_QUEUE_DIRS_MODE) for d in constants.JOB_QUEUE_DIRS]
-  utils.EnsureDirs(dirs)
+  getents = runtime.GetEnts()
 
   # Lock queue
   queue_lock = utils.FileLock.Open(constants.JOB_QUEUE_LOCK_FILE)
@@ -99,6 +99,7 @@ def InitAndVerifyQueue(must_lock):
       if version is None:
         # Write new version file
         utils.WriteFile(constants.JOB_QUEUE_VERSION_FILE,
+                        uid=getents.masterd_uid, gid=getents.masterd_gid,
                         data="%s\n" % constants.JOB_QUEUE_VERSION)
 
         # Read again
@@ -112,6 +113,7 @@ def InitAndVerifyQueue(must_lock):
       if serial is None:
         # Write new serial file
         utils.WriteFile(constants.JOB_QUEUE_SERIAL_FILE,
+                        uid=getents.masterd_uid, gid=getents.masterd_gid,
                         data="%s\n" % 0)
 
         # Read again
index f901490..934638f 100644 (file)
@@ -32,6 +32,7 @@ import time
 import errno
 import weakref
 import logging
+import heapq
 
 from ganeti import errors
 from ganeti import utils
@@ -41,6 +42,8 @@ from ganeti import compat
 _EXCLUSIVE_TEXT = "exclusive"
 _SHARED_TEXT = "shared"
 
+_DEFAULT_PRIORITY = 0
+
 
 def ssynchronized(mylock, shared=0):
   """Shared Synchronization decorator.
@@ -406,6 +409,19 @@ class PipeCondition(_BaseCondition):
     return bool(self._waiters)
 
 
+class _PipeConditionWithMode(PipeCondition):
+  __slots__ = [
+    "shared",
+    ]
+
+  def __init__(self, lock, shared):
+    """Initializes this class.
+
+    """
+    self.shared = shared
+    PipeCondition.__init__(self, lock)
+
+
 class SharedLock(object):
   """Implements a shared lock.
 
@@ -413,9 +429,13 @@ class SharedLock(object):
   C{acquire(shared=1)}. In order to acquire the lock in an exclusive way
   threads can call C{acquire(shared=0)}.
 
-  The lock prevents starvation but does not guarantee that threads will acquire
-  the shared lock in the order they queued for it, just that they will
-  eventually do so.
+  Notes on data structures: C{__pending} contains a priority queue (heapq) of
+  all pending acquires: C{[(priority1: prioqueue1), (priority2: prioqueue2),
+  ...]}. Each per-priority queue contains a normal in-order list of conditions
+  to be notified when the lock can be acquired. Shared locks are grouped
+  together by priority and the condition for them is stored in
+  C{__pending_shared} if it already exists. C{__pending_by_prio} keeps
+  references for the per-priority queues indexed by priority for faster access.
 
   @type name: string
   @ivar name: the name of the lock
@@ -423,17 +443,17 @@ class SharedLock(object):
   """
   __slots__ = [
     "__weakref__",
-    "__active_shr_c",
-    "__inactive_shr_c",
     "__deleted",
     "__exc",
     "__lock",
     "__pending",
+    "__pending_by_prio",
+    "__pending_shared",
     "__shr",
     "name",
     ]
 
-  __condition_class = PipeCondition
+  __condition_class = _PipeConditionWithMode
 
   def __init__(self, name, monitor=None):
     """Construct a new SharedLock.
@@ -452,10 +472,8 @@ class SharedLock(object):
 
     # Queue containing waiting acquires
     self.__pending = []
-
-    # Active and inactive conditions for shared locks
-    self.__active_shr_c = self.__condition_class(self.__lock)
-    self.__inactive_shr_c = self.__condition_class(self.__lock)
+    self.__pending_by_prio = {}
+    self.__pending_shared = {}
 
     # Current lock holders
     self.__shr = set()
@@ -509,16 +527,18 @@ class SharedLock(object):
         elif fname == "pending":
           data = []
 
-          for cond in self.__pending:
-            if cond in (self.__active_shr_c, self.__inactive_shr_c):
-              mode = _SHARED_TEXT
-            else:
-              mode = _EXCLUSIVE_TEXT
+          # Sorting instead of copying and using heaq functions for simplicity
+          for (_, prioqueue) in sorted(self.__pending):
+            for cond in prioqueue:
+              if cond.shared:
+                mode = _SHARED_TEXT
+              else:
+                mode = _EXCLUSIVE_TEXT
 
-            # This function should be fast as it runs with the lock held. Hence
-            # not using utils.NiceSort.
-            data.append((mode, sorted([i.getName()
-                                       for i in cond.get_waiting()])))
+              # This function should be fast as it runs with the lock held.
+              # Hence not using utils.NiceSort.
+              data.append((mode, sorted(i.getName()
+                                        for i in cond.get_waiting())))
 
           info.append(data)
         else:
@@ -584,7 +604,23 @@ class SharedLock(object):
     """
     self.__lock.acquire()
     try:
-      return len(self.__pending)
+      return sum(len(prioqueue) for (_, prioqueue) in self.__pending)
+    finally:
+      self.__lock.release()
+
+  def _check_empty(self):
+    """Checks whether there are any pending acquires.
+
+    @rtype: bool
+
+    """
+    self.__lock.acquire()
+    try:
+      # Order is important: __find_first_pending_queue modifies __pending
+      return not (self.__find_first_pending_queue() or
+                  self.__pending or
+                  self.__pending_by_prio or
+                  self.__pending_shared)
     finally:
       self.__lock.release()
 
@@ -606,20 +642,42 @@ class SharedLock(object):
     else:
       return len(self.__shr) == 0 and self.__exc is None
 
+  def __find_first_pending_queue(self):
+    """Tries to find the topmost queued entry with pending acquires.
+
+    Removes empty entries while going through the list.
+
+    """
+    while self.__pending:
+      (priority, prioqueue) = self.__pending[0]
+
+      if not prioqueue:
+        heapq.heappop(self.__pending)
+        del self.__pending_by_prio[priority]
+        assert priority not in self.__pending_shared
+        continue
+
+      if prioqueue:
+        return prioqueue
+
+    return None
+
   def __is_on_top(self, cond):
     """Checks whether the passed condition is on top of the queue.
 
     The caller must make sure the queue isn't empty.
 
     """
-    return self.__pending[0] == cond
+    return cond == self.__find_first_pending_queue()[0]
 
-  def __acquire_unlocked(self, shared, timeout):
+  def __acquire_unlocked(self, shared, timeout, priority):
     """Acquire a shared lock.
 
     @param shared: whether to acquire in shared mode; by default an
         exclusive lock will be acquired
     @param timeout: maximum waiting time before giving up
+    @type priority: integer
+    @param priority: Priority for acquiring lock
 
     """
     self.__check_deleted()
@@ -628,26 +686,46 @@ class SharedLock(object):
     assert not self.__is_owned(), ("double acquire() on a non-recursive lock"
                                    " %s" % self.name)
 
+    # Remove empty entries from queue
+    self.__find_first_pending_queue()
+
     # Check whether someone else holds the lock or there are pending acquires.
     if not self.__pending and self.__can_acquire(shared):
       # Apparently not, can acquire lock directly.
       self.__do_acquire(shared)
       return True
 
-    if shared:
-      wait_condition = self.__active_shr_c
+    prioqueue = self.__pending_by_prio.get(priority, None)
 
-      # Check if we're not yet in the queue
-      if wait_condition not in self.__pending:
-        self.__pending.append(wait_condition)
+    if shared:
+      # Try to re-use condition for shared acquire
+      wait_condition = self.__pending_shared.get(priority, None)
+      assert (wait_condition is None or
+              (wait_condition.shared and wait_condition in prioqueue))
     else:
-      wait_condition = self.__condition_class(self.__lock)
-      # Always add to queue
-      self.__pending.append(wait_condition)
+      wait_condition = None
+
+    if wait_condition is None:
+      if prioqueue is None:
+        assert priority not in self.__pending_by_prio
+
+        prioqueue = []
+        heapq.heappush(self.__pending, (priority, prioqueue))
+        self.__pending_by_prio[priority] = prioqueue
+
+      wait_condition = self.__condition_class(self.__lock, shared)
+      prioqueue.append(wait_condition)
+
+      if shared:
+        # Keep reference for further shared acquires on same priority. This is
+        # better than trying to find it in the list of pending acquires.
+        assert priority not in self.__pending_shared
+        self.__pending_shared[priority] = wait_condition
 
     try:
       # Wait until we become the topmost acquire in the queue or the timeout
       # expires.
+      # TODO: Decrease timeout with spurious notifications
       while not (self.__is_on_top(wait_condition) and
                  self.__can_acquire(shared)):
         # Wait for notification
@@ -664,12 +742,15 @@ class SharedLock(object):
         return True
     finally:
       # Remove condition from queue if there are no more waiters
-      if not wait_condition.has_waiting() and not self.__deleted:
-        self.__pending.remove(wait_condition)
+      if not wait_condition.has_waiting():
+        prioqueue.remove(wait_condition)
+        if wait_condition.shared:
+          del self.__pending_shared[priority]
 
     return False
 
-  def acquire(self, shared=0, timeout=None, test_notify=None):
+  def acquire(self, shared=0, timeout=None, priority=None,
+              test_notify=None):
     """Acquire a shared lock.
 
     @type shared: integer (0/1) used as a boolean
@@ -677,17 +758,22 @@ class SharedLock(object):
         exclusive lock will be acquired
     @type timeout: float
     @param timeout: maximum waiting time before giving up
+    @type priority: integer
+    @param priority: Priority for acquiring lock
     @type test_notify: callable or None
     @param test_notify: Special callback function for unittesting
 
     """
+    if priority is None:
+      priority = _DEFAULT_PRIORITY
+
     self.__lock.acquire()
     try:
       # We already got the lock, notify now
       if __debug__ and callable(test_notify):
         test_notify()
 
-      return self.__acquire_unlocked(shared, timeout)
+      return self.__acquire_unlocked(shared, timeout, priority)
     finally:
       self.__lock.release()
 
@@ -710,18 +796,14 @@ class SharedLock(object):
         self.__shr.remove(threading.currentThread())
 
       # Notify topmost condition in queue
-      if self.__pending:
-        first_condition = self.__pending[0]
-        first_condition.notifyAll()
-
-        if first_condition == self.__active_shr_c:
-          self.__active_shr_c = self.__inactive_shr_c
-          self.__inactive_shr_c = first_condition
+      prioqueue = self.__find_first_pending_queue()
+      if prioqueue:
+        prioqueue[0].notifyAll()
 
     finally:
       self.__lock.release()
 
-  def delete(self, timeout=None):
+  def delete(self, timeout=None, priority=None):
     """Delete a Shared Lock.
 
     This operation will declare the lock for removal. First the lock will be
@@ -730,8 +812,13 @@ class SharedLock(object):
 
     @type timeout: float
     @param timeout: maximum waiting time before giving up
+    @type priority: integer
+    @param priority: Priority for acquiring lock
 
     """
+    if priority is None:
+      priority = _DEFAULT_PRIORITY
+
     self.__lock.acquire()
     try:
       assert not self.__is_sharer(), "Cannot delete() a lock while sharing it"
@@ -742,7 +829,7 @@ class SharedLock(object):
       acquired = self.__is_exclusive()
 
       if not acquired:
-        acquired = self.__acquire_unlocked(0, timeout)
+        acquired = self.__acquire_unlocked(0, timeout, priority)
 
         assert self.__is_exclusive() and not self.__is_sharer(), \
           "Lock wasn't acquired in exclusive mode"
@@ -754,8 +841,11 @@ class SharedLock(object):
         assert not (self.__exc or self.__shr), "Found owner during deletion"
 
         # Notify all acquires. They'll throw an error.
-        while self.__pending:
-          self.__pending.pop().notifyAll()
+        for (_, prioqueue) in self.__pending:
+          for cond in prioqueue:
+            cond.notifyAll()
+
+        assert self.__deleted
 
       return acquired
     finally:
@@ -908,7 +998,8 @@ class LockSet:
         self.__lock.release()
     return set(result)
 
-  def acquire(self, names, timeout=None, shared=0, test_notify=None):
+  def acquire(self, names, timeout=None, shared=0, priority=None,
+              test_notify=None):
     """Acquire a set of resource locks.
 
     @type names: list of strings (or string)
@@ -919,6 +1010,8 @@ class LockSet:
         exclusive lock will be acquired
     @type timeout: float or None
     @param timeout: Maximum time to acquire all locks
+    @type priority: integer
+    @param priority: Priority for acquiring locks
     @type test_notify: callable or None
     @param test_notify: Special callback function for unittesting
 
@@ -935,6 +1028,9 @@ class LockSet:
     assert not self._is_owned(), ("Cannot acquire locks in the same set twice"
                                   " (lockset %s)" % self.name)
 
+    if priority is None:
+      priority = _DEFAULT_PRIORITY
+
     # We need to keep track of how long we spent waiting for a lock. The
     # timeout passed to this function is over all lock acquires.
     running_timeout = RunningTimeout(timeout, False)
@@ -945,7 +1041,7 @@ class LockSet:
         if isinstance(names, basestring):
           names = [names]
 
-        return self.__acquire_inner(names, False, shared,
+        return self.__acquire_inner(names, False, shared, priority,
                                     running_timeout.Remaining, test_notify)
 
       else:
@@ -954,18 +1050,18 @@ class LockSet:
         # Some of them may then be deleted later, but we'll cope with this.
         #
         # We'd like to acquire this lock in a shared way, as it's nice if
-        # everybody else can use the instances at the same time. If are
+        # everybody else can use the instances at the same time. If we are
         # acquiring them exclusively though they won't be able to do this
         # anyway, though, so we'll get the list lock exclusively as well in
         # order to be able to do add() on the set while owning it.
-        if not self.__lock.acquire(shared=shared,
+        if not self.__lock.acquire(shared=shared, priority=priority,
                                    timeout=running_timeout.Remaining()):
           raise _AcquireTimeout()
         try:
           # note we own the set-lock
           self._add_owned()
 
-          return self.__acquire_inner(self.__names(), True, shared,
+          return self.__acquire_inner(self.__names(), True, shared, priority,
                                       running_timeout.Remaining, test_notify)
         except:
           # We shouldn't have problems adding the lock to the owners list, but
@@ -978,13 +1074,15 @@ class LockSet:
     except _AcquireTimeout:
       return None
 
-  def __acquire_inner(self, names, want_all, shared, timeout_fn, test_notify):
+  def __acquire_inner(self, names, want_all, shared, priority,
+                      timeout_fn, test_notify):
     """Inner logic for acquiring a number of locks.
 
     @param names: Names of the locks to be acquired
     @param want_all: Whether all locks in the set should be acquired
     @param shared: Whether to acquire in shared mode
     @param timeout_fn: Function returning remaining timeout
+    @param priority: Priority for acquiring locks
     @param test_notify: Special callback function for unittesting
 
     """
@@ -1028,6 +1126,7 @@ class LockSet:
         try:
           # raises LockError if the lock was deleted
           acq_success = lock.acquire(shared=shared, timeout=timeout,
+                                     priority=priority,
                                      test_notify=test_notify_fn)
         except errors.LockError:
           if want_all:
@@ -1146,6 +1245,8 @@ class LockSet:
         lock = SharedLock(self._GetLockName(lockname), monitor=self.__monitor)
 
         if acquired:
+          # No need for priority or timeout here as this lock has just been
+          # created
           lock.acquire(shared=shared)
           # now the lock cannot be deleted, we have it!
           try:
@@ -1266,7 +1367,7 @@ class GanetiLockManager:
   """
   _instance = None
 
-  def __init__(self, nodes=None, instances=None):
+  def __init__(self, nodes, instances):
     """Constructs a new GanetiLockManager object.
 
     There should be only a GanetiLockManager object at any time, so this
@@ -1351,7 +1452,7 @@ class GanetiLockManager:
     """
     return level == LEVEL_CLUSTER and (names is None or BGL in names)
 
-  def acquire(self, level, names, timeout=None, shared=0):
+  def acquire(self, level, names, timeout=None, shared=0, priority=None):
     """Acquire a set of resource locks, at the same level.
 
     @type level: member of locking.LEVELS
@@ -1364,6 +1465,8 @@ class GanetiLockManager:
         an exclusive lock will be acquired
     @type timeout: float
     @param timeout: Maximum time to acquire all locks
+    @type priority: integer
+    @param priority: Priority for acquiring lock
 
     """
     assert level in LEVELS, "Invalid locking level %s" % level
@@ -1382,7 +1485,8 @@ class GanetiLockManager:
            " while owning some at a greater one")
 
     # Acquire the locks in the set.
-    return self.__keyring[level].acquire(names, shared=shared, timeout=timeout)
+    return self.__keyring[level].acquire(names, shared=shared, timeout=timeout,
+                                         priority=priority)
 
   def release(self, level, names=None):
     """Release a set of resource locks, at the same level.
index 669c3dd..16d969f 100644 (file)
@@ -45,6 +45,7 @@ KEY_METHOD = "method"
 KEY_ARGS = "args"
 KEY_SUCCESS = "success"
 KEY_RESULT = "result"
+KEY_VERSION = "version"
 
 REQ_SUBMIT_JOB = "SubmitJob"
 REQ_SUBMIT_MANY_JOBS = "SubmitManyJobs"
@@ -70,7 +71,7 @@ DEF_RWTO = 60
 WFJC_TIMEOUT = (DEF_RWTO - 1) / 2
 
 
-class ProtocolError(errors.GenericError):
+class ProtocolError(errors.LuxiError):
   """Denotes an error in the LUXI protocol."""
 
 
@@ -274,13 +275,14 @@ def ParseRequest(msg):
 
   method = request.get(KEY_METHOD, None) # pylint: disable-msg=E1103
   args = request.get(KEY_ARGS, None) # pylint: disable-msg=E1103
+  version = request.get(KEY_VERSION, None) # pylint: disable-msg=E1103
 
   if method is None or args is None:
     logging.error("LUXI request missing method or arguments: %r", msg)
     raise ProtocolError(("Invalid LUXI request (no method or arguments"
                          " in request): %r") % msg)
 
-  return (method, args)
+  return (method, args, version)
 
 
 def ParseResponse(msg):
@@ -299,10 +301,11 @@ def ParseResponse(msg):
           KEY_RESULT in data):
     raise ProtocolError("Invalid response from server: %r" % data)
 
-  return (data[KEY_SUCCESS], data[KEY_RESULT])
+  return (data[KEY_SUCCESS], data[KEY_RESULT],
+          data.get(KEY_VERSION, None)) # pylint: disable-msg=E1103
 
 
-def FormatResponse(success, result):
+def FormatResponse(success, result, version=None):
   """Formats a LUXI response message.
 
   """
@@ -311,12 +314,15 @@ def FormatResponse(success, result):
     KEY_RESULT: result,
     }
 
+  if version is not None:
+    response[KEY_VERSION] = version
+
   logging.debug("LUXI response: %s", response)
 
   return serializer.DumpJson(response)
 
 
-def FormatRequest(method, args):
+def FormatRequest(method, args, version=None):
   """Formats a LUXI request message.
 
   """
@@ -326,22 +332,30 @@ def FormatRequest(method, args):
     KEY_ARGS: args,
     }
 
+  if version is not None:
+    request[KEY_VERSION] = version
+
   # Serialize the request
   return serializer.DumpJson(request, indent=False)
 
 
-def CallLuxiMethod(transport_cb, method, args):
+def CallLuxiMethod(transport_cb, method, args, version=None):
   """Send a LUXI request via a transport and return the response.
 
   """
   assert callable(transport_cb)
 
-  request_msg = FormatRequest(method, args)
+  request_msg = FormatRequest(method, args, version=version)
 
   # Send request and wait for response
   response_msg = transport_cb(request_msg)
 
-  (success, result) = ParseResponse(response_msg)
+  (success, result, resp_version) = ParseResponse(response_msg)
+
+  # Verify version if there was one in the response
+  if resp_version is not None and resp_version != version:
+    raise errors.LuxiError("LUXI version mismatch, client %s, response %s" %
+                           (version, resp_version))
 
   if success:
     return result
@@ -412,7 +426,8 @@ class Client(object):
     """Send a generic request and return the response.
 
     """
-    return CallLuxiMethod(self._SendMethodCall, method, args)
+    return CallLuxiMethod(self._SendMethodCall, method, args,
+                          version=constants.LUXI_VERSION)
 
   def SetQueueDrainFlag(self, drain_flag):
     return self.CallMethod(REQ_QUEUE_SET_DRAIN_FLAG, drain_flag)
index de8b462..e2577c1 100644 (file)
@@ -1554,7 +1554,7 @@ def CheckRemoteExportDiskInfo(cds, disk_index, disk_info):
   if not utils.VerifySha1Hmac(cds, msg, hmac_digest, salt=hmac_salt):
     raise errors.GenericError("HMAC is wrong")
 
-  return (netutils.HostInfo.NormalizeName(host),
+  return (netutils.Hostname.GetNormalizedName(host),
           utils.ValidateServiceName(port),
           magic)
 
index 7248f93..0cddbc6 100644 (file)
@@ -40,8 +40,8 @@ from ganeti import cmdlib
 from ganeti import locking
 
 
-class _LockAcquireTimeout(Exception):
-  """Internal exception to report timeouts on acquiring locks.
+class LockAcquireTimeout(Exception):
+  """Exception to report timeouts on acquiring locks.
 
   """
 
@@ -71,60 +71,40 @@ def _CalculateLockAttemptTimeouts():
   return result
 
 
-class _LockAttemptTimeoutStrategy(object):
+class LockAttemptTimeoutStrategy(object):
   """Class with lock acquire timeout strategy.
 
   """
   __slots__ = [
-    "_attempt",
+    "_timeouts",
     "_random_fn",
-    "_start_time",
     "_time_fn",
-    "_running_timeout",
     ]
 
   _TIMEOUT_PER_ATTEMPT = _CalculateLockAttemptTimeouts()
 
-  def __init__(self, attempt=0, _time_fn=time.time, _random_fn=random.random):
+  def __init__(self, _time_fn=time.time, _random_fn=random.random):
     """Initializes this class.
 
-    @type attempt: int
-    @param attempt: Current attempt number
     @param _time_fn: Time function for unittests
     @param _random_fn: Random number generator for unittests
 
     """
     object.__init__(self)
 
-    if attempt < 0:
-      raise ValueError("Attempt must be zero or positive")
-
-    self._attempt = attempt
+    self._timeouts = iter(self._TIMEOUT_PER_ATTEMPT)
     self._time_fn = _time_fn
     self._random_fn = _random_fn
 
-    try:
-      timeout = self._TIMEOUT_PER_ATTEMPT[attempt]
-    except IndexError:
-      # No more timeouts, do blocking acquire
-      timeout = None
-
-    self._running_timeout = locking.RunningTimeout(timeout, False,
-                                                   _time_fn=_time_fn)
-
   def NextAttempt(self):
-    """Returns the strategy for the next attempt.
+    """Returns the timeout for the next attempt.
 
     """
-    return _LockAttemptTimeoutStrategy(attempt=self._attempt + 1,
-                                       _time_fn=self._time_fn,
-                                       _random_fn=self._random_fn)
-
-  def CalcRemainingTimeout(self):
-    """Returns the remaining timeout.
-
-    """
-    timeout = self._running_timeout.Remaining()
+    try:
+      timeout = self._timeouts.next()
+    except StopIteration:
+      # No more timeouts, do blocking acquire
+      timeout = None
 
     if timeout is not None:
       # Add a small variation (-/+ 5%) to timeout. This helps in situations
@@ -238,7 +218,7 @@ class Processor(object):
     self.rpc = rpc.RpcRunner(context.cfg)
     self.hmclass = HooksMaster
 
-  def _AcquireLocks(self, level, names, shared, timeout):
+  def _AcquireLocks(self, level, names, shared, timeout, priority):
     """Acquires locks via the Ganeti lock manager.
 
     @type level: int
@@ -249,13 +229,18 @@ class Processor(object):
     @param shared: Whether the locks should be acquired in shared mode
     @type timeout: None or float
     @param timeout: Timeout for acquiring the locks
+    @raise LockAcquireTimeout: In case locks couldn't be acquired in specified
+        amount of time
 
     """
     if self._cbs:
       self._cbs.CheckCancel()
 
     acquired = self.context.glm.acquire(level, names, shared=shared,
-                                        timeout=timeout)
+                                        timeout=timeout, priority=priority)
+
+    if acquired is None:
+      raise LockAcquireTimeout()
 
     return acquired
 
@@ -290,7 +275,7 @@ class Processor(object):
 
     return result
 
-  def _LockAndExecLU(self, lu, level, calc_timeout):
+  def _LockAndExecLU(self, lu, level, calc_timeout, priority):
     """Execute a Logical Unit, with the needed locks.
 
     This is a recursive function that starts locking the given level, and
@@ -325,11 +310,7 @@ class Processor(object):
           needed_locks = lu.needed_locks[level]
 
           acquired = self._AcquireLocks(level, needed_locks, share,
-                                        calc_timeout())
-
-          if acquired is None:
-            raise _LockAcquireTimeout()
-
+                                        calc_timeout(), priority)
         else:
           # Adding locks
           add_locks = lu.add_locks[level]
@@ -348,7 +329,7 @@ class Processor(object):
         try:
           lu.acquired_locks[level] = acquired
 
-          result = self._LockAndExecLU(lu, level + 1, calc_timeout)
+          result = self._LockAndExecLU(lu, level + 1, calc_timeout, priority)
         finally:
           if level in lu.remove_locks:
             self.context.glm.remove(level, lu.remove_locks[level])
@@ -357,63 +338,59 @@ class Processor(object):
           self.context.glm.release(level)
 
     else:
-      result = self._LockAndExecLU(lu, level + 1, calc_timeout)
+      result = self._LockAndExecLU(lu, level + 1, calc_timeout, priority)
 
     return result
 
-  def ExecOpCode(self, op, cbs):
+  def ExecOpCode(self, op, cbs, timeout=None, priority=None):
     """Execute an opcode.
 
     @type op: an OpCode instance
     @param op: the opcode to be executed
     @type cbs: L{OpExecCbBase}
     @param cbs: Runtime callbacks
+    @type timeout: float or None
+    @param timeout: Maximum time to acquire all locks, None for no timeout
+    @type priority: number or None
+    @param priority: Priority for acquiring lock(s)
+    @raise LockAcquireTimeout: In case locks couldn't be acquired in specified
+        amount of time
 
     """
     if not isinstance(op, opcodes.OpCode):
       raise errors.ProgrammerError("Non-opcode instance passed"
                                    " to ExecOpcode")
 
+    lu_class = self.DISPATCH_TABLE.get(op.__class__, None)
+    if lu_class is None:
+      raise errors.OpCodeUnknown("Unknown opcode")
+
+    if timeout is None:
+      calc_timeout = lambda: None
+    else:
+      calc_timeout = locking.RunningTimeout(timeout, False).Remaining
+
     self._cbs = cbs
     try:
-      lu_class = self.DISPATCH_TABLE.get(op.__class__, None)
-      if lu_class is None:
-        raise errors.OpCodeUnknown("Unknown opcode")
-
-      timeout_strategy = _LockAttemptTimeoutStrategy()
+      # Acquire the Big Ganeti Lock exclusively if this LU requires it,
+      # and in a shared fashion otherwise (to prevent concurrent run with
+      # an exclusive LU.
+      self._AcquireLocks(locking.LEVEL_CLUSTER, locking.BGL,
+                          not lu_class.REQ_BGL, calc_timeout(),
+                          priority)
+      try:
+        lu = lu_class(self, op, self.context, self.rpc)
+        lu.ExpandNames()
+        assert lu.needed_locks is not None, "needed_locks not set by LU"
 
-      while True:
         try:
-          acquire_timeout = timeout_strategy.CalcRemainingTimeout()
-
-          # Acquire the Big Ganeti Lock exclusively if this LU requires it,
-          # and in a shared fashion otherwise (to prevent concurrent run with
-          # an exclusive LU.
-          if self._AcquireLocks(locking.LEVEL_CLUSTER, locking.BGL,
-                                not lu_class.REQ_BGL, acquire_timeout) is None:
-            raise _LockAcquireTimeout()
-
-          try:
-            lu = lu_class(self, op, self.context, self.rpc)
-            lu.ExpandNames()
-            assert lu.needed_locks is not None, "needed_locks not set by LU"
-
-            try:
-              return self._LockAndExecLU(lu, locking.LEVEL_INSTANCE,
-                                         timeout_strategy.CalcRemainingTimeout)
-            finally:
-              if self._ec_id:
-                self.context.cfg.DropECReservations(self._ec_id)
-
-          finally:
-            self.context.glm.release(locking.LEVEL_CLUSTER)
-
-        except _LockAcquireTimeout:
-          # Timeout while waiting for lock, try again
-          pass
-
-        timeout_strategy = timeout_strategy.NextAttempt()
-
+          return self._LockAndExecLU(lu, locking.LEVEL_INSTANCE, calc_timeout,
+                                     priority)
+        finally:
+          if self._ec_id:
+            self.context.cfg.DropECReservations(self._ec_id)
+      finally:
+        self.context.glm.release(locking.LEVEL_CLUSTER)
     finally:
       self._cbs = None
 
index 99e1181..9dbf97b 100644 (file)
@@ -62,75 +62,104 @@ def GetSocketCredentials(sock):
   return struct.unpack(_STRUCT_UCRED, peercred)
 
 
-def GetHostInfo(name=None):
-  """Lookup host name and raise an OpPrereqError for failures"""
+def GetHostname(name=None, family=None):
+  """Returns a Hostname object.
 
+  @type name: str
+  @param name: hostname or None
+  @type family: int
+  @param family: AF_INET | AF_INET6 | None
+  @rtype: L{Hostname}
+  @return: Hostname object
+  @raise errors.OpPrereqError: in case of errors in resolving
+
+  """
   try:
-    return HostInfo(name)
+    return Hostname(name=name, family=family)
   except errors.ResolverError, err:
     raise errors.OpPrereqError("The given name (%s) does not resolve: %s" %
                                (err[0], err[2]), errors.ECODE_RESOLVER)
 
 
-class HostInfo:
-  """Class implementing resolver and hostname functionality
+class Hostname:
+  """Class implementing resolver and hostname functionality.
 
   """
   _VALID_NAME_RE = re.compile("^[a-z0-9._-]{1,255}$")
 
-  def __init__(self, name=None):
+  def __init__(self, name=None, family=None):
     """Initialize the host name object.
 
-    If the name argument is not passed, it will use this system's
-    name.
+    If the name argument is None, it will use this system's name.
 
-    """
-    if name is None:
-      name = self.SysName()
+    @type family: int
+    @param family: AF_INET | AF_INET6 | None
+    @type name: str
+    @param name: hostname or None
 
-    self.query = name
-    self.name, self.aliases, self.ipaddrs = self.LookupHostname(name)
-    self.ip = self.ipaddrs[0]
+    """
+    self.name = self.GetNormalizedName(self.GetFqdn(name))
+    self.ip = self.GetIP(self.name, family=family)
 
-  def ShortName(self):
-    """Returns the hostname without domain.
+  @classmethod
+  def GetSysName(cls):
+    """Legacy method the get the current system's name.
 
     """
-    return self.name.split('.')[0]
+    return cls.GetFqdn()
 
   @staticmethod
-  def SysName():
-    """Return the current system's name.
+  def GetFqdn(hostname=None):
+    """Return fqdn.
+
+    If hostname is None the system's fqdn is returned.
 
-    This is simply a wrapper over C{socket.gethostname()}.
+    @type hostname: str
+    @param hostname: name to be fqdn'ed
+    @rtype: str
+    @return: fqdn of given name, if it exists, unmodified name otherwise
 
     """
-    return socket.gethostname()
+    if hostname is None:
+      return socket.getfqdn()
+    else:
+      return socket.getfqdn(hostname)
 
   @staticmethod
-  def LookupHostname(hostname):
-    """Look up hostname
+  def GetIP(hostname, family=None):
+    """Return IP address of given hostname.
+
+    Supports both IPv4 and IPv6.
 
     @type hostname: str
     @param hostname: hostname to look up
-
-    @rtype: tuple
-    @return: a tuple (name, aliases, ipaddrs) as returned by
-        C{socket.gethostbyname_ex}
+    @type family: int
+    @param family: AF_INET | AF_INET6 | None
+    @rtype: str
+    @return: IP address
     @raise errors.ResolverError: in case of errors in resolving
 
     """
     try:
-      result = socket.gethostbyname_ex(hostname)
+      if family in (socket.AF_INET, socket.AF_INET6):
+        result = socket.getaddrinfo(hostname, None, family)
+      else:
+        result = socket.getaddrinfo(hostname, None)
     except (socket.gaierror, socket.herror, socket.error), err:
       # hostname not found in DNS, or other socket exception in the
       # (code, description format)
       raise errors.ResolverError(hostname, err.args[0], err.args[1])
 
-    return result
+    # getaddrinfo() returns a list of 5-tupes (family, socktype, proto,
+    # canonname, sockaddr). We return the first tuple's first address in
+    # sockaddr
+    try:
+      return result[0][4][0]
+    except IndexError, err:
+      raise errors.ResolverError("Unknown error in getaddrinfo(): %s" % err)
 
   @classmethod
-  def NormalizeName(cls, hostname):
+  def GetNormalizedName(cls, hostname):
     """Validate and normalize the given hostname.
 
     @attention: the validation is a bit more relaxed than the standards
@@ -151,84 +180,6 @@ class HostInfo:
     return hostname
 
 
-def _GenericIsValidIP(family, ip):
-  """Generic internal version of ip validation.
-
-  @type family: int
-  @param family: socket.AF_INET | socket.AF_INET6
-  @type ip: str
-  @param ip: the address to be checked
-  @rtype: boolean
-  @return: True if ip is valid, False otherwise
-
-  """
-  try:
-    socket.inet_pton(family, ip)
-    return True
-  except socket.error:
-    return False
-
-
-def IsValidIP4(ip):
-  """Verifies an IPv4 address.
-
-  This function checks if the given address is a valid IPv4 address.
-
-  @type ip: str
-  @param ip: the address to be checked
-  @rtype: boolean
-  @return: True if ip is valid, False otherwise
-
-  """
-  return _GenericIsValidIP(socket.AF_INET, ip)
-
-
-def IsValidIP6(ip):
-  """Verifies an IPv6 address.
-
-  This function checks if the given address is a valid IPv6 address.
-
-  @type ip: str
-  @param ip: the address to be checked
-  @rtype: boolean
-  @return: True if ip is valid, False otherwise
-
-  """
-  return _GenericIsValidIP(socket.AF_INET6, ip)
-
-
-def IsValidIP(ip):
-  """Verifies an IP address.
-
-  This function checks if the given IP address (both IPv4 and IPv6) is valid.
-
-  @type ip: str
-  @param ip: the address to be checked
-  @rtype: boolean
-  @return: True if ip is valid, False otherwise
-
-  """
-  return IsValidIP4(ip) or IsValidIP6(ip)
-
-
-def GetAddressFamily(ip):
-  """Get the address family of the given address.
-
-  @type ip: str
-  @param ip: ip address whose family will be returned
-  @rtype: int
-  @return: socket.AF_INET or socket.AF_INET6
-  @raise errors.GenericError: for invalid addresses
-
-  """
-  if IsValidIP6(ip):
-    return socket.AF_INET6
-  elif IsValidIP4(ip):
-    return socket.AF_INET
-  else:
-    raise errors.GenericError("Address %s not valid" % ip)
-
-
 def TcpPing(target, port, timeout=10, live_port_needed=False, source=None):
   """Simple ping implementation using TCP connect(2).
 
@@ -251,7 +202,7 @@ def TcpPing(target, port, timeout=10, live_port_needed=False, source=None):
 
   """
   try:
-    family = GetAddressFamily(target)
+    family = IPAddress.GetAddressFamily(target)
   except errors.GenericError:
     return False
 
@@ -279,32 +230,6 @@ def TcpPing(target, port, timeout=10, live_port_needed=False, source=None):
   return success
 
 
-def OwnIpAddress(address):
-  """Check if the current host has the the given IP address.
-
-  This is done by trying to bind the given address. We return True if we
-  succeed or false if a socket.error is raised.
-
-  @type address: string
-  @param address: the address to check
-  @rtype: bool
-  @return: True if we own the address
-
-  """
-  family = GetAddressFamily(address)
-  s = socket.socket(family, socket.SOCK_DGRAM)
-  success = False
-  try:
-    try:
-      s.bind((address, 0))
-      success = True
-    except socket.error:
-      success = False
-  finally:
-    s.close()
-  return success
-
-
 def GetDaemonPort(daemon_name):
   """Get the daemon port for this cluster.
 
@@ -327,3 +252,272 @@ def GetDaemonPort(daemon_name):
     port = default_port
 
   return port
+
+
+class IPAddress(object):
+  """Class that represents an IP address.
+
+  """
+  iplen = 0
+  family = None
+  loopback_cidr = None
+
+  @staticmethod
+  def _GetIPIntFromString(address):
+    """Abstract method to please pylint.
+
+    """
+    raise NotImplementedError
+
+  @classmethod
+  def IsValid(cls, address):
+    """Validate a IP address.
+
+    @type address: str
+    @param address: IP address to be checked
+    @rtype: bool
+    @return: True if valid, False otherwise
+
+    """
+    if cls.family is None:
+      try:
+        family = cls.GetAddressFamily(address)
+      except errors.IPAddressError:
+        return False
+    else:
+      family = cls.family
+
+    try:
+      socket.inet_pton(family, address)
+      return True
+    except socket.error:
+      return False
+
+  @classmethod
+  def Own(cls, address):
+    """Check if the current host has the the given IP address.
+
+    This is done by trying to bind the given address. We return True if we
+    succeed or false if a socket.error is raised.
+
+    @type address: str
+    @param address: IP address to be checked
+    @rtype: bool
+    @return: True if we own the address, False otherwise
+
+    """
+    if cls.family is None:
+      try:
+        family = cls.GetAddressFamily(address)
+      except errors.IPAddressError:
+        return False
+    else:
+      family = cls.family
+
+    s = socket.socket(family, socket.SOCK_DGRAM)
+    success = False
+    try:
+      try:
+        s.bind((address, 0))
+        success = True
+      except socket.error:
+        success = False
+    finally:
+      s.close()
+    return success
+
+  @classmethod
+  def InNetwork(cls, cidr, address):
+    """Determine whether an address is within a network.
+
+    @type cidr: string
+    @param cidr: Network in CIDR notation, e.g. '192.0.2.0/24', '2001:db8::/64'
+    @type address: str
+    @param address: IP address
+    @rtype: bool
+    @return: True if address is in cidr, False otherwise
+
+    """
+    address_int = cls._GetIPIntFromString(address)
+    subnet = cidr.split("/")
+    assert len(subnet) == 2
+    try:
+      prefix = int(subnet[1])
+    except ValueError:
+      return False
+
+    assert 0 <= prefix <= cls.iplen
+    target_int = cls._GetIPIntFromString(subnet[0])
+    # Convert prefix netmask to integer value of netmask
+    netmask_int = (2**cls.iplen)-1 ^ ((2**cls.iplen)-1 >> prefix)
+    # Calculate hostmask
+    hostmask_int = netmask_int ^ (2**cls.iplen)-1
+    # Calculate network address by and'ing netmask
+    network_int = target_int & netmask_int
+    # Calculate broadcast address by or'ing hostmask
+    broadcast_int = target_int | hostmask_int
+
+    return network_int <= address_int <= broadcast_int
+
+  @staticmethod
+  def GetAddressFamily(address):
+    """Get the address family of the given address.
+
+    @type address: str
+    @param address: ip address whose family will be returned
+    @rtype: int
+    @return: socket.AF_INET or socket.AF_INET6
+    @raise errors.GenericError: for invalid addresses
+
+    """
+    try:
+      return IP4Address(address).family
+    except errors.IPAddressError:
+      pass
+
+    try:
+      return IP6Address(address).family
+    except errors.IPAddressError:
+      pass
+
+    raise errors.IPAddressError("Invalid address '%s'" % address)
+
+  @classmethod
+  def IsLoopback(cls, address):
+    """Determine whether it is a loopback address.
+
+    @type address: str
+    @param address: IP address to be checked
+    @rtype: bool
+    @return: True if loopback, False otherwise
+
+    """
+    try:
+      return cls.InNetwork(cls.loopback_cidr, address)
+    except errors.IPAddressError:
+      return False
+
+
+class IP4Address(IPAddress):
+  """IPv4 address class.
+
+  """
+  iplen = 32
+  family = socket.AF_INET
+  loopback_cidr = "127.0.0.0/8"
+
+  def __init__(self, address):
+    """Constructor for IPv4 address.
+
+    @type address: str
+    @param address: IP address
+    @raises errors.IPAddressError: if address invalid
+
+    """
+    IPAddress.__init__(self)
+    if not self.IsValid(address):
+      raise errors.IPAddressError("IPv4 Address %s invalid" % address)
+
+    self.address = address
+
+  @staticmethod
+  def _GetIPIntFromString(address):
+    """Get integer value of IPv4 address.
+
+    @type address: str
+    @param address: IPv6 address
+    @rtype: int
+    @return: integer value of given IP address
+
+    """
+    address_int = 0
+    parts = address.split(".")
+    assert len(parts) == 4
+    for part in parts:
+      address_int = (address_int << 8) | int(part)
+
+    return address_int
+
+
+class IP6Address(IPAddress):
+  """IPv6 address class.
+
+  """
+  iplen = 128
+  family = socket.AF_INET6
+  loopback_cidr = "::1/128"
+
+  def __init__(self, address):
+    """Constructor for IPv6 address.
+
+    @type address: str
+    @param address: IP address
+    @raises errors.IPAddressError: if address invalid
+
+    """
+    IPAddress.__init__(self)
+    if not self.IsValid(address):
+      raise errors.IPAddressError("IPv6 Address [%s] invalid" % address)
+    self.address = address
+
+  @staticmethod
+  def _GetIPIntFromString(address):
+    """Get integer value of IPv6 address.
+
+    @type address: str
+    @param address: IPv6 address
+    @rtype: int
+    @return: integer value of given IP address
+
+    """
+    doublecolons = address.count("::")
+    assert not doublecolons > 1
+    if doublecolons == 1:
+      # We have a shorthand address, expand it
+      parts = []
+      twoparts = address.split("::")
+      sep = len(twoparts[0].split(':')) + len(twoparts[1].split(':'))
+      parts = twoparts[0].split(':')
+      [parts.append("0") for _ in range(8 - sep)]
+      parts += twoparts[1].split(':')
+    else:
+      parts = address.split(":")
+
+    address_int = 0
+    for part in parts:
+      address_int = (address_int << 16) + int(part or '0', 16)
+
+    return address_int
+
+
+def FormatAddress(address, family=None):
+  """Format a socket address
+
+  @type address: family specific (usually tuple)
+  @param address: address, as reported by this class
+  @type family: integer
+  @param family: socket family (one of socket.AF_*) or None
+
+  """
+  if family is None:
+    try:
+      family = IPAddress.GetAddressFamily(address[0])
+    except errors.IPAddressError:
+      raise errors.ParameterError(address)
+
+  if family == socket.AF_UNIX and len(address) == 3:
+    return "pid=%s, uid=%s, gid=%s" % address
+
+  if family in (socket.AF_INET, socket.AF_INET6) and len(address) == 2:
+    host, port = address
+    if family == socket.AF_INET6:
+      res = "[%s]" % host
+    else:
+      res = host
+
+    if port is not None:
+      res += ":%s" % port
+
+    return res
+
+  raise errors.ParameterError(family, address)
index 01c914d..768bf99 100644 (file)
@@ -41,9 +41,11 @@ from cStringIO import StringIO
 from ganeti import errors
 from ganeti import constants
 
+from socket import AF_INET
+
 
 __all__ = ["ConfigObject", "ConfigData", "NIC", "Disk", "Instance",
-           "OS", "Node", "Cluster", "FillDict"]
+           "OS", "Node", "NodeGroup", "Cluster", "FillDict"]
 
 _TIMESTAMPS = ["ctime", "mtime"]
 _UUID = ["uuid"]
@@ -200,6 +202,8 @@ class ConfigObject(object):
     if not isinstance(c_type, type):
       raise TypeError("Container type %s passed to _ContainerFromDicts is"
                       " not a type" % type(c_type))
+    if source is None:
+      source = c_type()
     if c_type is dict:
       ret = dict([(k, e_type.FromDict(v)) for k, v in source.iteritems()])
     elif c_type in (list, tuple, set, frozenset):
@@ -312,8 +316,14 @@ class TaggableObject(ConfigObject):
 
 class ConfigData(ConfigObject):
   """Top-level config object."""
-  __slots__ = (["version", "cluster", "nodes", "instances", "serial_no"] +
-               _TIMESTAMPS)
+  __slots__ = [
+    "version",
+    "cluster",
+    "nodes",
+    "nodegroups",
+    "instances",
+    "serial_no",
+    ] + _TIMESTAMPS
 
   def ToDict(self):
     """Custom function for top-level config data.
@@ -324,7 +334,7 @@ class ConfigData(ConfigObject):
     """
     mydict = super(ConfigData, self).ToDict()
     mydict["cluster"] = mydict["cluster"].ToDict()
-    for key in "nodes", "instances":
+    for key in "nodes", "instances", "nodegroups":
       mydict[key] = self._ContainerToDicts(mydict[key])
 
     return mydict
@@ -338,6 +348,7 @@ class ConfigData(ConfigObject):
     obj.cluster = Cluster.FromDict(obj.cluster)
     obj.nodes = cls._ContainerFromDicts(obj.nodes, dict, Node)
     obj.instances = cls._ContainerFromDicts(obj.instances, dict, Instance)
+    obj.nodegroups = cls._ContainerFromDicts(obj.nodegroups, dict, NodeGroup)
     return obj
 
   def HasAnyDiskOfType(self, dev_type):
@@ -364,6 +375,10 @@ class ConfigData(ConfigObject):
       node.UpgradeConfig()
     for instance in self.instances.values():
       instance.UpgradeConfig()
+    if self.nodegroups is None:
+      self.nodegroups = {}
+    for nodegroup in self.nodegroups.values():
+      nodegroup.UpgradeConfig()
     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
@@ -924,8 +939,53 @@ class Node(TaggableObject):
     "master_candidate",
     "offline",
     "drained",
+    "group",
+    "master_capable",
+    "vm_capable",
+    ] + _TIMESTAMPS + _UUID
+
+  def UpgradeConfig(self):
+    """Fill defaults for missing configuration values.
+
+    """
+    # pylint: disable-msg=E0203
+    # because these are "defined" via slots, not manually
+    if self.master_capable is None:
+      self.master_capable = True
+
+    if self.vm_capable is None:
+      self.vm_capable = True
+
+
+class NodeGroup(ConfigObject):
+  """Config object representing a node group."""
+  __slots__ = [
+    "name",
+    "members",
     ] + _TIMESTAMPS + _UUID
 
+  def ToDict(self):
+    """Custom function for nodegroup.
+
+    This discards the members object, which gets recalculated and is only kept
+    in memory.
+
+    """
+    mydict = super(NodeGroup, self).ToDict()
+    del mydict["members"]
+    return mydict
+
+  @classmethod
+  def FromDict(cls, val):
+    """Custom function for nodegroup.
+
+    The members slot is initialized to an empty list, upon deserialization.
+
+    """
+    obj = super(NodeGroup, cls).FromDict(val)
+    obj.members = []
+    return obj
+
 
 class Cluster(TaggableObject):
   """Config object representing the cluster."""
@@ -959,6 +1019,8 @@ class Cluster(TaggableObject):
     "default_iallocator",
     "hidden_os",
     "blacklisted_os",
+    "primary_ip_family",
+    "prealloc_wipe_disks",
     ] + _TIMESTAMPS + _UUID
 
   def UpgradeConfig(self):
@@ -1031,6 +1093,13 @@ class Cluster(TaggableObject):
     if self.blacklisted_os is None:
       self.blacklisted_os = []
 
+    # primary_ip_family added before 2.3
+    if self.primary_ip_family is None:
+      self.primary_ip_family = AF_INET
+
+    if self.prealloc_wipe_disks is None:
+      self.prealloc_wipe_disks = False
+
   def ToDict(self):
     """Custom function for cluster.
 
index 96d3436..b916287 100644 (file)
@@ -117,10 +117,11 @@ class OpCode(BaseOpCode):
                children of this class.
   @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
 
   """
   OP_ID = "OP_ABSTRACT"
-  __slots__ = ["dry_run", "debug_level"]
+  __slots__ = ["dry_run", "debug_level", "priority"]
 
   def __getstate__(self):
     """Specialized getstate for opcodes.
@@ -317,6 +318,7 @@ class OpSetClusterParams(OpCode):
     "reserved_lvs",
     "hidden_os",
     "blacklisted_os",
+    "prealloc_wipe_disks",
     ]
 
 
@@ -362,11 +364,18 @@ class OpAddNode(OpCode):
                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_ID = "OP_NODE_ADD"
   OP_DSC_FIELD = "node_name"
-  __slots__ = ["node_name", "primary_ip", "secondary_ip", "readd"]
+  __slots__ = ["node_name", "primary_ip", "secondary_ip", "readd", "group",
+               "vm_capable", "master_capable"]
 
 
 class OpQueryNodes(OpCode):
@@ -426,6 +435,9 @@ class OpSetNodeParams(OpCode):
     "offline",
     "drained",
     "auto_promote",
+    "master_capable",
+    "vm_capable",
+    "secondary_ip",
     ]
 
 
@@ -491,7 +503,7 @@ class OpReinstallInstance(OpCode):
   """Reinstall an instance's OS."""
   OP_ID = "OP_INSTANCE_REINSTALL"
   OP_DSC_FIELD = "instance_name"
-  __slots__ = ["instance_name", "os_type", "force_variant"]
+  __slots__ = ["instance_name", "os_type", "force_variant", "osparams"]
 
 
 class OpRemoveInstance(OpCode):
@@ -518,7 +530,7 @@ class OpStartupInstance(OpCode):
   OP_ID = "OP_INSTANCE_STARTUP"
   OP_DSC_FIELD = "instance_name"
   __slots__ = [
-    "instance_name", "force", "hvparams", "beparams",
+    "instance_name", "force", "hvparams", "beparams", "ignore_offline_nodes",
     ]
 
 
@@ -526,7 +538,9 @@ class OpShutdownInstance(OpCode):
   """Shutdown an instance."""
   OP_ID = "OP_INSTANCE_SHUTDOWN"
   OP_DSC_FIELD = "instance_name"
-  __slots__ = ["instance_name", "timeout"]
+  __slots__ = [
+    "instance_name", "timeout", "ignore_offline_nodes",
+    ]
 
 
 class OpRebootInstance(OpCode):
@@ -803,6 +817,18 @@ class OpTestJobqueue(OpCode):
     ]
 
 
+class OpTestDummy(OpCode):
+  """Utility opcode used by unittests.
+
+  """
+  OP_ID = "OP_TEST_DUMMY"
+  __slots__ = [
+    "result",
+    "messages",
+    "fail",
+    ]
+
+
 OP_MAPPING = dict([(v.OP_ID, v) for v in globals().values()
                    if (isinstance(v, type) and issubclass(v, OpCode) and
                        hasattr(v, "OP_ID"))])
index 5a549e7..ed3eb62 100644 (file)
@@ -35,6 +35,7 @@
 
 import logging
 import simplejson
+import socket
 import urllib
 import threading
 import pycurl
@@ -265,7 +266,13 @@ class GanetiRapiClient(object):
     self._curl_config_fn = curl_config_fn
     self._curl_factory = curl_factory
 
-    self._base_url = "https://%s:%s" % (host, port)
+    try:
+      socket.inet_pton(socket.AF_INET6, host)
+      address = "[%s]:%s" % (host, port)
+    except socket.error:
+      address = "%s:%s" % (host, port)
+
+    self._base_url = "https://%s" % address
 
     if username is not None:
       if password is None:
index 648aa0b..ead1876 100644 (file)
@@ -31,6 +31,7 @@ import re
 
 from ganeti import constants
 from ganeti import http
+from ganeti import utils
 
 from ganeti.rapi import baserlib
 from ganeti.rapi import rlib2
@@ -76,25 +77,15 @@ class Mapper:
       query = None
       args = {}
 
-    result = None
+    # Try to find handler for request path
+    result = utils.FindMatch(self._connector, path)
 
-    for key, handler in self._connector.iteritems():
-      # Regex objects
-      if hasattr(key, "match"):
-        m = key.match(path)
-        if m:
-          result = (handler, list(m.groups()), args)
-          break
+    if result is None:
+      raise http.HttpNotFound()
 
-      # String objects
-      elif key == path:
-        result = (handler, [], args)
-        break
+    (handler, groups) = result
 
-    if result:
-      return result
-    else:
-      raise http.HttpNotFound()
+    return (handler, groups, args)
 
 
 class R_root(baserlib.R_Generic):
index 0d8367a..bdbc4fa 100644 (file)
@@ -59,6 +59,7 @@ I_FIELDS = ["name", "admin_state", "os",
             "disk.sizes", "disk_usage",
             "beparams", "hvparams",
             "oper_state", "oper_ram", "oper_vcpus", "status",
+            "custom_hvparams", "custom_beparams", "custom_nicparams",
             ] + _COMMON_FIELDS
 
 N_FIELDS = ["name", "offline", "master_candidate", "drained",
@@ -68,6 +69,7 @@ N_FIELDS = ["name", "offline", "master_candidate", "drained",
             "ctotal", "cnodes", "csockets",
             "pip", "sip", "role",
             "pinst_list", "sinst_list",
+            "master_capable", "vm_capable",
             ] + _COMMON_FIELDS
 
 _NR_DRAINED = "drained"
index 37e8c4e..4ec0ed4 100644 (file)
@@ -44,6 +44,7 @@ from ganeti import serializer
 from ganeti import constants
 from ganeti import errors
 from ganeti import netutils
+from ganeti import ssconf
 
 # pylint has a bug here, doesn't see this import
 import ganeti.http.client  # pylint: disable-msg=W0611
@@ -118,7 +119,12 @@ def _ConfigRpcCurl(curl):
   curl.setopt(pycurl.CONNECTTIMEOUT, _RPC_CONNECT_TIMEOUT)
 
 
-class _RpcThreadLocal(threading.local):
+# Aliasing this module avoids the following warning by epydoc: "Warning: No
+# information available for ganeti.rpc._RpcThreadLocal's base threading.local"
+_threading = threading
+
+
+class _RpcThreadLocal(_threading.local):
   def GetHttpClientPool(self):
     """Returns a per-thread HTTP client pool.
 
@@ -134,6 +140,10 @@ class _RpcThreadLocal(threading.local):
     return pool
 
 
+# Remove module alias (see above)
+del _threading
+
+
 _thread_local = _RpcThreadLocal()
 
 
@@ -257,6 +267,35 @@ class RpcResult(object):
     raise ec(*args) # pylint: disable-msg=W0142
 
 
+def _AddressLookup(node_list,
+                   ssc=ssconf.SimpleStore,
+                   nslookup_fn=netutils.Hostname.GetIP):
+  """Return addresses for given node names.
+
+  @type node_list: list
+  @param node_list: List of node names
+  @type ssc: class
+  @param ssc: SimpleStore class that is used to obtain node->ip mappings
+  @type nslookup_fn: callable
+  @param nslookup_fn: function use to do NS lookup
+  @rtype: list of addresses and/or None's
+  @returns: List of corresponding addresses, if found
+
+  """
+  ss = ssc()
+  iplist = ss.GetNodePrimaryIPList()
+  family = ss.GetPrimaryIPFamily()
+  addresses = []
+  ipmap = dict(entry.split() for entry in iplist)
+  for node in node_list:
+    address = ipmap.get(node)
+    if address is None:
+      address = nslookup_fn(node, family=family)
+    addresses.append(address)
+
+  return addresses
+
+
 class Client:
   """RPC Client class.
 
@@ -269,13 +308,14 @@ class Client:
   cause bugs.
 
   """
-  def __init__(self, procedure, body, port):
+  def __init__(self, procedure, body, port, address_lookup_fn=_AddressLookup):
     assert procedure in _TIMEOUTS, ("New RPC call not declared in the"
                                     " timeouts table")
     self.procedure = procedure
     self.body = body
     self.port = port
     self._request = {}
+    self._address_lookup_fn = address_lookup_fn
 
   def ConnectList(self, node_list, address_list=None, read_timeout=None):
     """Add a list of nodes to the target nodes.
@@ -286,15 +326,16 @@ class Client:
     @keyword address_list: either None or a list with node addresses,
         which must have the same length as the node list
     @type read_timeout: int
-    @param read_timeout: overwrites the default read timeout for the
-        given operation
+    @param read_timeout: overwrites default timeout for operation
 
     """
     if address_list is None:
-      address_list = [None for _ in node_list]
-    else:
-      assert len(node_list) == len(address_list), \
-             "Name and address lists should have the same length"
+      # Always use IP address instead of node name
+      address_list = self._address_lookup_fn(node_list)
+
+    assert len(node_list) == len(address_list), \
+           "Name and address lists must have the same length"
+
     for node, address in zip(node_list, address_list):
       self.ConnectNode(node, address, read_timeout=read_timeout)
 
@@ -304,11 +345,16 @@ class Client:
     @type name: str
     @param name: the node name
     @type address: str
-    @keyword address: the node address, if known
+    @param address: the node address, if known
+    @type read_timeout: int
+    @param read_timeout: overwrites default timeout for operation
 
     """
     if address is None:
-      address = name
+      # Always use IP address instead of node name
+      address = self._address_lookup_fn([name])[0]
+
+    assert(address is not None)
 
     if read_timeout is None:
       read_timeout = _TIMEOUTS[self.procedure]
@@ -396,7 +442,7 @@ class RpcRunner(object):
     @type bep: dict or None
     @param bep: a dictionary with overridden backend parameters
     @type osp: dict or None
-    @param osp: a dictionary with overriden os parameters
+    @param osp: a dictionary with overridden os parameters
     @rtype: dict
     @return: the instance dict, with the hvparams filled with the
         cluster defaults
@@ -715,14 +761,15 @@ class RpcRunner(object):
                                  shutdown_timeout])
 
   @_RpcTimeout(_TMO_1DAY)
-  def call_instance_os_add(self, node, inst, reinstall, debug):
+  def call_instance_os_add(self, node, inst, reinstall, debug, osparams=None):
     """Installs an OS on the given instance.
 
     This is a single-node call.
 
     """
     return self._SingleNodeCall(node, "instance_os_add",
-                                [self._InstDict(inst), reinstall, debug])
+                                [self._InstDict(inst, osp=osparams),
+                                 reinstall, debug])
 
   @_RpcTimeout(_TMO_SLOW)
   def call_instance_run_rename(self, node, inst, old_name, debug):
@@ -838,14 +885,20 @@ class RpcRunner(object):
                                [vg_name, hypervisor_type])
 
   @_RpcTimeout(_TMO_NORMAL)
-  def call_node_add(self, node, dsa, dsapub, rsa, rsapub, ssh, sshpub):
-    """Add a node to the cluster.
+  def call_etc_hosts_modify(self, node, mode, name, ip):
+    """Modify hosts file with name
 
-    This is a single-node call.
+    @type node: string
+    @param node: The node to call
+    @type mode: string
+    @param mode: The mode to operate. Currently "add" or "remove"
+    @type name: string
+    @param name: The host name to be modified
+    @type ip: string
+    @param ip: The ip of the entry (just valid if mode is "add")
 
     """
-    return self._SingleNodeCall(node, "node_add",
-                                [dsa, dsapub, rsa, rsapub, ssh, sshpub])
+    return self._SingleNodeCall(node, "etc_hosts_modify", [mode, name, ip])
 
   @_RpcTimeout(_TMO_NORMAL)
   def call_node_verify(self, node_list, checkdict, cluster_name):
@@ -909,6 +962,16 @@ class RpcRunner(object):
     return self._SingleNodeCall(node, "blockdev_create",
                                 [bdev.ToDict(), size, owner, on_primary, info])
 
+  @_RpcTimeout(_TMO_SLOW)
+  def call_blockdev_wipe(self, node, bdev, offset, size):
+    """Request wipe at given offset with given size of a block device.
+
+    This is a single-node call.
+
+    """
+    return self._SingleNodeCall(node, "blockdev_wipe",
+                                [bdev.ToDict(), offset, size])
+
   @_RpcTimeout(_TMO_NORMAL)
   def call_blockdev_remove(self, node, bdev):
     """Request removal of a given block device.
@@ -984,6 +1047,26 @@ class RpcRunner(object):
     return result
 
   @_RpcTimeout(_TMO_NORMAL)
+  def call_blockdev_getmirrorstatus_multi(self, node_list, node_disks):
+    """Request status of (mirroring) devices from multiple nodes.
+
+    This is a multi-node call.
+
+    """
+    result = self._MultiNodeCall(node_list, "blockdev_getmirrorstatus_multi",
+                                 [dict((name, [dsk.ToDict() for dsk in disks])
+                                       for name, disks in node_disks.items())])
+    for nres in result.values():
+      if nres.fail_msg:
+        continue
+
+      for idx, (success, status) in enumerate(nres.payload):
+        if success:
+          nres.payload[idx] = (success, objects.BlockDevStatus.FromDict(status))
+
+    return result
+
+  @_RpcTimeout(_TMO_NORMAL)
   def call_blockdev_find(self, node, disk):
     """Request identification of a given block device.
 
diff --git a/lib/runtime.py b/lib/runtime.py
new file mode 100644 (file)
index 0000000..2d65ef0
--- /dev/null
@@ -0,0 +1,120 @@
+#
+
+# Copyright (C) 2010 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 implementing configuration details at runtime.
+
+"""
+
+
+import grp
+import pwd
+import threading
+
+from ganeti import constants
+from ganeti import errors
+
+
+_priv = None
+_priv_lock = threading.Lock()
+
+
+def GetUid(user, _getpwnam):
+  """Retrieve the uid from the database.
+
+  @type user: string
+  @param user: The username to retrieve
+  @return: The resolved uid
+
+  """
+  try:
+    return _getpwnam(user).pw_uid
+  except KeyError, err:
+    raise errors.ConfigurationError("User '%s' not found (%s)" % (user, err))
+
+
+def GetGid(group, _getgrnam):
+  """Retrieve the gid from the database.
+
+  @type group: string
+  @param group: The group name to retrieve
+  @return: The resolved gid
+
+  """
+  try:
+    return _getgrnam(group).gr_gid
+  except KeyError, err:
+    raise errors.ConfigurationError("Group '%s' not found (%s)" % (group, err))
+
+
+class GetentResolver:
+  """Resolves Ganeti uids and gids by name.
+
+  @ivar masterd_uid: The resolved uid of the masterd user
+  @ivar masterd_gid: The resolved gid of the masterd group
+  @ivar confd_uid: The resolved uid of the confd user
+  @ivar confd_gid: The resolved gid of the confd group
+  @ivar rapi_uid: The resolved uid of the rapi user
+  @ivar rapi_gid: The resolved gid of the rapi group
+  @ivar noded_uid: The resolved uid of the noded user
+
+  @ivar daemons_gid: The resolved gid of the daemons group
+  @ivar admin_gid: The resolved gid of the admin group
+  """
+  def __init__(self, _getpwnam=pwd.getpwnam, _getgrnam=grp.getgrnam):
+    """Initialize the resolver.
+
+    """
+    # Daemon pairs
+    self.masterd_uid = GetUid(constants.MASTERD_USER, _getpwnam)
+    self.masterd_gid = GetGid(constants.MASTERD_GROUP, _getgrnam)
+
+    self.confd_uid = GetUid(constants.CONFD_USER, _getpwnam)
+    self.confd_gid = GetGid(constants.CONFD_GROUP, _getgrnam)
+
+    self.rapi_uid = GetUid(constants.RAPI_USER, _getpwnam)
+    self.rapi_gid = GetGid(constants.RAPI_GROUP, _getgrnam)
+
+    self.noded_uid = GetUid(constants.NODED_USER, _getpwnam)
+
+    # Misc Ganeti groups
+    self.daemons_gid = GetGid(constants.DAEMONS_GROUP, _getgrnam)
+    self.admin_gid = GetGid(constants.ADMIN_GROUP, _getgrnam)
+
+
+def GetEnts(resolver=GetentResolver):
+  """Singleton wrapper around resolver instance.
+
+  As this method is accessed by multiple threads at the same time
+  we need to take thread-safty carefully
+
+  """
+  # We need to use the global keyword here
+  global _priv # pylint: disable-msg=W0603
+
+  if not _priv:
+    _priv_lock.acquire()
+    try:
+      if not _priv:
+        # W0621: Redefine '_priv' from outer scope (used for singleton)
+        _priv = resolver() # pylint: disable-msg=W0621
+    finally:
+      _priv_lock.release()
+
+  return _priv
+
diff --git a/lib/server/__init__.py b/lib/server/__init__.py
new file mode 100644 (file)
index 0000000..aee6aa0
--- /dev/null
@@ -0,0 +1,24 @@
+#
+#
+
+# Copyright (C) 2010 Google Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+
+
+"""Empty file for package definition.
+
+"""
old mode 100755 (executable)
new mode 100644 (file)
similarity index 95%
rename from daemons/ganeti-confd
rename to lib/server/confd.py
index 804c87a..b24a495
@@ -1,7 +1,7 @@
-#!/usr/bin/python
+#
 #
 
-# Copyright (C) 2009, Google Inc.
+# Copyright (C) 2009, 2010 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
@@ -66,8 +66,8 @@ class ConfdAsyncUDPServer(daemon.AsyncUDPSocket):
     @param processor: ConfdProcessor to use to handle queries
 
     """
-    daemon.AsyncUDPSocket.__init__(self,
-                                   netutils.GetAddressFamily(bind_address))
+    family = netutils.IPAddress.GetAddressFamily(bind_address)
+    daemon.AsyncUDPSocket.__init__(self, family)
     self.bind_address = bind_address
     self.port = port
     self.processor = processor
@@ -258,12 +258,13 @@ def CheckConfd(_, args):
   # conflict with that. If so, we might warn or EXIT_FAILURE.
 
 
-def ExecConfd(options, _):
-  """Main confd function, executed with PID file held
+def PrepConfd(options, _):
+  """Prep confd function, executed with PID file held
 
   """
   # TODO: clarify how the server and reloader variables work (they are
   # not used)
+
   # pylint: disable-msg=W0612
   mainloop = daemon.Mainloop()
 
@@ -281,10 +282,18 @@ def ExecConfd(options, _):
   # Configuration reloader
   reloader = ConfdConfigurationReloader(processor, mainloop)
 
+  return mainloop
+
+
+def ExecConfd(options, args, prep_data): # pylint: disable-msg=W0613
+  """Main confd function, executed with PID file held
+
+  """
+  mainloop = prep_data
   mainloop.Run()
 
 
-def main():
+def Main():
   """Main function for the confd daemon.
 
   """
@@ -293,10 +302,4 @@ def main():
                         version="%%prog (ganeti) %s" %
                         constants.RELEASE_VERSION)
 
-  dirs = [(val, constants.RUN_DIRS_MODE) for val in constants.SUB_RUN_DIRS]
-  dirs.append((constants.LOCK_DIR, 1777))
-  daemon.GenericMain(constants.CONFD, parser, dirs, CheckConfd, ExecConfd)
-
-
-if __name__ == "__main__":
-  main()
+  daemon.GenericMain(constants.CONFD, parser, CheckConfd, PrepConfd, ExecConfd)
old mode 100755 (executable)
new mode 100644 (file)
similarity index 89%
rename from daemons/ganeti-masterd
rename to lib/server/masterd.py
index d47aeb7..bb6d620
@@ -1,7 +1,7 @@
-#!/usr/bin/python
+#
 #
 
-# Copyright (C) 2006, 2007 Google Inc.
+# Copyright (C) 2006, 2007, 2010 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
@@ -72,7 +72,7 @@ class ClientRequestWorker(workerpool.BaseWorker):
     client_ops = ClientOps(server)
 
     try:
-      (method, args) = luxi.ParseRequest(message)
+      (method, args, version) = luxi.ParseRequest(message)
     except luxi.ProtocolError, err:
       logging.error("Protocol Error: %s", err)
       client.close_log()
@@ -80,6 +80,11 @@ 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:
+        raise errors.LuxiError("LUXI version mismatch, server %s, request %s" %
+                               (constants.LUXI_VERSION, version))
+
       result = client_ops.handle_request(method, args)
       success = True
     except errors.GenericError, err:
@@ -314,6 +319,9 @@ class ClientOps:
     """
     # Queries don't have a job id
     proc = mcpu.Processor(self.server.context, None)
+
+    # TODO: Executing an opcode using locks will acquire them in blocking mode.
+    # Consider using a timeout for retries.
     return proc.ExecOpCode(op, None)
 
 
@@ -428,7 +436,7 @@ def CheckAgreement():
   other node to be up too to confirm our status.
 
   """
-  myself = netutils.HostInfo().name
+  myself = netutils.Hostname.GetSysName()
   #temp instantiation of a config writer, used only to get the node list
   cfg = config.ConfigWriter()
   node_list = cfg.GetNodeList()
@@ -496,6 +504,28 @@ def CheckMasterd(options, args):
                           (constants.MASTERD_USER, constants.DAEMONS_GROUP))
     sys.exit(constants.EXIT_FAILURE)
 
+  # Check the configuration is sane before anything else
+  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])
+    print >> sys.stderr,  \
+        ("Configuration version mismatch. The current Ganeti software"
+         " expects version %s, but the on-disk configuration file has"
+         " version %s. This is likely the result of upgrading the"
+         " software without running the upgrade procedure. Please contact"
+         " your cluster administrator or complete the upgrade using the"
+         " cfgupgrade utility, after reading the upgrade notes." %
+         (v1, v2))
+    sys.exit(constants.EXIT_FAILURE)
+  except errors.ConfigurationError, err:
+    print >> sys.stderr, \
+        ("Configuration error while opening the configuration file: %s\n"
+         "This might be caused by an incomplete software upgrade or"
+         " by a corrupted configuration file. Until the problem is fixed"
+         " the master daemon cannot start." % str(err))
+    sys.exit(constants.EXIT_FAILURE)
 
   # If CheckMaster didn't fail we believe we are the master, but we have to
   # confirm with the other nodes.
@@ -527,8 +557,8 @@ def CheckMasterd(options, args):
   utils.RunInSeparateProcess(ActivateMasterIP)
 
 
-def ExecMasterd(options, args): # pylint: disable-msg=W0613
-  """Main master daemon function, executed with the PID file held.
+def PrepMasterd(options, _):
+  """Prep master daemon function, executed with the PID file held.
 
   """
   # This is safe to do as the pid file guarantees against
@@ -538,6 +568,14 @@ def ExecMasterd(options, args): # pylint: disable-msg=W0613
   mainloop = daemon.Mainloop()
   master = MasterServer(mainloop, constants.MASTER_SOCKET,
                         options.uid, options.gid)
+  return (mainloop, master)
+
+
+def ExecMasterd(options, args, prep_data): # pylint: disable-msg=W0613
+  """Main master daemon function, executed with the PID file held.
+
+  """
+  (mainloop, master) = prep_data
   try:
     rpc.Init()
     try:
@@ -552,7 +590,7 @@ def ExecMasterd(options, args): # pylint: disable-msg=W0613
     utils.RemoveFile(constants.MASTER_SOCKET)
 
 
-def main():
+def Main():
   """Main function"""
   parser = OptionParser(description="Ganeti master daemon",
                         usage="%prog [-f] [-d]",
@@ -565,13 +603,5 @@ def main():
   parser.add_option("--yes-do-it", dest="yes_do_it",
                     help="Override interactive check for --no-voting",
                     default=False, action="store_true")
-  dirs = [(constants.RUN_GANETI_DIR, constants.RUN_DIRS_MODE),
-          (constants.SOCKET_DIR, constants.SOCKET_DIR_MODE),
-         ]
-  daemon.GenericMain(constants.MASTERD, parser, dirs,
-                     CheckMasterd, ExecMasterd,
-                     multithreaded=True)
-
-
-if __name__ == "__main__":
-  main()
+  daemon.GenericMain(constants.MASTERD, parser, CheckMasterd, PrepMasterd,
+                     ExecMasterd, multithreaded=True)
old mode 100755 (executable)
new mode 100644 (file)
similarity index 95%
rename from daemons/ganeti-noded
rename to lib/server/noded.py
index 6774ea4..7a76882
@@ -1,7 +1,7 @@
-#!/usr/bin/python
+#
 #
 
-# Copyright (C) 2006, 2007 Google Inc.
+# Copyright (C) 2006, 2007, 2010 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
@@ -184,6 +184,15 @@ class NodeHttpServer(http.server.HttpServer):
     return backend.BlockdevCreate(bdev, size, owner, on_primary, info)
 
   @staticmethod
+  def perspective_blockdev_wipe(params):
+    """Wipe a block device.
+
+    """
+    bdev_s, offset, size = params
+    bdev = objects.Disk.FromDict(bdev_s)
+    return backend.BlockdevWipe(bdev, offset, size)
+
+  @staticmethod
   def perspective_blockdev_remove(params):
     """Remove a block device.
 
@@ -263,6 +272,28 @@ class NodeHttpServer(http.server.HttpServer):
             for status in backend.BlockdevGetmirrorstatus(disks)]
 
   @staticmethod
+  def perspective_blockdev_getmirrorstatus_multi(params):
+    """Return the mirror status for a list of disks.
+
+    """
+    (node_disks, ) = params
+
+    node_name = netutils.Hostname.GetSysName()
+
+    disks = [objects.Disk.FromDict(dsk_s)
+             for dsk_s in node_disks.get(node_name, [])]
+
+    result = []
+
+    for (success, status) in backend.BlockdevGetmirrorstatusMulti(disks):
+      if success:
+        result.append((success, status.ToDict()))
+      else:
+        result.append((success, status))
+
+    return result
+
+  @staticmethod
   def perspective_blockdev_find(params):
     """Expose the FindBlockDevice functionality for a disk.
 
@@ -598,7 +629,7 @@ class NodeHttpServer(http.server.HttpServer):
     """Checks if a node has the given ip address.
 
     """
-    return netutils.OwnIpAddress(params[0])
+    return netutils.IPAddress.Own(params[0])
 
   @staticmethod
   def perspective_node_info(params):
@@ -609,12 +640,13 @@ class NodeHttpServer(http.server.HttpServer):
     return backend.GetNodeInfo(vgname, hypervisor_type)
 
   @staticmethod
-  def perspective_node_add(params):
-    """Complete the registration of this node in the cluster.
+  def perspective_etc_hosts_modify(params):
+    """Modify a node entry in /etc/hosts.
 
     """
-    return backend.AddNode(params[0], params[1], params[2],
-                           params[3], params[4], params[5])
+    backend.EtcHostsModify(params[0], params[1], params[2])
+
+    return True
 
   @staticmethod
   def perspective_node_verify(params):
@@ -913,8 +945,8 @@ def CheckNoded(_, args):
     sys.exit(constants.EXIT_FAILURE)
 
 
-def ExecNoded(options, _):
-  """Main node daemon function, executed with the PID file held.
+def PrepNoded(options, _):
+  """Preparation node daemon function, executed with the PID file held.
 
   """
   if options.mlock:
@@ -946,13 +978,21 @@ def ExecNoded(options, _):
                           ssl_params=ssl_params, ssl_verify_peer=True,
                           request_executor_class=request_executor_class)
   server.Start()
+  return (mainloop, server)
+
+
+def ExecNoded(options, args, prep_data): # pylint: disable-msg=W0613
+  """Main node daemon function, executed with the PID file held.
+
+  """
+  (mainloop, server) = prep_data
   try:
     mainloop.Run()
   finally:
     server.Stop()
 
 
-def main():
+def Main():
   """Main function for the node daemon.
 
   """
@@ -964,16 +1004,7 @@ def main():
                     help="Do not mlock the node memory in ram",
                     default=True, action="store_false")
 
-  dirs = [(val, constants.RUN_DIRS_MODE) for val in constants.SUB_RUN_DIRS]
-  dirs.append((constants.LOG_OS_DIR, 0750))
-  dirs.append((constants.LOCK_DIR, 1777))
-  dirs.append((constants.CRYPTO_KEYS_DIR, constants.CRYPTO_KEYS_DIR_MODE))
-  dirs.append((constants.IMPORT_EXPORT_DIR, constants.IMPORT_EXPORT_DIR_MODE))
-  daemon.GenericMain(constants.NODED, parser, dirs, CheckNoded, ExecNoded,
+  daemon.GenericMain(constants.NODED, parser, CheckNoded, PrepNoded, ExecNoded,
                      default_ssl_cert=constants.NODED_CERT_FILE,
                      default_ssl_key=constants.NODED_CERT_FILE,
                      console_logging=True)
-
-
-if __name__ == '__main__':
-  main()
old mode 100755 (executable)
new mode 100644 (file)
similarity index 69%
rename from daemons/ganeti-rapi
rename to lib/server/rapi.py
index 28cdcd4..b3a71aa
@@ -1,7 +1,7 @@
-#!/usr/bin/python
+#
 #
 
-# Copyright (C) 2006, 2007 Google Inc.
+# Copyright (C) 2006, 2007, 2008, 2009, 2010 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
@@ -31,13 +31,22 @@ import optparse
 import sys
 import os
 import os.path
+import errno
+
+try:
+  from pyinotify import pyinotify # pylint: disable-msg=E0611
+except ImportError:
+  import pyinotify
 
+from ganeti import asyncnotifier
 from ganeti import constants
 from ganeti import http
 from ganeti import daemon
 from ganeti import ssconf
 from ganeti import luxi
 from ganeti import serializer
+from ganeti import compat
+from ganeti import utils
 from ganeti.rapi import connector
 
 import ganeti.http.auth   # pylint: disable-msg=W0611
@@ -82,16 +91,41 @@ class RemoteApiHttpServer(http.auth.HttpServerRequestAuthentication,
 
   def __init__(self, *args, **kwargs):
     # pylint: disable-msg=W0233
-  # it seems pylint doesn't see the second parent class there
+    # it seems pylint doesn't see the second parent class there
     http.server.HttpServer.__init__(self, *args, **kwargs)
     http.auth.HttpServerRequestAuthentication.__init__(self)
     self._resmap = connector.Mapper()
+    self._users = None
 
-    # Load password file
-    if os.path.isfile(constants.RAPI_USERS_FILE):
-      self._users = http.auth.ReadPasswordFile(constants.RAPI_USERS_FILE)
-    else:
-      self._users = None
+  def LoadUsers(self, filename):
+    """Loads a file containing users and passwords.
+
+    @type filename: string
+    @param filename: Path to file
+
+    """
+    logging.info("Reading users file at %s", filename)
+    try:
+      try:
+        contents = utils.ReadFile(filename)
+      except EnvironmentError, err:
+        self._users = None
+        if err.errno == errno.ENOENT:
+          logging.warning("No users file at %s", filename)
+        else:
+          logging.warning("Error while reading %s: %s", filename, err)
+        return False
+
+      users = http.auth.ParsePasswordFile(contents)
+
+    except Exception, err: # pylint: disable-msg=W0703
+      # We don't care about the type of exception
+      logging.error("Error while parsing %s: %s", filename, err)
+      return False
+
+    self._users = users
+
+    return True
 
   def _GetRequestContext(self, req):
     """Returns the context for a request.
@@ -203,6 +237,54 @@ class RemoteApiHttpServer(http.auth.HttpServerRequestAuthentication,
     return serializer.DumpJson(result)
 
 
+class FileEventHandler(asyncnotifier.FileEventHandlerBase):
+  def __init__(self, wm, path, cb):
+    """Initializes this class.
+
+    @param wm: Inotify watch manager
+    @type path: string
+    @param path: File path
+    @type cb: callable
+    @param cb: Function called on file change
+
+    """
+    asyncnotifier.FileEventHandlerBase.__init__(self, wm)
+
+    self._cb = cb
+    self._filename = os.path.basename(path)
+
+    # Different Pyinotify versions have the flag constants at different places,
+    # hence not accessing them directly
+    mask = (pyinotify.EventsCodes.ALL_FLAGS["IN_CLOSE_WRITE"] |
+            pyinotify.EventsCodes.ALL_FLAGS["IN_DELETE"] |
+            pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_FROM"] |
+            pyinotify.EventsCodes.ALL_FLAGS["IN_MOVED_TO"])
+
+    self._handle = self.AddWatch(os.path.dirname(path), mask)
+
+  def process_default(self, event):
+    """Called upon inotify event.
+
+    """
+    if event.name == self._filename:
+      logging.debug("Received inotify event %s", event)
+      self._cb()
+
+
+def SetupFileWatcher(filename, cb):
+  """Configures an inotify watcher for a file.
+
+  @type filename: string
+  @param filename: File to watch
+  @type cb: callable
+  @param cb: Function called on file change
+
+  """
+  wm = pyinotify.WatchManager()
+  handler = FileEventHandler(wm, filename, cb)
+  asyncnotifier.AsyncNotifier(wm, default_proc_fun=handler)
+
+
 def CheckRapi(options, args):
   """Initial checks whether to run or exit with a failure.
 
@@ -222,8 +304,8 @@ def CheckRapi(options, args):
     options.ssl_params = None
 
 
-def ExecRapi(options, _):
-  """Main remote API function, executed with the PID file held.
+def PrepRapi(options, _):
+  """Prep remote API function, executed with the PID file held.
 
   """
 
@@ -232,16 +314,32 @@ def ExecRapi(options, _):
                                ssl_params=options.ssl_params,
                                ssl_verify_peer=False,
                                request_executor_class=JsonErrorRequestExecutor)
+
+  # Setup file watcher (it'll be driven by asyncore)
+  SetupFileWatcher(constants.RAPI_USERS_FILE,
+                   compat.partial(server.LoadUsers, constants.RAPI_USERS_FILE))
+
+  server.LoadUsers(constants.RAPI_USERS_FILE)
+
   # pylint: disable-msg=E1101
   # it seems pylint doesn't see the second parent class there
   server.Start()
+
+  return (mainloop, server)
+
+
+def ExecRapi(options, args, prep_data): # pylint: disable-msg=W0613
+  """Main remote API function, executed with the PID file held.
+
+  """
+  (mainloop, server) = prep_data
   try:
     mainloop.Run()
   finally:
     server.Stop()
 
 
-def main():
+def Main():
   """Main function.
 
   """
@@ -249,13 +347,6 @@ def main():
                     usage="%prog [-f] [-d] [-p port] [-b ADDRESS]",
                     version="%%prog (ganeti) %s" % constants.RELEASE_VERSION)
 
-  dirs = [(val, constants.RUN_DIRS_MODE) for val in constants.SUB_RUN_DIRS]
-  dirs.append((constants.LOG_OS_DIR, 0750))
-  daemon.GenericMain(constants.RAPI, parser, dirs, CheckRapi, ExecRapi,
+  daemon.GenericMain(constants.RAPI, parser, CheckRapi, PrepRapi, ExecRapi,
                      default_ssl_cert=constants.RAPI_CERT_FILE,
-                     default_ssl_key=constants.RAPI_CERT_FILE,
-                     user=constants.RAPI_USER, group=constants.DAEMONS_GROUP)
-
-
-if __name__ == "__main__":
-  main()
+                     default_ssl_key=constants.RAPI_CERT_FILE)
index c29d63c..2eccc59 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007, 2008 Google Inc.
+# Copyright (C) 2006, 2007, 2008, 2010 Google Inc.
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -29,6 +29,7 @@ configuration data, which is mostly static and available to all nodes.
 import sys
 import re
 import os
+import errno
 
 from ganeti import errors
 from ganeti import constants
@@ -281,11 +282,13 @@ class SimpleStore(object):
     constants.SS_NODE_SECONDARY_IPS,
     constants.SS_OFFLINE_NODES,
     constants.SS_ONLINE_NODES,
+    constants.SS_PRIMARY_IP_FAMILY,
     constants.SS_INSTANCE_LIST,
     constants.SS_RELEASE_VERSION,
     constants.SS_HYPERVISOR_LIST,
     constants.SS_MAINTAIN_NODE_HEALTH,
     constants.SS_UID_POOL,
+    constants.SS_NODEGROUPS,
     )
   _MAX_SIZE = 131072
 
@@ -306,7 +309,7 @@ class SimpleStore(object):
     filename = self._cfg_dir + '/' + self._SS_FILEPREFIX + key
     return filename
 
-  def _ReadFile(self, key):
+  def _ReadFile(self, key, default=None):
     """Generic routine to read keys.
 
     This will read the file which holds the value requested. Errors
@@ -317,6 +320,8 @@ class SimpleStore(object):
     try:
       data = utils.ReadFile(filename, size=self._MAX_SIZE)
     except EnvironmentError, err:
+      if err.errno == errno.ENOENT and default is not None:
+        return default
       raise errors.ConfigurationError("Can't read from the ssconf file:"
                                       " '%s'" % str(err))
     data = data.rstrip('\n')
@@ -422,6 +427,14 @@ class SimpleStore(object):
     nl = data.splitlines(False)
     return nl
 
+  def GetNodegroupList(self):
+    """Return the list of nodegroups.
+
+    """
+    data = self._ReadFile(constants.SS_NODEGROUPS)
+    nl = data.splitlines(False)
+    return nl
+
   def GetClusterTags(self):
     """Return the cluster tags.
 
@@ -460,6 +473,17 @@ class SimpleStore(object):
     data = self._ReadFile(constants.SS_UID_POOL)
     return data
 
+  def GetPrimaryIPFamily(self):
+    """Return the cluster-wide primary address family.
+
+    """
+    try:
+      return int(self._ReadFile(constants.SS_PRIMARY_IP_FAMILY,
+                                default=netutils.IP4Address.family))
+    except (ValueError, TypeError), err:
+      raise errors.ConfigurationError("Error while trying to parse primary ip"
+                                      " family: %s" % err)
+
 
 def GetMasterAndMyself(ss=None):
   """Get the master node and my own hostname.
@@ -478,7 +502,7 @@ def GetMasterAndMyself(ss=None):
   """
   if ss is None:
     ss = SimpleStore()
-  return ss.GetMasterNode(), netutils.HostInfo().name
+  return ss.GetMasterNode(), netutils.Hostname.GetSysName()
 
 
 def CheckMaster(debug, ss=None):
index 8daf579..84ae692 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007 Google Inc.
+# Copyright (C) 2006, 2007, 2010 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
@@ -31,6 +31,7 @@ import re
 from ganeti import utils
 from ganeti import errors
 from ganeti import constants
+from ganeti import netutils
 
 
 def FormatParamikoFingerprint(fingerprint):
@@ -79,8 +80,17 @@ class SshRunner:
   """Wrapper for SSH commands.
 
   """
-  def __init__(self, cluster_name):
+  def __init__(self, cluster_name, ipv6=False):
+    """Initializes this class.
+
+    @type cluster_name: str
+    @param cluster_name: name of the cluster
+    @type ipv6: bool
+    @param ipv6: If true, force ssh to use IPv6 addresses only
+
+    """
     self.cluster_name = cluster_name
+    self.ipv6 = ipv6
 
   def _BuildSshOptions(self, batch, ask_key, use_cluster_key,
                        strict_host_check, private_key=None, quiet=True):
@@ -141,6 +151,9 @@ class SshRunner:
       else:
         options.append("-oStrictHostKeyChecking=no")
 
+    if self.ipv6:
+      options.append("-6")
+
     return options
 
   def BuildCmd(self, hostname, user, command, batch=True, ask_key=False,
@@ -208,6 +221,9 @@ class SshRunner:
     command = [constants.SCP, "-p"]
     command.extend(self._BuildSshOptions(True, False, True, True))
     command.append(filename)
+    if netutils.IP6Address.IsValid(node):
+      node = netutils.FormatAddress((node, None))
+
     command.append("%s:%s" % (node, filename))
 
     result = utils.RunCmd(command)
index deea868..bdd8610 100644 (file)
@@ -61,7 +61,6 @@ except ImportError:
 from ganeti import errors
 from ganeti import constants
 from ganeti import compat
-from ganeti import netutils
 
 
 _locksheld = []
@@ -83,6 +82,9 @@ X509_SIGNATURE = re.compile(r"^%s:\s*(?P<salt>%s+)/(?P<sign>%s+)$" %
 
 _VALID_SERVICE_NAME_RE = re.compile("^[-_.a-zA-Z0-9]{1,128}$")
 
+UUID_RE = re.compile('^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-'
+                     '[a-f0-9]{4}-[a-f0-9]{12}$')
+
 # Certificate verification results
 (CERT_WARNING,
  CERT_ERROR) = range(1, 3)
@@ -162,7 +164,8 @@ def _BuildCmdEnvironment(env, reset):
   return cmd_env
 
 
-def RunCmd(cmd, env=None, output=None, cwd="/", reset_env=False):
+def RunCmd(cmd, env=None, output=None, cwd="/", reset_env=False,
+           interactive=False):
   """Execute a (shell) command.
 
   The command should not read from its standard input, as it will be
@@ -181,6 +184,9 @@ def RunCmd(cmd, env=None, output=None, cwd="/", reset_env=False):
       directory for the command; the default will be /
   @type reset_env: boolean
   @param reset_env: whether to reset or keep the default os environment
+  @type interactive: boolean
+  @param interactive: weather we pipe stdin, stdout and stderr
+                      (default behaviour) or run the command interactive
   @rtype: L{RunResult}
   @return: RunResult instance
   @raise errors.ProgrammerError: if we call this when forks are disabled
@@ -189,6 +195,10 @@ def RunCmd(cmd, env=None, output=None, cwd="/", reset_env=False):
   if no_fork:
     raise errors.ProgrammerError("utils.RunCmd() called with fork() disabled")
 
+  if output and interactive:
+    raise errors.ProgrammerError("Parameters 'output' and 'interactive' can"
+                                 " not be provided at the same time")
+
   if isinstance(cmd, basestring):
     strcmd = cmd
     shell = True
@@ -206,7 +216,7 @@ def RunCmd(cmd, env=None, output=None, cwd="/", reset_env=False):
 
   try:
     if output is None:
-      out, err, status = _RunCmdPipe(cmd, cmd_env, shell, cwd)
+      out, err, status = _RunCmdPipe(cmd, cmd_env, shell, cwd, interactive)
     else:
       status = _RunCmdFile(cmd, cmd_env, shell, output, cwd)
       out = err = ""
@@ -227,6 +237,53 @@ def RunCmd(cmd, env=None, output=None, cwd="/", reset_env=False):
   return RunResult(exitcode, signal_, out, err, strcmd)
 
 
+def SetupDaemonEnv(cwd="/", umask=077):
+  """Setup a daemon's environment.
+
+  This should be called between the first and second fork, due to
+  setsid usage.
+
+  @param cwd: the directory to which to chdir
+  @param umask: the umask to setup
+
+  """
+  os.chdir(cwd)
+  os.umask(umask)
+  os.setsid()
+
+
+def SetupDaemonFDs(output_file, output_fd):
+  """Setups up a daemon's file descriptors.
+
+  @param output_file: if not None, the file to which to redirect
+      stdout/stderr
+  @param output_fd: if not None, the file descriptor for stdout/stderr
+
+  """
+  # check that at most one is defined
+  assert [output_file, output_fd].count(None) >= 1
+
+  # Open /dev/null (read-only, only for stdin)
+  devnull_fd = os.open(os.devnull, os.O_RDONLY)
+
+  if output_fd is not None:
+    pass
+  elif output_file is not None:
+    # Open output file
+    try:
+      output_fd = os.open(output_file,
+                          os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0600)
+    except EnvironmentError, err:
+      raise Exception("Opening output file failed: %s" % err)
+  else:
+    output_fd = os.open(os.devnull, os.O_WRONLY)
+
+  # Redirect standard I/O
+  os.dup2(devnull_fd, 0)
+  os.dup2(output_fd, 1)
+  os.dup2(output_fd, 2)
+
+
 def StartDaemon(cmd, env=None, cwd="/", output=None, output_fd=None,
                 pidfile=None):
   """Start a daemon process after forking twice.
@@ -291,8 +348,8 @@ def StartDaemon(cmd, env=None, cwd="/", output=None, output_fd=None,
         finally:
           _CloseFDNoErr(errpipe_write)
 
-        # Wait for daemon to be started (or an error message to arrive) and read
-        # up to 100 KB as an error message
+        # Wait for daemon to be started (or an error message to
+        # arrive) and read up to 100 KB as an error message
         errormsg = RetryOnSignal(os.read, errpipe_read, 100 * 1024)
       finally:
         _CloseFDNoErr(errpipe_read)
@@ -334,9 +391,7 @@ def _StartDaemonChild(errpipe_read, errpipe_write,
     _CloseFDNoErr(pidpipe_read)
 
     # First child process
-    os.chdir("/")
-    os.umask(077)
-    os.setsid()
+    SetupDaemonEnv()
 
     # And fork for the second time
     pid = os.fork()
@@ -344,7 +399,8 @@ def _StartDaemonChild(errpipe_read, errpipe_write,
       # Exit first child process
       os._exit(0) # pylint: disable-msg=W0212
 
-    # Make sure pipe is closed on execv* (and thereby notifies original process)
+    # Make sure pipe is closed on execv* (and thereby notifies
+    # original process)
     SetCloseOnExecFlag(errpipe_write, True)
 
     # List of file descriptors to be left open
@@ -352,20 +408,7 @@ def _StartDaemonChild(errpipe_read, errpipe_write,
 
     # Open PID file
     if pidfile:
-      try:
-        # TODO: Atomic replace with another locked file instead of writing into
-        # it after creating
-        fd_pidfile = os.open(pidfile, os.O_WRONLY | os.O_CREAT, 0600)
-
-        # Lock the PID file (and fail if not possible to do so). Any code
-        # wanting to send a signal to the daemon should try to lock the PID
-        # file before reading it. If acquiring the lock succeeds, the daemon is
-        # no longer running and the signal should not be sent.
-        LockFile(fd_pidfile)
-
-        os.write(fd_pidfile, "%d\n" % os.getpid())
-      except Exception, err:
-        raise Exception("Creating and locking PID file failed: %s" % err)
+      fd_pidfile = WritePidFile(pidfile)
 
       # Keeping the file open to hold the lock
       noclose_fds.append(fd_pidfile)
@@ -374,27 +417,7 @@ def _StartDaemonChild(errpipe_read, errpipe_write,
     else:
       fd_pidfile = None
 
-    # Open /dev/null
-    fd_devnull = os.open(os.devnull, os.O_RDWR)
-
-    assert not output or (bool(output) ^ (fd_output is not None))
-
-    if fd_output is not None:
-      pass
-    elif output:
-      # Open output file
-      try:
-        # TODO: Implement flag to set append=yes/no
-        fd_output = os.open(output, os.O_WRONLY | os.O_CREAT, 0600)
-      except EnvironmentError, err:
-        raise Exception("Opening output file failed: %s" % err)
-    else:
-      fd_output = fd_devnull
-
-    # Redirect standard I/O
-    os.dup2(fd_devnull, 0)
-    os.dup2(fd_output, 1)
-    os.dup2(fd_output, 2)
+    SetupDaemonFDs(output, fd_output)
 
     # Send daemon PID to parent
     RetryOnSignal(os.write, pidpipe_write, str(os.getpid()))
@@ -412,9 +435,7 @@ def _StartDaemonChild(errpipe_read, errpipe_write,
   except: # pylint: disable-msg=W0702
     try:
       # Report errors to original process
-      buf = str(sys.exc_info()[1])
-
-      RetryOnSignal(os.write, errpipe_write, buf)
+      WriteErrorToFD(errpipe_write, str(sys.exc_info()[1]))
     except: # pylint: disable-msg=W0702
       # Ignore errors in error handling
       pass
@@ -422,7 +443,24 @@ def _StartDaemonChild(errpipe_read, errpipe_write,
   os._exit(1) # pylint: disable-msg=W0212
 
 
-def _RunCmdPipe(cmd, env, via_shell, cwd):
+def WriteErrorToFD(fd, err):
+  """Possibly write an error message to a fd.
+
+  @type fd: None or int (file descriptor)
+  @param fd: if not None, the error will be written to this fd
+  @param err: string, the error message
+
+  """
+  if fd is None:
+    return
+
+  if not err:
+    err = "<unknown error>"
+
+  RetryOnSignal(os.write, fd, err)
+
+
+def _RunCmdPipe(cmd, env, via_shell, cwd, interactive):
   """Run a command and return its output.
 
   @type  cmd: string or list
@@ -433,46 +471,57 @@ def _RunCmdPipe(cmd, env, via_shell, cwd):
   @param via_shell: if we should run via the shell
   @type cwd: string
   @param cwd: the working directory for the program
+  @type interactive: boolean
+  @param interactive: Run command interactive (without piping)
   @rtype: tuple
   @return: (out, err, status)
 
   """
   poller = select.poll()
+
+  stderr = subprocess.PIPE
+  stdout = subprocess.PIPE
+  stdin = subprocess.PIPE
+
+  if interactive:
+    stderr = stdout = stdin = None
+
   child = subprocess.Popen(cmd, shell=via_shell,
-                           stderr=subprocess.PIPE,
-                           stdout=subprocess.PIPE,
-                           stdin=subprocess.PIPE,
+                           stderr=stderr,
+                           stdout=stdout,
+                           stdin=stdin,
                            close_fds=True, env=env,
                            cwd=cwd)
 
-  child.stdin.close()
-  poller.register(child.stdout, select.POLLIN)
-  poller.register(child.stderr, select.POLLIN)
   out = StringIO()
   err = StringIO()
-  fdmap = {
-    child.stdout.fileno(): (out, child.stdout),
-    child.stderr.fileno(): (err, child.stderr),
-    }
-  for fd in fdmap:
-    SetNonblockFlag(fd, True)
-
-  while fdmap:
-    pollresult = RetryOnSignal(poller.poll)
-
-    for fd, event in pollresult:
-      if event & select.POLLIN or event & select.POLLPRI:
-        data = fdmap[fd][1].read()
-        # no data from read signifies EOF (the same as POLLHUP)
-        if not data:
+  if not interactive:
+    child.stdin.close()
+    poller.register(child.stdout, select.POLLIN)
+    poller.register(child.stderr, select.POLLIN)
+    fdmap = {
+      child.stdout.fileno(): (out, child.stdout),
+      child.stderr.fileno(): (err, child.stderr),
+      }
+    for fd in fdmap:
+      SetNonblockFlag(fd, True)
+
+    while fdmap:
+      pollresult = RetryOnSignal(poller.poll)
+
+      for fd, event in pollresult:
+        if event & select.POLLIN or event & select.POLLPRI:
+          data = fdmap[fd][1].read()
+          # no data from read signifies EOF (the same as POLLHUP)
+          if not data:
+            poller.unregister(fd)
+            del fdmap[fd]
+            continue
+          fdmap[fd][0].write(data)
+        if (event & select.POLLNVAL or event & select.POLLHUP or
+            event & select.POLLERR):
           poller.unregister(fd)
           del fdmap[fd]
-          continue
-        fdmap[fd][0].write(data)
-      if (event & select.POLLNVAL or event & select.POLLHUP or
-          event & select.POLLERR):
-        poller.unregister(fd)
-        del fdmap[fd]
 
   out = out.getvalue()
   err = err.getvalue()
@@ -1422,50 +1471,42 @@ def SetEtcHostsEntry(file_name, ip, hostname, aliases):
   @param aliases: the list of aliases to add for the hostname
 
   """
-  # FIXME: use WriteFile + fn rather than duplicating its efforts
   # Ensure aliases are unique
   aliases = UniqueSequence([hostname] + aliases)[1:]
 
-  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
-  try:
-    out = os.fdopen(fd, 'w')
+  def _WriteEtcHosts(fd):
+    # Duplicating file descriptor because os.fdopen's result will automatically
+    # close the descriptor, but we would still like to have its functionality.
+    out = os.fdopen(os.dup(fd), "w")
     try:
-      f = open(file_name, 'r')
-      try:
-        for line in f:
-          fields = line.split()
-          if fields and not fields[0].startswith('#') and ip == fields[0]:
-            continue
-          out.write(line)
-
-        out.write("%s\t%s" % (ip, hostname))
-        if aliases:
-          out.write(" %s" % ' '.join(aliases))
-        out.write('\n')
+      for line in ReadFile(file_name).splitlines(True):
+        fields = line.split()
+        if fields and not fields[0].startswith("#") and ip == fields[0]:
+          continue
+        out.write(line)
 
-        out.flush()
-        os.fsync(out)
-        os.chmod(tmpname, 0644)
-        os.rename(tmpname, file_name)
-      finally:
-        f.close()
+      out.write("%s\t%s" % (ip, hostname))
+      if aliases:
+        out.write(" %s" % " ".join(aliases))
+      out.write("\n")
+      out.flush()
     finally:
       out.close()
-  except:
-    RemoveFile(tmpname)
-    raise
 
+  WriteFile(file_name, fn=_WriteEtcHosts, mode=0644)
 
-def AddHostToEtcHosts(hostname):
+
+def AddHostToEtcHosts(hostname, ip):
   """Wrapper around SetEtcHostsEntry.
 
   @type hostname: str
   @param hostname: a hostname that will be resolved and added to
       L{constants.ETC_HOSTS}
+  @type ip: str
+  @param ip: The ip address of the host
 
   """
-  hi = netutils.HostInfo(name=hostname)
-  SetEtcHostsEntry(constants.ETC_HOSTS, hi.ip, hi.name, [hi.ShortName()])
+  SetEtcHostsEntry(constants.ETC_HOSTS, ip, hostname, [hostname.split(".")[0]])
 
 
 def RemoveEtcHostsEntry(file_name, hostname):
@@ -1479,37 +1520,29 @@ def RemoveEtcHostsEntry(file_name, hostname):
   @param hostname: the hostname to be removed
 
   """
-  # FIXME: use WriteFile + fn rather than duplicating its efforts
-  fd, tmpname = tempfile.mkstemp(dir=os.path.dirname(file_name))
-  try:
-    out = os.fdopen(fd, 'w')
+  def _WriteEtcHosts(fd):
+    # Duplicating file descriptor because os.fdopen's result will automatically
+    # close the descriptor, but we would still like to have its functionality.
+    out = os.fdopen(os.dup(fd), "w")
     try:
-      f = open(file_name, 'r')
-      try:
-        for line in f:
-          fields = line.split()
-          if len(fields) > 1 and not fields[0].startswith('#'):
-            names = fields[1:]
-            if hostname in names:
-              while hostname in names:
-                names.remove(hostname)
-              if names:
-                out.write("%s %s\n" % (fields[0], ' '.join(names)))
-              continue
-
-          out.write(line)
+      for line in ReadFile(file_name).splitlines(True):
+        fields = line.split()
+        if len(fields) > 1 and not fields[0].startswith("#"):
+          names = fields[1:]
+          if hostname in names:
+            while hostname in names:
+              names.remove(hostname)
+            if names:
+              out.write("%s %s\n" % (fields[0], " ".join(names)))
+            continue
 
-        out.flush()
-        os.fsync(out)
-        os.chmod(tmpname, 0644)
-        os.rename(tmpname, file_name)
-      finally:
-        f.close()
+        out.write(line)
+
+      out.flush()
     finally:
       out.close()
-  except:
-    RemoveFile(tmpname)
-    raise
+
+  WriteFile(file_name, fn=_WriteEtcHosts, mode=0644)
 
 
 def RemoveHostFromEtcHosts(hostname):
@@ -1521,9 +1554,8 @@ def RemoveHostFromEtcHosts(hostname):
       L{constants.ETC_HOSTS}
 
   """
-  hi = netutils.HostInfo(name=hostname)
-  RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.name)
-  RemoveEtcHostsEntry(constants.ETC_HOSTS, hi.ShortName())
+  RemoveEtcHostsEntry(constants.ETC_HOSTS, hostname)
+  RemoveEtcHostsEntry(constants.ETC_HOSTS, hostname.split(".")[0])
 
 
 def TimestampForFilename():
@@ -1840,6 +1872,71 @@ def WriteFile(file_name, fn=None, data=None,
   return result
 
 
+def GetFileID(path=None, fd=None):
+  """Returns the file 'id', i.e. the dev/inode and mtime information.
+
+  Either the path to the file or the fd must be given.
+
+  @param path: the file path
+  @param fd: a file descriptor
+  @return: a tuple of (device number, inode number, mtime)
+
+  """
+  if [path, fd].count(None) != 1:
+    raise errors.ProgrammerError("One and only one of fd/path must be given")
+
+  if fd is None:
+    st = os.stat(path)
+  else:
+    st = os.fstat(fd)
+
+  return (st.st_dev, st.st_ino, st.st_mtime)
+
+
+def VerifyFileID(fi_disk, fi_ours):
+  """Verifies that two file IDs are matching.
+
+  Differences in the inode/device are not accepted, but and older
+  timestamp for fi_disk is accepted.
+
+  @param fi_disk: tuple (dev, inode, mtime) representing the actual
+      file data
+  @param fi_ours: tuple (dev, inode, mtime) representing the last
+      written file data
+  @rtype: boolean
+
+  """
+  (d1, i1, m1) = fi_disk
+  (d2, i2, m2) = fi_ours
+
+  return (d1, i1) == (d2, i2) and m1 <= m2
+
+
+def SafeWriteFile(file_name, file_id, **kwargs):
+  """Wraper over L{WriteFile} that locks the target file.
+
+  By keeping the target file locked during WriteFile, we ensure that
+  cooperating writers will safely serialise access to the file.
+
+  @type file_name: str
+  @param file_name: the target filename
+  @type file_id: tuple
+  @param file_id: a result from L{GetFileID}
+
+  """
+  fd = os.open(file_name, os.O_RDONLY | os.O_CREAT)
+  try:
+    LockFile(fd)
+    if file_id is not None:
+      disk_id = GetFileID(fd=fd)
+      if not VerifyFileID(disk_id, file_id):
+        raise errors.LockError("Cannot overwrite file %s, it has been modified"
+                               " since last written" % file_name)
+    return WriteFile(file_name, **kwargs)
+  finally:
+    os.close(fd)
+
+
 def ReadOneLineFile(file_name, strict=False):
   """Return the first non-empty line from a file.
 
@@ -2114,7 +2211,7 @@ def Mlockall(_ctypes=ctypes):
   logging.debug("Memory lock set")
 
 
-def Daemonize(logfile, run_uid, run_gid):
+def Daemonize(logfile):
   """Daemonize the current process.
 
   This detaches the current process from the controlling terminal and
@@ -2122,48 +2219,45 @@ def Daemonize(logfile, run_uid, run_gid):
 
   @type logfile: str
   @param logfile: the logfile to which we should redirect stdout/stderr
-  @type run_uid: int
-  @param run_uid: Run the child under this uid
-  @type run_gid: int
-  @param run_gid: Run the child under this gid
   @rtype: int
   @return: the value zero
 
   """
   # pylint: disable-msg=W0212
   # yes, we really want os._exit
-  UMASK = 077
-  WORKDIR = "/"
+
+  # TODO: do another attempt to merge Daemonize and StartDaemon, or at
+  # least abstract the pipe functionality between them
+
+  # Create pipe for sending error messages
+  (rpipe, wpipe) = os.pipe()
 
   # this might fail
   pid = os.fork()
   if (pid == 0):  # The first child.
-    os.setsid()
-    # FIXME: When removing again and moving to start-stop-daemon privilege drop
-    #        make sure to check for config permission and bail out when invoked
-    #        with wrong user.
-    os.setgid(run_gid)
-    os.setuid(run_uid)
+    SetupDaemonEnv()
+
     # this might fail
     pid = os.fork() # Fork a second child.
     if (pid == 0):  # The second child.
-      os.chdir(WORKDIR)
-      os.umask(UMASK)
+      _CloseFDNoErr(rpipe)
     else:
       # exit() or _exit()?  See below.
       os._exit(0) # Exit parent (the first child) of the second child.
   else:
-    os._exit(0) # Exit parent of the first child.
+    _CloseFDNoErr(wpipe)
+    # Wait for daemon to be started (or an error message to
+    # arrive) and read up to 100 KB as an error message
+    errormsg = RetryOnSignal(os.read, rpipe, 100 * 1024)
+    if errormsg:
+      sys.stderr.write("Error when starting daemon process: %r\n" % errormsg)
+      rcode = 1
+    else:
+      rcode = 0
+    os._exit(rcode) # Exit parent of the first child.
 
-  for fd in range(3):
-    _CloseFDNoErr(fd)
-  i = os.open("/dev/null", os.O_RDONLY) # stdin
-  assert i == 0, "Can't close/reopen stdin"
-  i = os.open(logfile, os.O_WRONLY|os.O_CREAT|os.O_APPEND, 0600) # stdout
-  assert i == 1, "Can't close/reopen stdout"
-  # Duplicate standard output to standard error.
-  os.dup2(1, 2)
-  return 0
+  SetupDaemonFDs(logfile, None)
+  return wpipe
 
 
 def DaemonPidFileName(name):
@@ -2205,23 +2299,31 @@ def StopDaemon(name):
   return True
 
 
-def WritePidFile(name):
+def WritePidFile(pidfile):
   """Write the current process pidfile.
 
-  The file will be written to L{constants.RUN_GANETI_DIR}I{/name.pid}
-
-  @type name: str
-  @param name: the daemon name to use
-  @raise errors.GenericError: if the pid file already exists and
+  @type pidfile: sting
+  @param pidfile: the path to the file to be written
+  @raise errors.LockError: if the pid file already exists and
       points to a live process
+  @rtype: int
+  @return: the file descriptor of the lock file; do not close this unless
+      you want to unlock the pid file
 
   """
-  pid = os.getpid()
-  pidfilename = DaemonPidFileName(name)
-  if IsProcessAlive(ReadPidFile(pidfilename)):
-    raise errors.GenericError("%s contains a live process" % pidfilename)
+  # We don't rename nor truncate the file to not drop locks under
+  # existing processes
+  fd_pidfile = os.open(pidfile, os.O_WRONLY | os.O_CREAT, 0600)
 
-  WriteFile(pidfilename, data="%d\n" % pid)
+  # Lock the PID file (and fail if not possible to do so). Any code
+  # wanting to send a signal to the daemon should try to lock the PID
+  # file before reading it. If acquiring the lock succeeds, the daemon is
+  # no longer running and the signal should not be sent.
+  LockFile(fd_pidfile)
+
+  os.write(fd_pidfile, "%d\n" % os.getpid())
+
+  return fd_pidfile
 
 
 def RemovePidFile(name):
@@ -2922,6 +3024,33 @@ def CommaJoin(names):
   return ", ".join([str(val) for val in names])
 
 
+def FindMatch(data, name):
+  """Tries to find an item in a dictionary matching a name.
+
+  Callers have to ensure the data names aren't contradictory (e.g. a regexp
+  that matches a string). If the name isn't a direct key, all regular
+  expression objects in the dictionary are matched against it.
+
+  @type data: dict
+  @param data: Dictionary containing data
+  @type name: string
+  @param name: Name to look for
+  @rtype: tuple; (value in dictionary, matched groups as list)
+
+  """
+  if name in data:
+    return (data[name], [])
+
+  for key, value in data.items():
+    # Regex objects
+    if hasattr(key, "match"):
+      m = key.match(name)
+      if m:
+        return (value, list(m.groups()))
+
+  return None
+
+
 def BytesToMebibyte(value):
   """Converts bytes to mebibytes.
 
old mode 100755 (executable)
new mode 100644 (file)
similarity index 97%
rename from daemons/ganeti-watcher
rename to lib/watcher/__init__.py
index 6eb4446..9abdade
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#
 #
 
 # Copyright (C) 2006, 2007, 2008, 2009, 2010 Google Inc.
@@ -215,7 +215,7 @@ class NodeMaintenance(object):
     """Check node status versus cluster desired state.
 
     """
-    my_name = netutils.HostInfo().name
+    my_name = netutils.Hostname.GetSysName()
     req = confd_client.ConfdClientRequest(type=
                                           constants.CONFD_REQ_NODE_ROLE_BYNAME,
                                           query=my_name)
@@ -466,7 +466,7 @@ class Watcher(object):
   def __init__(self, opts, notepad):
     self.notepad = notepad
     master = client.QueryConfigValues(["master_node"])[0]
-    if master != netutils.HostInfo().name:
+    if master != netutils.Hostname.GetSysName():
       raise NotMasterError("This is not the master node")
     # first archive old jobs
     self.ArchiveJobs(opts.job_age)
@@ -671,13 +671,15 @@ def ParseOptions():
   parser.add_option("-A", "--job-age", dest="job_age",
                     help="Autoarchive jobs older than this age (default"
                     " 6 hours)", default=6*3600)
+  parser.add_option("--ignore-pause", dest="ignore_pause", default=False,
+                    action="store_true", help="Ignore cluster pause setting")
   options, args = parser.parse_args()
   options.job_age = cli.ParseTimespec(options.job_age)
   return options, args
 
 
 @rapi.client.UsesRapiClient
-def main():
+def Main():
   """Main function.
 
   """
@@ -687,18 +689,18 @@ def main():
 
   if args: # watcher doesn't take any arguments
     print >> sys.stderr, ("Usage: %s [-f] " % sys.argv[0])
-    sys.exit(constants.EXIT_FAILURE)
+    return constants.EXIT_FAILURE
 
   utils.SetupLogging(constants.LOG_WATCHER, debug=options.debug,
                      stderr_logging=options.debug)
 
-  if ShouldPause():
+  if ShouldPause() and not options.ignore_pause:
     logging.debug("Pause has been set, exiting")
-    sys.exit(constants.EXIT_SUCCESS)
+    return constants.EXIT_SUCCESS
 
   statefile = OpenStateFile(constants.WATCHER_STATEFILE)
   if not statefile:
-    sys.exit(constants.EXIT_FAILURE)
+    return constants.EXIT_FAILURE
 
   update_file = False
   try:
@@ -717,13 +719,13 @@ def main():
         # this is, from cli.GetClient, a not-master case
         logging.debug("Not on master, exiting")
         update_file = True
-        sys.exit(constants.EXIT_SUCCESS)
+        return constants.EXIT_SUCCESS
       except luxi.NoMasterError, err:
         logging.warning("Master seems to be down (%s), trying to restart",
                         str(err))
         if not utils.EnsureDaemon(constants.MASTERD):
           logging.critical("Can't start the master, exiting")
-          sys.exit(constants.EXIT_FAILURE)
+          return constants.EXIT_FAILURE
         # else retry the connection
         client = cli.GetClient()
 
@@ -747,7 +749,7 @@ def main():
       except errors.ConfigurationError:
         # Just exit if there's no configuration
         update_file = True
-        sys.exit(constants.EXIT_SUCCESS)
+        return constants.EXIT_SUCCESS
 
       watcher.Run()
       update_file = True
@@ -761,18 +763,16 @@ def main():
     raise
   except NotMasterError:
     logging.debug("Not master, exiting")
-    sys.exit(constants.EXIT_NOTMASTER)
+    return constants.EXIT_NOTMASTER
   except errors.ResolverError, err:
     logging.error("Cannot resolve hostname '%s', exiting.", err.args[0])
-    sys.exit(constants.EXIT_NODESETUP_ERROR)
+    return constants.EXIT_NODESETUP_ERROR
   except errors.JobQueueFull:
     logging.error("Job queue is full, can't query cluster state")
   except errors.JobQueueDrainError:
     logging.error("Job queue is drained, can't maintain cluster state")
   except Exception, err:
     logging.exception(str(err))
-    sys.exit(constants.EXIT_FAILURE)
-
+    return constants.EXIT_FAILURE
 
-if __name__ == '__main__':
-  main()
+  return constants.EXIT_SUCCESS
index 9f00b91..d276b18 100644 (file)
 
 """
 
-import collections
 import logging
 import threading
+import heapq
 
 from ganeti import compat
+from ganeti import errors
 
 
 _TERMINATE = object()
+_DEFAULT_PRIORITY = 0
+
+
+class DeferTask(Exception):
+  """Special exception class to defer a task.
+
+  This class can be raised by L{BaseWorker.RunTask} to defer the execution of a
+  task. Optionally, the priority of the task can be changed.
+
+  """
+  def __init__(self, priority=None):
+    """Initializes this class.
+
+    @type priority: number
+    @param priority: New task priority (None means no change)
+
+    """
+    Exception.__init__(self)
+    self.priority = priority
 
 
 class BaseWorker(threading.Thread, object):
@@ -67,6 +87,22 @@ class BaseWorker(threading.Thread, object):
     finally:
       self.pool._lock.release()
 
+  def GetCurrentPriority(self):
+    """Returns the priority of the current task.
+
+    Should only be called from within L{RunTask}.
+
+    """
+    self.pool._lock.acquire()
+    try:
+      assert self._HasRunningTaskUnlocked()
+
+      (priority, _, _) = self._current_task
+
+      return priority
+    finally:
+      self.pool._lock.release()
+
   def SetTaskName(self, taskname):
     """Sets the name of the current task.
 
@@ -100,6 +136,8 @@ class BaseWorker(threading.Thread, object):
 
     while True:
       assert self._current_task is None
+
+      defer = None
       try:
         # Wait on lock to be told either to terminate or to do a task
         pool._lock.acquire()
@@ -124,15 +162,28 @@ class BaseWorker(threading.Thread, object):
         finally:
           pool._lock.release()
 
-        # Run the actual task
+        (priority, _, args) = self._current_task
         try:
-          logging.debug("Starting task %r", self._current_task)
+          # Run the actual task
+          assert defer is None
+          logging.debug("Starting task %r, priority %s", args, priority)
           assert self.getName() == self._worker_id
           try:
-            self.RunTask(*self._current_task)
+            self.RunTask(*args) # pylint: disable-msg=W0142
           finally:
             self.SetTaskName(None)
-          logging.debug("Done with task %r", self._current_task)
+          logging.debug("Done with task %r, priority %s", args, priority)
+        except DeferTask, err:
+          defer = err
+
+          if defer.priority is None:
+            # Use same priority
+            defer.priority = priority
+
+          logging.debug("Deferring task %r, new priority %s",
+                        args, defer.priority)
+
+          assert self._HasRunningTaskUnlocked()
         except: # pylint: disable-msg=W0702
           logging.exception("Caught unhandled exception")
 
@@ -141,6 +192,12 @@ class BaseWorker(threading.Thread, object):
         # Notify pool
         pool._lock.acquire()
         try:
+          if defer:
+            assert self._current_task
+            # Schedule again for later run
+            (_, _, args) = self._current_task
+            pool._AddTaskUnlocked(args, defer.priority)
+
           if self._current_task:
             self._current_task = None
             pool._worker_to_pool.notifyAll()
@@ -194,7 +251,8 @@ class WorkerPool(object):
     self._termworkers = []
 
     # Queued tasks
-    self._tasks = collections.deque()
+    self._counter = 0
+    self._tasks = []
 
     # Start workers
     self.Resize(num_workers)
@@ -208,44 +266,77 @@ class WorkerPool(object):
     while self._quiescing:
       self._pool_to_pool.wait()
 
-  def _AddTaskUnlocked(self, args):
+  def _AddTaskUnlocked(self, args, priority):
+    """Adds a task to the internal queue.
+
+    @type args: sequence
+    @param args: Arguments passed to L{BaseWorker.RunTask}
+    @type priority: number
+    @param priority: Task priority
+
+    """
     assert isinstance(args, (tuple, list)), "Arguments must be a sequence"
+    assert isinstance(priority, (int, long)), "Priority must be numeric"
 
-    self._tasks.append(args)
+    # This counter is used to ensure elements are processed in their
+    # incoming order. For processing they're sorted by priority and then
+    # counter.
+    self._counter += 1
+
+    heapq.heappush(self._tasks, (priority, self._counter, args))
 
     # Notify a waiting worker
     self._pool_to_worker.notify()
 
-  def AddTask(self, args):
+  def AddTask(self, args, priority=_DEFAULT_PRIORITY):
     """Adds a task to the queue.
 
     @type args: sequence
     @param args: arguments passed to L{BaseWorker.RunTask}
+    @type priority: number
+    @param priority: Task priority
 
     """
     self._lock.acquire()
     try:
       self._WaitWhileQuiescingUnlocked()
-      self._AddTaskUnlocked(args)
+      self._AddTaskUnlocked(args, priority)
     finally:
       self._lock.release()
 
-  def AddManyTasks(self, tasks):
+  def AddManyTasks(self, tasks, priority=_DEFAULT_PRIORITY):
     """Add a list of tasks to the queue.
 
     @type tasks: list of tuples
     @param tasks: list of args passed to L{BaseWorker.RunTask}
+    @type priority: number or list of numbers
+    @param priority: Priority for all added tasks or a list with the priority
+                     for each task
 
     """
     assert compat.all(isinstance(task, (tuple, list)) for task in tasks), \
       "Each task must be a sequence"
 
+    assert (isinstance(priority, (int, long)) or
+            compat.all(isinstance(prio, (int, long)) for prio in priority)), \
+           "Priority must be numeric or be a list of numeric values"
+
+    if isinstance(priority, (int, long)):
+      priority = [priority] * len(tasks)
+    elif len(priority) != len(tasks):
+      raise errors.ProgrammerError("Number of priorities (%s) doesn't match"
+                                   " number of tasks (%s)" %
+                                   (len(priority), len(tasks)))
+
     self._lock.acquire()
     try:
       self._WaitWhileQuiescingUnlocked()
 
-      for args in tasks:
-        self._AddTaskUnlocked(args)
+      assert compat.all(isinstance(prio, (int, long)) for prio in priority)
+      assert len(tasks) == len(priority)
+
+      for args, priority in zip(tasks, priority):
+        self._AddTaskUnlocked(args, priority)
     finally:
       self._lock.release()
 
@@ -278,7 +369,7 @@ class WorkerPool(object):
 
     # Get task from queue and tell pool about it
     try:
-      return self._tasks.popleft()
+      return heapq.heappop(self._tasks)
     finally:
       self._worker_to_pool.notifyAll()
 
index 2c6ec60..49fdc67 100644 (file)
           </listitem>
         </varlistentry>
         <varlistentry>
+          <term>NIC_%N_MODE</term>
+          <listitem>
+            <simpara>The NIC mode, either routed or bridged</simpara>
+          </listitem>
+        </varlistentry>
+        <varlistentry>
           <term>NIC_%N_BRIDGE</term>
           <listitem>
-            <simpara>The bridge to which this NIC will be attached
-            to.</simpara>
+            <simpara>The bridge to which this NIC will be attached. This
+            variable is defined only when the NIC is in bridged mode.</simpara>
           </listitem>
         </varlistentry>
         <varlistentry>
+          <term>NIC_%N_LINK</term>
+          <listitem>
+           <simpara>If the NIC is in bridged mode, this is the same as
+            NIC_%N_BRIDGE. If it is in routed mode, the routing table
+            which will be used by the hypervisor to insert the appropriate
+            routes.</simpara> </listitem>
+        </varlistentry>
+        <varlistentry>
           <term>NIC_%N_FRONTEND_TYPE</term>
           <listitem>
             <para>(Optional) If applicable, the type of the exported
index 8a02417..d787253 100644 (file)
     <cmdsynopsis>
       <command>&dhpackage; </command>
 
+      <arg><option>--debug</option></arg>
+      <arg><option>--job-age=<replaceable>age</replaceable></option></arg>
+      <arg><option>--ignore-pause</option></arg>
+
     </cmdsynopsis>
   </refsynopsisdiv>
   <refsect1>
       and another one that runs on every node.
     </para>
 
+    <para>
+      If the watcher is disabled at cluster level (via
+      the <command>gnt-cluster watcher pause</command> command), it
+      will exit without doing anything. The cluster-level pause can be
+      overriden via the <option>--ignore-pause</option> option, for
+      example if during a maintenance the watcher needs to be disabled
+      in general, but the administrator wants to run it just once.
+    </para>
+
+    <para>
+      The <option>--debug</option> option will increase the verbosity
+      of the watcher and also activate logging to the standard error.
+    </para>
+
     <refsect2>
       <title>Master operations</title>
 
       </para>
 
       <para>
-        Its other function is to <quote>repair</quote> DRBD links by
+        Another function is to <quote>repair</quote> DRBD links by
         reactivating the block devices of instances which have
         secondaries on nodes that have been rebooted.
       </para>
 
+      <para>
+        The watcher will also archive old jobs (older than the age
+        given via the <option>--job-age</option> option, which
+        defaults to 6 hours), in order to keep the job queue
+        manageable.
+      </para>
+
     </refsect2>
 
     <refsect2>
       <para>
         The watcher does synchronous queries but will submit jobs for
         executing the changes. Due to locking, it could be that the jobs
-        execute much later than the watcher executes them.
+        execute much later than the watcher submits them.
       </para>
 
     </refsect2>
index f7e1bb1..3bb4f42 100644 (file)
     </refsect2>
 
     <refsect2>
+      <title>Node flags</title>
+
+      <para>Nodes have two flags which govern which roles they can take:
+        <variablelist>
+          <varlistentry>
+            <term>master_capable</term>
+            <listitem>
+              <para>
+                The node can become a master candidate, and
+                furthermore the master node. When this flag is
+                disabled, the node cannot become a candidate; this can
+                be useful for special networking cases, or less
+                reliable hardware.
+              </para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>vm_capable</term>
+            <listitem>
+              <para>
+                The node can host instances. When enabled (the default
+                state), the node will participate in instance
+                allocation, capacity calculation, etc. When disabled,
+                the node will be skipped in many cluster checks and
+                operations.
+              </para>
+            </listitem>
+          </varlistentry>
+        </variablelist>
+      </para>
+    </refsect2>
+
+    <refsect2>
       <title>Cluster configuration</title>
 
       <para>The master node keeps and is responsible for the cluster
     </refsect2>
   </refsect1>
 
+  <refsect1>
+    <title>Common options</title>
+
+    <para>
+      Many Ganeti commands provide the following options. The availability for
+      a certain command can be checked by calling the command using the
+      <option>--help</option> option.
+    </para>
+
+    <cmdsynopsis>
+      <command>gnt-<replaceable>...</replaceable> <replaceable>command</replaceable></command>
+      <arg>--dry-run</arg>
+      <arg>--priority <group choice="req">
+        <arg>low</arg>
+        <arg>normal</arg>
+        <arg>high</arg>
+      </group></arg>
+    </cmdsynopsis>
+
+    <para>
+      The <option>--dry-run</option> option can be used to check whether an
+      operation would succeed.
+    </para>
+
+    <para>
+      The option <option>--priority</option> sets the priority for opcodes
+      submitted by the command.
+    </para>
+
+  </refsect1>
+
   &footer;
 
 </refentry>
index b4ad6d8..53e9483 100644 (file)
         <sbr>
         <arg>-s <replaceable>secondary_ip</replaceable></arg>
         <sbr>
-        <arg>-g <replaceable>vg-name</replaceable></arg>
+        <arg>--vg-name <replaceable>vg-name</replaceable></arg>
         <sbr>
         <arg>--master-netdev <replaceable>interface-name</replaceable></arg>
         <sbr>
         <sbr>
         <arg>-I <replaceable>default instance allocator</replaceable></arg>
         <sbr>
+        <arg>--primary-ip-version <replaceable>version</replaceable></arg>
+        <sbr>
+        <arg>--prealloc-wipe-disks <group choice="req"><arg>yes</arg><arg>no</arg></group></arg>
+        <sbr>
         <arg choice="req"><replaceable>clustername</replaceable></arg>
       </cmdsynopsis>
 
       </para>
 
       <para>
-        The <option>-g</option> option will let you specify a volume group
+        The <option>--vg-name</option> option will let you specify a volume group
         different than "xenvg" for Ganeti to use when creating instance disks.
         This volume group must have the same name on all nodes. Once the
         cluster is initialized this can be altered by using the
       </para>
 
       <para>
+        The <option>--prealloc-wipe-disks</option> sets a cluster wide
+        configuration value for wiping disks prior to allocation. This
+        increases security on instance level as the instance can't
+        access untouched data from it's underlying storage.
+      </para>
+
+      <para>
         <variablelist>
           <varlistentry>
             <term>xen-pvm</term>
         <command>modify</command> command.
       </para>
 
+      <para>
+        The <option>--primary-ip-version</option> option specifies the
+        IP version used for the primary address. Possible values are 4 and
+        6 for IPv4 and IPv6, respectively. This option is used when resolving
+        node names and the cluster name.
+      </para>
+
     </refsect2>
 
     <refsect2>
       <cmdsynopsis>
         <command>modify</command>
         <sbr>
-        <arg choice="opt">-g <replaceable>vg-name</replaceable></arg>
+        <arg choice="opt">--vg-name <replaceable>vg-name</replaceable></arg>
         <sbr>
         <arg choice="opt">--no-lvm-storage</arg>
         <sbr>
         <sbr>
         <arg>--maintain-node-health <group choice="req"><arg>yes</arg><arg>no</arg></group></arg>
         <sbr>
+        <arg choice="opt">--prealloc-wipe-disks <group choice="req"><arg>yes</arg><arg>no</arg></group></arg>
+        <sbr>
         <arg choice="opt">-I <replaceable>default instance allocator</replaceable></arg>
 
         <sbr>
         </para>
 
         <para>
-          The <option>-g</option>, <option>--no-lvm-storarge</option>,
+          The <option>--vg-name</option>, <option>--no-lvm-storarge</option>,
           <option>--enabled-hypervisors</option>,
           <option>--hypervisor-parameters</option>,
           <option>--backend-parameters</option>,
           <option>--nic-parameters</option>,
           <option>--maintain-node-health</option>,
-          <option>--uid-pool</option> and
-          <option>-I</option> options are
-          described in the <command>init</command> command.
+          <option>--prealloc-wipe-disks</option>,
+          <option>--uid-pool</option> options are described in
+          the <command>init</command> command.
         </para>
 
       <para>
         or <option>--reserved-lvs ''</option>.
       </para>
 
+      <para>
+        The <option>-I</option> is described in
+        the <command>init</command> command. To clear the default
+        iallocator, just pass an empty string (<literal>''</literal>).
+      </para>
+
     </refsect2>
 
     <refsect2>
index abaef12..827d822 100644 (file)
@@ -1654,6 +1654,8 @@ instance5: 11225
             <arg>--secondary</arg>
             <arg>--all</arg>
           </group>
+          <sbr>
+          <arg choice="opt">-O <replaceable>OS_PARAMETERS</replaceable></arg>
           <arg>--submit</arg>
           <arg choice="opt" rep="repeat"><replaceable>instance</replaceable></arg>
         </cmdsynopsis>
@@ -1668,7 +1670,8 @@ instance5: 11225
         <para>
           The <option>--select-os</option> option switches to an
           interactive OS reinstall. The user is prompted to select the OS
-          template from the list of available OS templates.
+          template from the list of available OS templates. OS parameters
+          can be overridden using <option>-O</option>.
         </para>
 
         <para>
@@ -1690,7 +1693,6 @@ instance5: 11225
           <command>gnt-job info</command>.
         </para>
 
-
       </refsect3>
 
       <refsect3>
@@ -1744,6 +1746,7 @@ instance5: 11225
           <command>startup</command>
           <sbr>
           <arg>--force</arg>
+          <arg>--ignore-offline</arg>
           <sbr>
           <arg>--force-multiple</arg>
           <sbr>
@@ -1851,7 +1854,9 @@ instance5: 11225
 
         <para>
           Use <option>--force</option> to start even if secondary disks are
-          failing.
+          failing. <option>--ignore-offline</option> can be used to ignore
+          offline primary nodes and mark the instance as started even if
+          the primary is not available.
         </para>
 
         <para>
@@ -1907,6 +1912,7 @@ instance5: 11225
           <arg>--timeout=<replaceable>N</replaceable></arg>
           <sbr>
           <arg>--force-multiple</arg>
+          <arg>--ignore-offline</arg>
           <sbr>
           <group choice="opt">
             <arg>--instance</arg>
@@ -1957,6 +1963,12 @@ instance5: 11225
           <command>gnt-job info</command>.
         </para>
 
+        <para>
+          <option>--ignore-offline</option> can be used to ignore offline
+          primary nodes and force the instance to be marked as stopped. This
+          option should be used with care as it can lead to an
+          inconsistent cluster state.
+        </para>
 
         <para>
           Example:
index c55ee88..a3c6db6 100644 (file)
             </listitem>
           </varlistentry>
           <varlistentry>
+            <term>priority</term>
+            <listitem>
+              <simpara>current priority of the job</simpara>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
             <term>received_ts</term>
             <listitem>
               <simpara>the timestamp the job was received</simpara>
               <simpara>the list of opcode end times</simpara>
             </listitem>
           </varlistentry>
+          <varlistentry>
+            <term>oppriority</term>
+            <listitem>
+              <simpara>the priority of each opcode</simpara>
+            </listitem>
+          </varlistentry>
         </variablelist>
       </para>
 
index ae4720b..2294701 100644 (file)
@@ -64,6 +64,9 @@
         <command>add</command>
         <arg>--readd</arg>
         <arg>-s <replaceable>secondary_ip</replaceable></arg>
+        <arg>-g <replaceable>nodegroup</replaceable></arg>
+        <arg>--master-capable=<option>yes|no</option></arg>
+        <arg>--vm-capable=<option>yes|no</option></arg>
         <arg choice="req"><replaceable>nodename</replaceable></arg>
       </cmdsynopsis>
 
       </para>
 
       <para>
+        The <option>-g</option> is used to add the new node into a specific
+        node group, specified by uuid or name. If only one node group exists
+        you can skip this option, otherwise it's mandatory.
+      </para>
+
+      <para>
+        The <option>vm_capable</option>
+        and <option>master_capable</option> options are described
+        in <citerefentry><refentrytitle>ganeti</refentrytitle><manvolnum>7</manvolnum></citerefentry>,
+        and are used to set the properties of the new node.
+      </para>
+
+      <para>
         Example:
         <screen>
 # gnt-node add node5.example.com
 # gnt-node add -s 192.0.2.5 node5.example.com
+# gnt-node add -g group2 -s 192.0.2.9 node9.group2.example.com
         </screen>
       </para>
     </refsect2>
               </para>
             </listitem>
           </varlistentry>
+          <varlistentry>
+            <term>master_capable</term>
+            <listitem>
+              <para>whether the node can become a master candidate</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>vm_capable</term>
+            <listitem>
+              <para>whether the node can host instances</para>
+            </listitem>
+          </varlistentry>
         </variablelist>
       </para>
 
         <arg>--master-candidate=<option>yes|no</option></arg>
         <arg>--drained=<option>yes|no</option></arg>
         <arg>--offline=<option>yes|no</option></arg>
+        <arg>--master-capable=<option>yes|no</option></arg>
+        <arg>--vm-capable=<option>yes|no</option></arg>
+        <arg>-s <replaceable>secondary_ip</replaceable></arg>
         <arg>--auto-promote</arg>
         <arg choice="req"><replaceable>node</replaceable></arg>
       </cmdsynopsis>
         This command changes the role of the node. Each options takes
         either a literal <literal>yes</literal> or
         <literal>no</literal>, and only one option should be given as
-        <literal>yes</literal>. The meaning of the roles are described
-        in the manpage <citerefentry>
+        <literal>yes</literal>. The meaning of the roles and flags are
+        described in the manpage <citerefentry>
         <refentrytitle>ganeti</refentrytitle> <manvolnum>7</manvolnum>
         </citerefentry>.
       </para>
         </screen>
       </para>
 
+      <para>
+        The <option>-s</option> can be used to change the node's secondary ip.
+        No drbd instances can be running on the node, while this operation is
+        taking place.
+      </para>
+
       <para>Example (setting the node back to online and master candidate):
         <screen>
 # gnt-node modify --offline=no --master-candidate=yes node1.example.com
index 7d62897..af8ddb5 100755 (executable)
@@ -1,4 +1,4 @@
-#!/usr/bin/python
+#!/usr/bin/python -u
 #
 
 # Copyright (C) 2007, 2008, 2009, 2010 Google Inc.
@@ -44,6 +44,16 @@ from ganeti import rapi
 import ganeti.rapi.client
 
 
+def _FormatHeader(line, end=72, char="-"):
+  """Fill a line up to the end column.
+
+  """
+  line = "---- " + line + " "
+  line += "-" * (end-len(line))
+  line = line.rstrip()
+  return line
+
+
 def RunTest(fn, *args):
   """Runs a test after printing a header.
 
@@ -51,16 +61,22 @@ def RunTest(fn, *args):
   if fn.__doc__:
     desc = fn.__doc__.splitlines()[0].strip()
   else:
-    desc = '%r' % fn
+    desc = "%r" % fn
+
+  desc = desc.rstrip(".")
 
-  now = str(datetime.datetime.now())
+  tstart = datetime.datetime.now()
 
   print
-  print '---', now, ('-' * (55 - len(now)))
-  print desc
-  print '-' * 60
+  print _FormatHeader("%s start %s" % (tstart, desc))
 
-  return fn(*args)
+  try:
+    retval = fn(*args)
+    return retval
+  finally:
+    tstop = datetime.datetime.now()
+    tdelta = tstop - tstart
+    print _FormatHeader("%s time=%s %s" % (tstop, tdelta, desc))
 
 
 def RunEnvTests():
@@ -85,10 +101,16 @@ def SetupCluster(rapi_user, rapi_secret):
   if qa_config.TestEnabled('create-cluster'):
     RunTest(qa_cluster.TestClusterInit, rapi_user, rapi_secret)
     RunTest(qa_node.TestNodeAddAll)
-    RunTest(qa_cluster.TestJobqueue)
   else:
     # consider the nodes are already there
     qa_node.MarkNodeAddedAll()
+
+  if qa_config.TestEnabled("test-jobqueue"):
+    RunTest(qa_cluster.TestJobqueue)
+
+  # enable the watcher (unconditionally)
+  RunTest(qa_daemon.TestResumeWatcher)
+
   if qa_config.TestEnabled('node-info'):
     RunTest(qa_node.TestNodeInfo)
 
@@ -197,15 +219,20 @@ def RunCommonInstanceTests(instance):
   if qa_config.TestEnabled('tags'):
     RunTest(qa_tags.TestInstanceTags, instance)
 
+  if qa_rapi.Enabled():
+    RunTest(qa_rapi.TestInstance, instance)
+
+
+def RunCommonNodeTests():
+  """Run a few common node tests.
+
+  """
   if qa_config.TestEnabled('node-volumes'):
     RunTest(qa_node.TestNodeVolumes)
 
   if qa_config.TestEnabled("node-storage"):
     RunTest(qa_node.TestNodeStorage)
 
-  if qa_rapi.Enabled():
-    RunTest(qa_rapi.TestInstance, instance)
-
 
 def RunExportImportTests(instance, pnode, snode):
   """Tries to export and import the instance.
@@ -262,8 +289,8 @@ def RunDaemonTests(instance, pnode):
   consecutive_failures = \
     qa_config.TestEnabled('instance-consecutive-failures')
 
+  RunTest(qa_daemon.TestPauseWatcher)
   if automatic_restart or consecutive_failures:
-    qa_daemon.PrintCronWarning()
 
     if automatic_restart:
       RunTest(qa_daemon.TestInstanceAutomaticRestart, pnode, instance)
@@ -271,6 +298,8 @@ def RunDaemonTests(instance, pnode):
     if consecutive_failures:
       RunTest(qa_daemon.TestInstanceConsecutiveFailures, pnode, instance)
 
+  RunTest(qa_daemon.TestResumeWatcher)
+
 
 def RunHardwareFailureTests(instance, pnode, snode):
   """Test cluster internal hardware failure recovery.
@@ -345,13 +374,17 @@ def main():
   if qa_config.TestEnabled('tags'):
     RunTest(qa_tags.TestClusterTags)
 
-  if qa_config.TestEnabled('node-readd'):
-    master = qa_config.GetMasterNode()
-    pnode = qa_config.AcquireNode(exclude=master)
-    try:
+  RunCommonNodeTests()
+
+  pnode = qa_config.AcquireNode(exclude=qa_config.GetMasterNode())
+  try:
+    if qa_config.TestEnabled('node-readd'):
       RunTest(qa_node.TestNodeReadd, pnode)
-    finally:
-      qa_config.ReleaseNode(pnode)
+
+    if qa_config.TestEnabled("node-modify"):
+      RunTest(qa_node.TestNodeModify, pnode)
+  finally:
+    qa_config.ReleaseNode(pnode)
 
   pnode = qa_config.AcquireNode()
   try:
@@ -387,6 +420,8 @@ def main():
         snode = qa_config.AcquireNode(exclude=pnode)
         try:
           instance = RunTest(func, pnode, snode)
+          if qa_config.TestEnabled("cluster-verify"):
+            RunTest(qa_cluster.TestClusterVerify)
           RunCommonInstanceTests(instance)
           if qa_config.TestEnabled('instance-convert-disk'):
             RunTest(qa_instance.TestInstanceShutdown, instance)
@@ -401,15 +436,19 @@ def main():
 
     if (qa_config.TestEnabled('instance-add-plain-disk') and
         qa_config.TestEnabled("instance-export")):
-      instance = RunTest(qa_instance.TestInstanceAddWithPlainDisk, pnode)
-      expnode = qa_config.AcquireNode(exclude=pnode)
-      try:
-        RunTest(qa_instance.TestInstanceExportWithRemove, instance, expnode)
-        RunTest(qa_instance.TestBackupList, expnode)
-      finally:
-        qa_config.ReleaseNode(expnode)
-      del expnode
-      del instance
+      for shutdown in [False, True]:
+        instance = RunTest(qa_instance.TestInstanceAddWithPlainDisk, pnode)
+        expnode = qa_config.AcquireNode(exclude=pnode)
+        try:
+          if shutdown:
+            # Stop instance before exporting and removing it
+            RunTest(qa_instance.TestInstanceShutdown, instance)
+          RunTest(qa_instance.TestInstanceExportWithRemove, instance, expnode)
+          RunTest(qa_instance.TestBackupList, expnode)
+        finally:
+          qa_config.ReleaseNode(expnode)
+        del expnode
+        del instance
 
   finally:
     qa_config.ReleaseNode(pnode)
index 56a8a29..57fd353 100644 (file)
@@ -2,6 +2,7 @@
   "name": "xen-test",
   "rename": "xen-test-rename",
   "enabled-hypervisors": "xen-pvm",
+  "primary_ip_version": 4,
 
   "os": "debian-etch",
   "mem": "512M",
@@ -37,6 +38,7 @@
     "os": true,
     "tags": true,
     "rapi": true,
+    "test-jobqueue": true,
 
     "create-cluster": true,
     "cluster-verify": true,
@@ -55,6 +57,7 @@
     "node-volumes": true,
     "node-readd": true,
     "node-storage": true,
+    "node-modify": true,
 
     "# This test needs at least three nodes": null,
     "node-evacuate": false,
@@ -66,8 +69,6 @@
     "instance-add-drbd-disk": true,
     "instance-convert-disk": true,
 
-    "instance-automatic-restart": false,
-    "instance-consecutive-failures": false,
     "instance-export": true,
     "instance-failover": true,
     "instance-import": true,
     "instance-rename": true,
     "instance-shutdown": true,
 
+    "# cron/ganeti-watcher should be disabled for these tests": null,
+    "instance-automatic-restart": false,
+    "instance-consecutive-failures": false,
+
     "# This test might fail with certain hypervisor types, depending": null,
     "# on whether they support the `gnt-instance console' command.": null,
     "instance-console": false,
index fc4e5e7..f615a73 100644 (file)
@@ -80,6 +80,9 @@ def TestClusterInit(rapi_user, rapi_secret):
   # Initialize cluster
   cmd = ['gnt-cluster', 'init']
 
+  cmd.append("--primary-ip-version=%d" %
+             qa_config.get("primary_ip_version", 4))
+
   if master.get('secondary', None):
     cmd.append('--secondary-ip=%s' % master['secondary'])
 
index 4afe253..20f3308 100644 (file)
@@ -24,7 +24,8 @@
 """
 
 
-import simplejson
+from ganeti import utils
+from ganeti import serializer
 
 import qa_error
 
@@ -39,11 +40,7 @@ def Load(path):
   """
   global cfg
 
-  f = open(path, 'r')
-  try:
-    cfg = simplejson.load(f)
-  finally:
-    f.close()
+  cfg = serializer.LoadJson(utils.ReadFile(path))
 
   Validate()
 
@@ -63,8 +60,10 @@ def get(name, default=None):
 
 
 def TestEnabled(test):
-  """Returns True if the given test is enabled."""
-  return cfg.get('tests', {}).get(test, False)
+  """Returns True if the given test is enabled.
+
+  """
+  return cfg.get("tests", {}).get(test, True)
 
 
 def GetMasterNode():
index 4817bfe..a092803 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2007 Google Inc.
+# Copyright (C) 2007, 2008, 2009, 2010 Google Inc.
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -32,7 +32,7 @@ import qa_config
 import qa_utils
 import qa_error
 
-from qa_utils import AssertEqual, StartSSH
+from qa_utils import AssertEqual, AssertMatch, StartSSH, GetCommandOutput
 
 
 def _InstanceRunning(node, name):
@@ -89,19 +89,41 @@ def _RunWatcherDaemon():
   """
   master = qa_config.GetMasterNode()
 
-  cmd = ['ganeti-watcher', '-d']
-  AssertEqual(StartSSH(master['primary'],
+  cmd = ["ganeti-watcher", "-d", "--ignore-pause"]
+  AssertEqual(StartSSH(master["primary"],
                        utils.ShellQuoteArgs(cmd)).wait(), 0)
 
 
-def PrintCronWarning():
-  """Shows a warning about the cron job.
+def TestPauseWatcher():
+  """Tests and pauses the watcher.
 
   """
-  msg = ("For the following tests it's recommended to turn off the"
-         " ganeti-watcher cronjob.")
-  print
-  print qa_utils.FormatWarning(msg)
+  master = qa_config.GetMasterNode()
+
+  cmd = ["gnt-cluster", "watcher", "pause", "4h"]
+  AssertEqual(StartSSH(master["primary"],
+                       utils.ShellQuoteArgs(cmd)).wait(), 0)
+
+  cmd = ["gnt-cluster", "watcher", "info"]
+  output = GetCommandOutput(master["primary"],
+                            utils.ShellQuoteArgs(cmd))
+  AssertMatch(output, r"^.*\bis paused\b.*")
+
+
+def TestResumeWatcher():
+  """Tests and unpauses the watcher.
+
+  """
+  master = qa_config.GetMasterNode()
+
+  cmd = ["gnt-cluster", "watcher", "continue"]
+  AssertEqual(StartSSH(master["primary"],
+                       utils.ShellQuoteArgs(cmd)).wait(), 0)
+
+  cmd = ["gnt-cluster", "watcher", "info"]
+  output = GetCommandOutput(master["primary"],
+                            utils.ShellQuoteArgs(cmd))
+  AssertMatch(output, r"^.*\bis not paused\b.*")
 
 
 def TestInstanceAutomaticRestart(node, instance):
@@ -109,7 +131,7 @@ def TestInstanceAutomaticRestart(node, instance):
 
   """
   master = qa_config.GetMasterNode()
-  inst_name = qa_utils.ResolveInstanceName(instance)
+  inst_name = qa_utils.ResolveInstanceName(instance["name"])
 
   _ResetWatcherDaemon()
   _XmShutdownInstance(node, inst_name)
@@ -130,7 +152,7 @@ def TestInstanceConsecutiveFailures(node, instance):
 
   """
   master = qa_config.GetMasterNode()
-  inst_name = qa_utils.ResolveInstanceName(instance)
+  inst_name = qa_utils.ResolveInstanceName(instance["name"])
 
   _ResetWatcherDaemon()
 
index 3369acd..dd53198 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2007 Google Inc.
+# Copyright (C) 2007, 2010 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
@@ -69,14 +69,19 @@ def TestIcmpPing():
   """
   nodes = qa_config.get('nodes')
 
+  pingargs = ['-w', '3', '-c', '1 ']
+  pingprimary = "ping"
+  if qa_config.get("primary_ip_version") == 6:
+    pingprimary = "ping6"
+
   for node in nodes:
     check = []
     for i in nodes:
-      check.append(i['primary'])
+      cmd = [pingprimary] + pingargs + [i['primary']]
+      check.append(utils.ShellQuoteArgs(cmd))
       if i.has_key('secondary'):
-        check.append(i['secondary'])
-
-    ping = lambda ip: utils.ShellQuoteArgs(['ping', '-w', '3', '-c', '1', ip])
-    cmd = ' && '.join([ping(i) for i in check])
+        cmd = ["ping"] + pingargs + [i["secondary"]]
+        check.append(utils.ShellQuoteArgs(cmd))
 
-    AssertEqual(StartSSH(node['primary'], cmd).wait(), 0)
+    cmdall = ' && '.join(check)
+    AssertEqual(StartSSH(node['primary'], cmdall).wait(), 0)
index b8c692e..5b4b41a 100644 (file)
@@ -33,7 +33,7 @@ import qa_config
 import qa_utils
 import qa_error
 
-from qa_utils import AssertEqual, AssertNotEqual, StartSSH
+from qa_utils import AssertEqual, AssertNotEqual, AssertIn, StartSSH
 
 
 def _GetDiskStatePath(disk):
@@ -61,6 +61,9 @@ def _DiskTest(node, disk_template):
 
     AssertEqual(StartSSH(master['primary'],
                          utils.ShellQuoteArgs(cmd)).wait(), 0)
+
+    _CheckSsconfInstanceList(instance["name"])
+
     return instance
   except:
     qa_config.ReleaseInstance(instance)
@@ -129,6 +132,30 @@ def TestInstanceReinstall(instance):
                        utils.ShellQuoteArgs(cmd)).wait(), 0)
 
 
+def _ReadSsconfInstanceList():
+  """Reads ssconf_instance_list from the master node.
+
+  """
+  master = qa_config.GetMasterNode()
+
+  cmd = ["cat", utils.PathJoin(constants.DATA_DIR,
+                               "ssconf_%s" % constants.SS_INSTANCE_LIST)]
+
+  return qa_utils.GetCommandOutput(master["primary"],
+                                   utils.ShellQuoteArgs(cmd)).splitlines()
+
+
+def _CheckSsconfInstanceList(instance):
+  """Checks if a certain instance is in the ssconf instance list.
+
+  @type instance: string
+  @param instance: Instance name
+
+  """
+  AssertIn(qa_utils.ResolveInstanceName(instance),
+           _ReadSsconfInstanceList())
+
+
 def TestInstanceRename(instance, rename_target):
   """gnt-instance rename"""
   master = qa_config.GetMasterNode()
@@ -137,9 +164,11 @@ def TestInstanceRename(instance, rename_target):
 
   for name1, name2 in [(rename_source, rename_target),
                        (rename_target, rename_source)]:
+    _CheckSsconfInstanceList(name1)
     cmd = ['gnt-instance', 'rename', name1, name2]
     AssertEqual(StartSSH(master['primary'],
                          utils.ShellQuoteArgs(cmd)).wait(), 0)
+    _CheckSsconfInstanceList(name2)
 
 
 def TestInstanceFailover(instance):
@@ -287,7 +316,7 @@ def TestInstanceExport(instance, node):
   AssertEqual(StartSSH(master['primary'],
                        utils.ShellQuoteArgs(cmd)).wait(), 0)
 
-  return qa_utils.ResolveInstanceName(instance)
+  return qa_utils.ResolveInstanceName(instance["name"])
 
 
 def TestInstanceExportWithRemove(instance, node):
@@ -340,7 +369,7 @@ def _TestInstanceDiskFailure(instance, node, node2, onmaster):
   master = qa_config.GetMasterNode()
   sq = utils.ShellQuoteArgs
 
-  instance_full = qa_utils.ResolveInstanceName(instance)
+  instance_full = qa_utils.ResolveInstanceName(instance["name"])
   node_full = qa_utils.ResolveNodeName(node)
   node2_full = qa_utils.ResolveNodeName(node2)
 
index 025683c..15550f9 100644 (file)
@@ -219,3 +219,20 @@ def TestNodeEvacuate(node, node2):
                          utils.ShellQuoteArgs(cmd)).wait(), 0)
   finally:
     qa_config.ReleaseNode(node3)
+
+
+def TestNodeModify(node):
+  """gnt-node modify"""
+  master = qa_config.GetMasterNode()
+
+  for flag in ["master-candidate", "drained", "offline"]:
+    for value in ["yes", "no"]:
+      cmd = ["gnt-node", "modify", "--force",
+             "--%s=%s" % (flag, value), node["primary"]]
+      AssertEqual(StartSSH(master["primary"],
+                           utils.ShellQuoteArgs(cmd)).wait(), 0)
+
+  cmd = ["gnt-node", "modify", "--master-candidate=yes", "--auto-promote",
+         node["primary"]]
+  AssertEqual(StartSSH(master["primary"],
+                       utils.ShellQuoteArgs(cmd)).wait(), 0)
index 143fedc..e6e259a 100644 (file)
@@ -86,6 +86,9 @@ def _TestOsStates():
       new_cmd = cmd + ["--%s" % param, val, _TEMP_OS_NAME]
       AssertEqual(StartSSH(master["primary"],
                            utils.ShellQuoteArgs(new_cmd)).wait(), 0)
+      # check that double-running the command is OK
+      AssertEqual(StartSSH(master["primary"],
+                           utils.ShellQuoteArgs(new_cmd)).wait(), 0)
 
 
 def _SetupTempOs(node, dir, valid):
index df01788..a1b371e 100644 (file)
@@ -178,6 +178,24 @@ def TestEmptyCluster():
     ("/2/os", None, 'GET', None),
     ])
 
+  # Test HTTP Not Found
+  for method in ["GET", "PUT", "POST", "DELETE"]:
+    try:
+      _DoTests([("/99/resource/not/here/99", None, method, None)])
+    except rapi.client.GanetiApiError, err:
+      AssertEqual(err.code, 404)
+    else:
+      raise qa_error.Error("Non-existent resource didn't return HTTP 404")
+
+  # Test HTTP Not Implemented
+  for method in ["PUT", "POST", "DELETE"]:
+    try:
+      _DoTests([("/version", None, method, None)])
+    except rapi.client.GanetiApiError, err:
+      AssertEqual(err.code, 501)
+    else:
+      raise qa_error.Error("Non-implemented method didn't fail")
+
 
 def TestInstance(instance):
   """Testing getting instance(s) info via remote API.
index afcc191..de48794 100644 (file)
@@ -209,8 +209,11 @@ def _ResolveName(cmd, key):
 def ResolveInstanceName(instance):
   """Gets the full name of an instance.
 
+  @type instance: string
+  @param instance: Instance name
+
   """
-  return _ResolveName(['gnt-instance', 'info', instance['name']],
+  return _ResolveName(['gnt-instance', 'info', instance],
                       'Instance name')
 
 
index 0e9c1e4..adef2dd 100755 (executable)
@@ -24,6 +24,8 @@
 import unittest
 import signal
 import os
+import tempfile
+import shutil
 
 try:
   # pylint: disable-msg=E0611
@@ -149,5 +151,23 @@ class TestSingleFileEventHandler(testutils.GanetiTestCase):
     self.assertEquals(self.notifiers[self.NOTIFIER_TERM].error_count, 0)
 
 
+class TestSingleFileEventHandlerError(unittest.TestCase):
+  def setUp(self):
+    self.tmpdir = tempfile.mkdtemp()
+
+  def tearDown(self):
+    shutil.rmtree(self.tmpdir)
+
+  def test(self):
+    wm = pyinotify.WatchManager()
+    handler = asyncnotifier.SingleFileEventHandler(wm, None,
+                                                   utils.PathJoin(self.tmpdir,
+                                                                  "nonexist"))
+    self.assertRaises(errors.InotifyError, handler.enable)
+    self.assertRaises(errors.InotifyError, handler.enable)
+    handler.disable()
+    self.assertRaises(errors.InotifyError, handler.enable)
+
+
 if __name__ == "__main__":
   testutils.GanetiTestProgram()
index fa81dd4..ff9a33c 100755 (executable)
@@ -74,7 +74,8 @@ class TestX509Certificates(unittest.TestCase):
 class TestNodeVerify(testutils.GanetiTestCase):
   def testMasterIPLocalhost(self):
     # this a real functional test, but requires localhost to be reachable
-    local_data = (netutils.HostInfo().name, constants.IP4_ADDRESS_LOCALHOST)
+    local_data = (netutils.Hostname.GetSysName(),
+                  constants.IP4_ADDRESS_LOCALHOST)
     result = backend.VerifyNode({constants.NV_MASTERIP: local_data}, None)
     self.failUnless(constants.NV_MASTERIP in result,
                     "Master IP data not returned")
index 0f352ac..64e3ddb 100755 (executable)
@@ -442,6 +442,14 @@ class TestParseFields(unittest.TestCase):
                      ["def", "ault", "name", "foo"])
 
 
+class TestConstants(unittest.TestCase):
+  def testPriority(self):
+    self.assertEqual(set(cli._PRIONAME_TO_VALUE.values()),
+                     set(constants.OP_PRIO_SUBMIT_VALID))
+    self.assertEqual(list(value for _, value in cli._PRIORITY_NAMES),
+                     sorted(constants.OP_PRIO_SUBMIT_VALID, reverse=True))
+
+
 class TestParseNicOption(unittest.TestCase):
   def test(self):
     self.assertEqual(cli.ParseNicOption([("0", { "link": "eth0", })]),
index 48aa590..51284a7 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 #
 
-# Copyright (C) 2006, 2007 Google Inc.
+# Copyright (C) 2006, 2007, 2010 Google Inc.
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -37,7 +37,14 @@ from ganeti import objects
 from ganeti import utils
 from ganeti import netutils
 
+from ganeti.config import TemporaryReservationManager
+
 import testutils
+import mocks
+
+
+def _StubGetEntResolver():
+  return mocks.FakeGetentResolver()
 
 
 class TestConfigRunner(unittest.TestCase):
@@ -55,12 +62,13 @@ class TestConfigRunner(unittest.TestCase):
 
   def _get_object(self):
     """Returns a instance of ConfigWriter"""
-    cfg = config.ConfigWriter(cfg_file=self.cfg_file, offline=True)
+    cfg = config.ConfigWriter(cfg_file=self.cfg_file, offline=True,
+                              _getents=_StubGetEntResolver)
     return cfg
 
   def _init_cluster(self, cfg):
     """Initializes the cfg object"""
-    me = netutils.HostInfo()
+    me = netutils.Hostname()
     ip = constants.IP4_ADDRESS_LOCALHOST
 
     cluster_config = objects.Cluster(
@@ -180,5 +188,23 @@ class TestConfigRunner(unittest.TestCase):
                       CheckSyntax, {mode: m_bridged, link: ''})
 
 
+class TestTRM(unittest.TestCase):
+  EC_ID = 1
+
+  def testEmpty(self):
+    t = TemporaryReservationManager()
+    t.Reserve(self.EC_ID, "a")
+    self.assertFalse(t.Reserved(self.EC_ID))
+    self.assertTrue(t.Reserved("a"))
+    self.assertEqual(len(t.GetReserved()), 1)
+
+  def testDuplicate(self):
+    t = TemporaryReservationManager()
+    t.Reserve(self.EC_ID, "a")
+    self.assertRaises(errors.ReservationError, t.Reserve, 2, "a")
+    t.DropECReservations(self.EC_ID)
+    self.assertFalse(t.Reserved("a"))
+
+
 if __name__ == '__main__':
   testutils.GanetiTestProgram()
index d73d650..9aab10f 100755 (executable)
@@ -26,6 +26,7 @@ import unittest
 import re
 
 from ganeti import constants
+from ganeti import locking
 
 import testutils
 
@@ -68,6 +69,14 @@ class TestConstants(unittest.TestCase):
     self.failUnless(constants.SSL_CERT_EXPIRATION_ERROR <
                     constants.SSL_CERT_EXPIRATION_WARN)
 
+  def testOpCodePriority(self):
+    self.failUnless(constants.OP_PRIO_LOWEST > constants.OP_PRIO_LOW)
+    self.failUnless(constants.OP_PRIO_LOW > constants.OP_PRIO_NORMAL)
+    self.failUnlessEqual(constants.OP_PRIO_NORMAL, locking._DEFAULT_PRIORITY)
+    self.failUnlessEqual(constants.OP_PRIO_DEFAULT, locking._DEFAULT_PRIORITY)
+    self.failUnless(constants.OP_PRIO_NORMAL > constants.OP_PRIO_HIGH)
+    self.failUnless(constants.OP_PRIO_HIGH > constants.OP_PRIO_HIGHEST)
+
 
 class TestParameterNames(unittest.TestCase):
   """HV/BE parameter tests"""
index 3180c7c..76577ea 100755 (executable)
@@ -26,6 +26,7 @@ import os
 import unittest
 import time
 import tempfile
+from cStringIO import StringIO
 
 from ganeti import http
 
@@ -290,32 +291,24 @@ class TestHttpServerRequestAuthentication(unittest.TestCase):
           self.assert_(ac.called)
 
 
-class TestReadPasswordFile(testutils.GanetiTestCase):
-  def setUp(self):
-    testutils.GanetiTestCase.setUp(self)
-
-    self.tmpfile = tempfile.NamedTemporaryFile()
-
+class TestReadPasswordFile(unittest.TestCase):
   def testSimple(self):
-    self.tmpfile.write("user1 password")
-    self.tmpfile.flush()
-
-    users = http.auth.ReadPasswordFile(self.tmpfile.name)
+    users = http.auth.ParsePasswordFile("user1 password")
     self.assertEqual(len(users), 1)
     self.assertEqual(users["user1"].password, "password")
     self.assertEqual(len(users["user1"].options), 0)
 
   def testOptions(self):
-    self.tmpfile.write("# Passwords\n")
-    self.tmpfile.write("user1 password\n")
-    self.tmpfile.write("\n")
-    self.tmpfile.write("# Comment\n")
-    self.tmpfile.write("user2 pw write,read\n")
-    self.tmpfile.write("   \t# Another comment\n")
-    self.tmpfile.write("invalidline\n")
-    self.tmpfile.flush()
-
-    users = http.auth.ReadPasswordFile(self.tmpfile.name)
+    buf = StringIO()
+    buf.write("# Passwords\n")
+    buf.write("user1 password\n")
+    buf.write("\n")
+    buf.write("# Comment\n")
+    buf.write("user2 pw write,read\n")
+    buf.write("   \t# Another comment\n")
+    buf.write("invalidline\n")
+
+    users = http.auth.ParsePasswordFile(buf.getvalue())
     self.assertEqual(len(users), 2)
     self.assertEqual(users["user1"].password, "password")
     self.assertEqual(len(users["user1"].options), 0)
index e6746ee..0939e08 100755 (executable)
@@ -27,11 +27,15 @@ import unittest
 import tempfile
 import shutil
 import errno
+import itertools
 
 from ganeti import constants
 from ganeti import utils
 from ganeti import errors
 from ganeti import jqueue
+from ganeti import opcodes
+from ganeti import compat
+from ganeti import mcpu
 
 import testutils
 
@@ -239,5 +243,918 @@ class TestEncodeOpError(unittest.TestCase):
     self.assertRaises(errors.OpExecError, errors.MaybeRaise, encerr)
 
 
+class TestQueuedOpCode(unittest.TestCase):
+  def testDefaults(self):
+    def _Check(op):
+      self.assertFalse(hasattr(op.input, "dry_run"))
+      self.assertEqual(op.priority, constants.OP_PRIO_DEFAULT)
+      self.assertFalse(op.log)
+      self.assert_(op.start_timestamp is None)
+      self.assert_(op.exec_timestamp is None)
+      self.assert_(op.end_timestamp is None)
+      self.assert_(op.result is None)
+      self.assertEqual(op.status, constants.OP_STATUS_QUEUED)
+
+    op1 = jqueue._QueuedOpCode(opcodes.OpTestDelay())
+    _Check(op1)
+    op2 = jqueue._QueuedOpCode.Restore(op1.Serialize())
+    _Check(op2)
+    self.assertEqual(op1.Serialize(), op2.Serialize())
+
+  def testPriority(self):
+    def _Check(op):
+      assert constants.OP_PRIO_DEFAULT != constants.OP_PRIO_HIGH, \
+             "Default priority equals high priority; test can't work"
+      self.assertEqual(op.priority, constants.OP_PRIO_HIGH)
+      self.assertEqual(op.status, constants.OP_STATUS_QUEUED)
+
+    inpop = opcodes.OpGetTags(priority=constants.OP_PRIO_HIGH)
+    op1 = jqueue._QueuedOpCode(inpop)
+    _Check(op1)
+    op2 = jqueue._QueuedOpCode.Restore(op1.Serialize())
+    _Check(op2)
+    self.assertEqual(op1.Serialize(), op2.Serialize())
+
+
+class TestQueuedJob(unittest.TestCase):
+  def test(self):
+    self.assertRaises(errors.GenericError, jqueue._QueuedJob,
+                      None, 1, [])
+
+  def testDefaults(self):
+    job_id = 4260
+    ops = [
+      opcodes.OpGetTags(),
+      opcodes.OpTestDelay(),
+      ]
+
+    def _Check(job):
+      self.assertEqual(job.id, job_id)
+      self.assertEqual(job.log_serial, 0)
+      self.assert_(job.received_timestamp)
+      self.assert_(job.start_timestamp is None)
+      self.assert_(job.end_timestamp is None)
+      self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_QUEUED)
+      self.assertEqual(job.CalcPriority(), constants.OP_PRIO_DEFAULT)
+      self.assert_(repr(job).startswith("<"))
+      self.assertEqual(len(job.ops), len(ops))
+      self.assert_(compat.all(inp.__getstate__() == op.input.__getstate__()
+                              for (inp, op) in zip(ops, job.ops)))
+      self.assertRaises(errors.OpExecError, job.GetInfo,
+                        ["unknown-field"])
+      self.assertEqual(job.GetInfo(["summary"]),
+                       [[op.input.Summary() for op in job.ops]])
+
+    job1 = jqueue._QueuedJob(None, job_id, ops)
+    _Check(job1)
+    job2 = jqueue._QueuedJob.Restore(None, job1.Serialize())
+    _Check(job2)
+    self.assertEqual(job1.Serialize(), job2.Serialize())
+
+  def testPriority(self):
+    job_id = 4283
+    ops = [
+      opcodes.OpGetTags(priority=constants.OP_PRIO_DEFAULT),
+      opcodes.OpTestDelay(),
+      ]
+
+    def _Check(job):
+      self.assertEqual(job.id, job_id)
+      self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_QUEUED)
+      self.assert_(repr(job).startswith("<"))
+
+    job = jqueue._QueuedJob(None, job_id, ops)
+    _Check(job)
+    self.assert_(compat.all(op.priority == constants.OP_PRIO_DEFAULT
+                            for op in job.ops))
+    self.assertEqual(job.CalcPriority(), constants.OP_PRIO_DEFAULT)
+
+    # Increase first
+    job.ops[0].priority -= 1
+    _Check(job)
+    self.assertEqual(job.CalcPriority(), constants.OP_PRIO_DEFAULT - 1)
+
+    # Mark opcode as finished
+    job.ops[0].status = constants.OP_STATUS_SUCCESS
+    _Check(job)
+    self.assertEqual(job.CalcPriority(), constants.OP_PRIO_DEFAULT)
+
+    # Increase second
+    job.ops[1].priority -= 10
+    self.assertEqual(job.CalcPriority(), constants.OP_PRIO_DEFAULT - 10)
+
+    # Test increasing first
+    job.ops[0].status = constants.OP_STATUS_RUNNING
+    job.ops[0].priority -= 19
+    self.assertEqual(job.CalcPriority(), constants.OP_PRIO_DEFAULT - 20)
+
+  def testCalcStatus(self):
+    def _Queued(ops):
+      # The default status is "queued"
+      self.assert_(compat.all(op.status == constants.OP_STATUS_QUEUED
+                              for op in ops))
+
+    def _Waitlock1(ops):
+      ops[0].status = constants.OP_STATUS_WAITLOCK
+
+    def _Waitlock2(ops):
+      ops[0].status = constants.OP_STATUS_SUCCESS
+      ops[1].status = constants.OP_STATUS_SUCCESS
+      ops[2].status = constants.OP_STATUS_WAITLOCK
+
+    def _Running(ops):
+      ops[0].status = constants.OP_STATUS_SUCCESS
+      ops[1].status = constants.OP_STATUS_RUNNING
+      for op in ops[2:]:
+        op.status = constants.OP_STATUS_QUEUED
+
+    def _Canceling1(ops):
+      ops[0].status = constants.OP_STATUS_SUCCESS
+      ops[1].status = constants.OP_STATUS_SUCCESS
+      for op in ops[2:]:
+        op.status = constants.OP_STATUS_CANCELING
+
+    def _Canceling2(ops):
+      for op in ops:
+        op.status = constants.OP_STATUS_CANCELING
+
+    def _Canceled(ops):
+      for op in ops:
+        op.status = constants.OP_STATUS_CANCELED
+
+    def _Error1(ops):
+      for idx, op in enumerate(ops):
+        if idx > 3:
+          op.status = constants.OP_STATUS_ERROR
+        else:
+          op.status = constants.OP_STATUS_SUCCESS
+
+    def _Error2(ops):
+      for op in ops:
+        op.status = constants.OP_STATUS_ERROR
+
+    def _Success(ops):
+      for op in ops:
+        op.status = constants.OP_STATUS_SUCCESS
+
+    tests = {
+      constants.JOB_STATUS_QUEUED: [_Queued],
+      constants.JOB_STATUS_WAITLOCK: [_Waitlock1, _Waitlock2],
+      constants.JOB_STATUS_RUNNING: [_Running],
+      constants.JOB_STATUS_CANCELING: [_Canceling1, _Canceling2],
+      constants.JOB_STATUS_CANCELED: [_Canceled],
+      constants.JOB_STATUS_ERROR: [_Error1, _Error2],
+      constants.JOB_STATUS_SUCCESS: [_Success],
+      }
+
+    def _NewJob():
+      job = jqueue._QueuedJob(None, 1,
+                              [opcodes.OpTestDelay() for _ in range(10)])
+      self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_QUEUED)
+      self.assert_(compat.all(op.status == constants.OP_STATUS_QUEUED
+                              for op in job.ops))
+      return job
+
+    for status in constants.JOB_STATUS_ALL:
+      sttests = tests[status]
+      assert sttests
+      for fn in sttests:
+        job = _NewJob()
+        fn(job.ops)
+        self.assertEqual(job.CalcStatus(), status)
+
+
+class _FakeQueueForProc:
+  def __init__(self):
+    self._acquired = False
+
+  def IsAcquired(self):
+    return self._acquired
+
+  def acquire(self, shared=0):
+    assert shared == 1
+    self._acquired = True
+
+  def release(self):
+    assert self._acquired
+    self._acquired = False
+
+  def UpdateJobUnlocked(self, job, replicate=None):
+    # TODO: Ensure job is updated at the correct places
+    pass
+
+
+class _FakeExecOpCodeForProc:
+  def __init__(self, before_start, after_start):
+    self._before_start = before_start
+    self._after_start = after_start
+
+  def __call__(self, op, cbs, timeout=None, priority=None):
+    assert isinstance(op, opcodes.OpTestDummy)
+
+    if self._before_start:
+      self._before_start(timeout, priority)
+
+    cbs.NotifyStart()
+
+    if self._after_start:
+      self._after_start(op, cbs)
+
+    if op.fail:
+      raise errors.OpExecError("Error requested (%s)" % op.result)
+
+    return op.result
+
+
+class _JobProcessorTestUtils:
+  def _CreateJob(self, queue, job_id, ops):
+    job = jqueue._QueuedJob(queue, job_id, ops)
+    self.assertFalse(job.start_timestamp)
+    self.assertFalse(job.end_timestamp)
+    self.assertEqual(len(ops), len(job.ops))
+    self.assert_(compat.all(op.input == inp
+                            for (op, inp) in zip(job.ops, ops)))
+    self.assertEqual(job.GetInfo(["ops"]), [[op.__getstate__() for op in ops]])
+    return job
+
+
+class TestJobProcessor(unittest.TestCase, _JobProcessorTestUtils):
+  def _GenericCheckJob(self, job):
+    assert compat.all(isinstance(op.input, opcodes.OpTestDummy)
+                      for op in job.ops)
+
+    self.assertEqual(job.GetInfo(["opstart", "opexec", "opend"]),
+                     [[op.start_timestamp for op in job.ops],
+                      [op.exec_timestamp for op in job.ops],
+                      [op.end_timestamp for op in job.ops]])
+    self.assertEqual(job.GetInfo(["received_ts", "start_ts", "end_ts"]),
+                     [job.received_timestamp,
+                      job.start_timestamp,
+                      job.end_timestamp])
+    self.assert_(job.start_timestamp)
+    self.assert_(job.end_timestamp)
+    self.assertEqual(job.start_timestamp, job.ops[0].start_timestamp)
+
+  def testSuccess(self):
+    queue = _FakeQueueForProc()
+
+    for (job_id, opcount) in [(25351, 1), (6637, 3),
+                              (24644, 10), (32207, 100)]:
+      ops = [opcodes.OpTestDummy(result="Res%s" % i, fail=False)
+             for i in range(opcount)]
+
+      # Create job
+      job = self._CreateJob(queue, job_id, ops)
+
+      def _BeforeStart(timeout, priority):
+        self.assertFalse(queue.IsAcquired())
+        self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_WAITLOCK)
+
+      def _AfterStart(op, cbs):
+        self.assertFalse(queue.IsAcquired())
+        self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_RUNNING)
+
+        # Job is running, cancelling shouldn't be possible
+        (success, _) = job.Cancel()
+        self.assertFalse(success)
+
+      opexec = _FakeExecOpCodeForProc(_BeforeStart, _AfterStart)
+
+      for idx in range(len(ops)):
+        result = jqueue._JobProcessor(queue, opexec, job)()
+        if idx == len(ops) - 1:
+          # Last opcode
+          self.assert_(result)
+        else:
+          self.assertFalse(result)
+
+          self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_QUEUED)
+          self.assert_(job.start_timestamp)
+          self.assertFalse(job.end_timestamp)
+
+      self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_SUCCESS)
+      self.assertEqual(job.GetInfo(["status"]), [constants.JOB_STATUS_SUCCESS])
+      self.assertEqual(job.GetInfo(["opresult"]),
+                       [[op.input.result for op in job.ops]])
+      self.assertEqual(job.GetInfo(["opstatus"]),
+                       [len(job.ops) * [constants.OP_STATUS_SUCCESS]])
+      self.assert_(compat.all(op.start_timestamp and op.end_timestamp
+                              for op in job.ops))
+
+      self._GenericCheckJob(job)
+
+      # Finished jobs can't be processed any further
+      self.assertRaises(errors.ProgrammerError,
+                        jqueue._JobProcessor(queue, opexec, job))
+
+  def testOpcodeError(self):
+    queue = _FakeQueueForProc()
+
+    testdata = [
+      (17077, 1, 0, 0),
+      (1782, 5, 2, 2),
+      (18179, 10, 9, 9),
+      (4744, 10, 3, 8),
+      (23816, 100, 39, 45),
+      ]
+
+    for (job_id, opcount, failfrom, failto) in testdata:
+      # Prepare opcodes
+      ops = [opcodes.OpTestDummy(result="Res%s" % i,
+                                 fail=(failfrom <= i and
+                                       i <= failto))
+             for i in range(opcount)]
+
+      # Create job
+      job = self._CreateJob(queue, job_id, ops)
+
+      opexec = _FakeExecOpCodeForProc(None, None)
+
+      for idx in range(len(ops)):
+        result = jqueue._JobProcessor(queue, opexec, job)()
+
+        if idx in (failfrom, len(ops) - 1):
+          # Last opcode
+          self.assert_(result)
+          break
+
+        self.assertFalse(result)
+
+        self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_QUEUED)
+
+      # Check job status
+      self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_ERROR)
+      self.assertEqual(job.GetInfo(["id"]), [job_id])
+      self.assertEqual(job.GetInfo(["status"]), [constants.JOB_STATUS_ERROR])
+
+      # Check opcode status
+      data = zip(job.ops,
+                 job.GetInfo(["opstatus"])[0],
+                 job.GetInfo(["opresult"])[0])
+
+      for idx, (op, opstatus, opresult) in enumerate(data):
+        if idx < failfrom:
+          assert not op.input.fail
+          self.assertEqual(opstatus, constants.OP_STATUS_SUCCESS)
+          self.assertEqual(opresult, op.input.result)
+        elif idx <= failto:
+          assert op.input.fail
+          self.assertEqual(opstatus, constants.OP_STATUS_ERROR)
+          self.assertRaises(errors.OpExecError, errors.MaybeRaise, opresult)
+        else:
+          assert not op.input.fail
+          self.assertEqual(opstatus, constants.OP_STATUS_ERROR)
+          self.assertRaises(errors.OpExecError, errors.MaybeRaise, opresult)
+
+      self.assert_(compat.all(op.start_timestamp and op.end_timestamp
+                              for op in job.ops[:failfrom]))
+
+      self._GenericCheckJob(job)
+
+      # Finished jobs can't be processed any further
+      self.assertRaises(errors.ProgrammerError,
+                        jqueue._JobProcessor(queue, opexec, job))
+
+  def testCancelWhileInQueue(self):
+    queue = _FakeQueueForProc()
+
+    ops = [opcodes.OpTestDummy(result="Res%s" % i, fail=False)
+           for i in range(5)]
+
+    # Create job
+    job_id = 17045
+    job = self._CreateJob(queue, job_id, ops)
+
+    self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_QUEUED)
+
+    # Mark as cancelled
+    (success, _) = job.Cancel()
+    self.assert_(success)
+
+    self.assert_(compat.all(op.status == constants.OP_STATUS_CANCELED
+                            for op in job.ops))
+
+    opexec = _FakeExecOpCodeForProc(None, None)
+    jqueue._JobProcessor(queue, opexec, job)()
+
+    # Check result
+    self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_CANCELED)
+    self.assertEqual(job.GetInfo(["status"]), [constants.JOB_STATUS_CANCELED])
+    self.assertFalse(job.start_timestamp)
+    self.assert_(job.end_timestamp)
+    self.assertFalse(compat.any(op.start_timestamp or op.end_timestamp
+                                for op in job.ops))
+    self.assertEqual(job.GetInfo(["opstatus", "opresult"]),
+                     [[constants.OP_STATUS_CANCELED for _ in job.ops],
+                      ["Job canceled by request" for _ in job.ops]])
+
+  def testCancelWhileWaitlock(self):
+    queue = _FakeQueueForProc()
+
+    ops = [opcodes.OpTestDummy(result="Res%s" % i, fail=False)
+           for i in range(5)]
+
+    # Create job
+    job_id = 11009
+    job = self._CreateJob(queue, job_id, ops)
+
+    self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_QUEUED)
+
+    def _BeforeStart(timeout, priority):
+      self.assertFalse(queue.IsAcquired())
+      self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_WAITLOCK)
+
+      # Mark as cancelled
+      (success, _) = job.Cancel()
+      self.assert_(success)
+
+      self.assert_(compat.all(op.status == constants.OP_STATUS_CANCELING
+                              for op in job.ops))
+
+    def _AfterStart(op, cbs):
+      self.assertFalse(queue.IsAcquired())
+      self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_RUNNING)
+
+    opexec = _FakeExecOpCodeForProc(_BeforeStart, _AfterStart)
+
+    jqueue._JobProcessor(queue, opexec, job)()
+
+    # Check result
+    self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_CANCELED)
+    self.assertEqual(job.GetInfo(["status"]), [constants.JOB_STATUS_CANCELED])
+    self.assert_(job.start_timestamp)
+    self.assert_(job.end_timestamp)
+    self.assertFalse(compat.all(op.start_timestamp and op.end_timestamp
+                                for op in job.ops))
+    self.assertEqual(job.GetInfo(["opstatus", "opresult"]),
+                     [[constants.OP_STATUS_CANCELED for _ in job.ops],
+                      ["Job canceled by request" for _ in job.ops]])
+
+  def testCancelWhileWaitlockWithTimeout(self):
+    queue = _FakeQueueForProc()
+
+    ops = [opcodes.OpTestDummy(result="Res%s" % i, fail=False)
+           for i in range(5)]
+
+    # Create job
+    job_id = 24314
+    job = self._CreateJob(queue, job_id, ops)
+
+    self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_QUEUED)
+
+    def _BeforeStart(timeout, priority):
+      self.assertFalse(queue.IsAcquired())
+      self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_WAITLOCK)
+
+      # Mark as cancelled
+      (success, _) = job.Cancel()
+      self.assert_(success)
+
+      self.assert_(compat.all(op.status == constants.OP_STATUS_CANCELING
+                              for op in job.ops))
+
+      # Fake an acquire attempt timing out
+      raise mcpu.LockAcquireTimeout()
+
+    def _AfterStart(op, cbs):
+      self.fail("Should not reach this")
+
+    opexec = _FakeExecOpCodeForProc(_BeforeStart, _AfterStart)
+
+    self.assert_(jqueue._JobProcessor(queue, opexec, job)())
+
+    # Check result
+    self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_CANCELED)
+    self.assertEqual(job.GetInfo(["status"]), [constants.JOB_STATUS_CANCELED])
+    self.assert_(job.start_timestamp)
+    self.assert_(job.end_timestamp)
+    self.assertFalse(compat.all(op.start_timestamp and op.end_timestamp
+                                for op in job.ops))
+    self.assertEqual(job.GetInfo(["opstatus", "opresult"]),
+                     [[constants.OP_STATUS_CANCELED for _ in job.ops],
+                      ["Job canceled by request" for _ in job.ops]])
+
+  def testCancelWhileRunning(self):
+    # Tests canceling a job with finished opcodes and more, unprocessed ones
+    queue = _FakeQueueForProc()
+
+    ops = [opcodes.OpTestDummy(result="Res%s" % i, fail=False)
+           for i in range(3)]
+
+    # Create job
+    job_id = 28492
+    job = self._CreateJob(queue, job_id, ops)
+
+    self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_QUEUED)
+
+    opexec = _FakeExecOpCodeForProc(None, None)
+
+    # Run one opcode
+    self.assertFalse(jqueue._JobProcessor(queue, opexec, job)())
+
+    # Job goes back to queued
+    self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_QUEUED)
+    self.assertEqual(job.GetInfo(["opstatus", "opresult"]),
+                     [[constants.OP_STATUS_SUCCESS,
+                       constants.OP_STATUS_QUEUED,
+                       constants.OP_STATUS_QUEUED],
+                      ["Res0", None, None]])
+
+    # Mark as cancelled
+    (success, _) = job.Cancel()
+    self.assert_(success)
+
+    # Try processing another opcode (this will actually cancel the job)
+    self.assert_(jqueue._JobProcessor(queue, opexec, job)())
+
+    # Check result
+    self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_CANCELED)
+    self.assertEqual(job.GetInfo(["id"]), [job_id])
+    self.assertEqual(job.GetInfo(["status"]), [constants.JOB_STATUS_CANCELED])
+    self.assertEqual(job.GetInfo(["opstatus", "opresult"]),
+                     [[constants.OP_STATUS_SUCCESS,
+                       constants.OP_STATUS_CANCELED,
+                       constants.OP_STATUS_CANCELED],
+                      ["Res0", "Job canceled by request",
+                       "Job canceled by request"]])
+
+  def testPartiallyRun(self):
+    # Tests calling the processor on a job that's been partially run before the
+    # program was restarted
+    queue = _FakeQueueForProc()
+
+    opexec = _FakeExecOpCodeForProc(None, None)
+
+    for job_id, successcount in [(30697, 1), (2552, 4), (12489, 9)]:
+      ops = [opcodes.OpTestDummy(result="Res%s" % i, fail=False)
+             for i in range(10)]
+
+      # Create job
+      job = self._CreateJob(queue, job_id, ops)
+
+      self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_QUEUED)
+
+      for _ in range(successcount):
+        self.assertFalse(jqueue._JobProcessor(queue, opexec, job)())
+
+      self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_QUEUED)
+      self.assertEqual(job.GetInfo(["opstatus"]),
+                       [[constants.OP_STATUS_SUCCESS
+                         for _ in range(successcount)] +
+                        [constants.OP_STATUS_QUEUED
+                         for _ in range(len(ops) - successcount)]])
+
+      self.assert_(job.ops_iter)
+
+      # Serialize and restore (simulates program restart)
+      newjob = jqueue._QueuedJob.Restore(queue, job.Serialize())
+      self.assertFalse(newjob.ops_iter)
+      self._TestPartial(newjob, successcount)
+
+  def _TestPartial(self, job, successcount):
+    self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_QUEUED)
+    self.assertEqual(job.start_timestamp, job.ops[0].start_timestamp)
+
+    queue = _FakeQueueForProc()
+    opexec = _FakeExecOpCodeForProc(None, None)
+
+    for remaining in reversed(range(len(job.ops) - successcount)):
+      result = jqueue._JobProcessor(queue, opexec, job)()
+
+      if remaining == 0:
+        # Last opcode
+        self.assert_(result)
+        break
+
+      self.assertFalse(result)
+
+      self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_QUEUED)
+
+    self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_SUCCESS)
+    self.assertEqual(job.GetInfo(["status"]), [constants.JOB_STATUS_SUCCESS])
+    self.assertEqual(job.GetInfo(["opresult"]),
+                     [[op.input.result for op in job.ops]])
+    self.assertEqual(job.GetInfo(["opstatus"]),
+                     [[constants.OP_STATUS_SUCCESS for _ in job.ops]])
+    self.assert_(compat.all(op.start_timestamp and op.end_timestamp
+                            for op in job.ops))
+
+    self._GenericCheckJob(job)
+
+    # Finished jobs can't be processed any further
+    self.assertRaises(errors.ProgrammerError,
+                      jqueue._JobProcessor(queue, opexec, job))
+
+    # ... also after being restored
+    job2 = jqueue._QueuedJob.Restore(queue, job.Serialize())
+    self.assertRaises(errors.ProgrammerError,
+                      jqueue._JobProcessor(queue, opexec, job2))
+
+  def testProcessorOnRunningJob(self):
+    ops = [opcodes.OpTestDummy(result="result", fail=False)]
+
+    queue = _FakeQueueForProc()
+    opexec = _FakeExecOpCodeForProc(None, None)
+
+    # Create job
+    job = self._CreateJob(queue, 9571, ops)
+
+    self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_QUEUED)
+
+    job.ops[0].status = constants.OP_STATUS_RUNNING
+
+    assert len(job.ops) == 1
+
+    self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_RUNNING)
+
+    # Calling on running job must fail
+    self.assertRaises(errors.ProgrammerError,
+                      jqueue._JobProcessor(queue, opexec, job))
+
+  def testLogMessages(self):
+    # Tests the "Feedback" callback function
+    queue = _FakeQueueForProc()
+
+    messages = {
+      1: [
+        (None, "Hello"),
+        (None, "World"),
+        (constants.ELOG_MESSAGE, "there"),
+        ],
+      4: [
+        (constants.ELOG_JQUEUE_TEST, (1, 2, 3)),
+        (constants.ELOG_JQUEUE_TEST, ("other", "type")),
+        ],
+      }
+    ops = [opcodes.OpTestDummy(result="Logtest%s" % i, fail=False,
+                               messages=messages.get(i, []))
+           for i in range(5)]
+
+    # Create job
+    job = self._CreateJob(queue, 29386, ops)
+
+    def _BeforeStart(timeout, priority):
+      self.assertFalse(queue.IsAcquired())
+      self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_WAITLOCK)
+
+    def _AfterStart(op, cbs):
+      self.assertFalse(queue.IsAcquired())
+      self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_RUNNING)
+
+      self.assertRaises(AssertionError, cbs.Feedback,
+                        "too", "many", "arguments")
+
+      for (log_type, msg) in op.messages:
+        if log_type:
+          cbs.Feedback(log_type, msg)
+        else:
+          cbs.Feedback(msg)
+
+    opexec = _FakeExecOpCodeForProc(_BeforeStart, _AfterStart)
+
+    for remaining in reversed(range(len(job.ops))):
+      result = jqueue._JobProcessor(queue, opexec, job)()
+
+      if remaining == 0:
+        # Last opcode
+        self.assert_(result)
+        break
+
+      self.assertFalse(result)
+
+      self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_QUEUED)
+
+    self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_SUCCESS)
+    self.assertEqual(job.GetInfo(["opresult"]),
+                     [[op.input.result for op in job.ops]])
+
+    logmsgcount = sum(len(m) for m in messages.values())
+
+    self._CheckLogMessages(job, logmsgcount)
+
+    # Serialize and restore (simulates program restart)
+    newjob = jqueue._QueuedJob.Restore(queue, job.Serialize())
+    self._CheckLogMessages(newjob, logmsgcount)
+
+    # Check each message
+    prevserial = -1
+    for idx, oplog in enumerate(job.GetInfo(["oplog"])[0]):
+      for (serial, timestamp, log_type, msg) in oplog:
+        (exptype, expmsg) = messages.get(idx).pop(0)
+        if exptype:
+          self.assertEqual(log_type, exptype)
+        else:
+          self.assertEqual(log_type, constants.ELOG_MESSAGE)
+        self.assertEqual(expmsg, msg)
+        self.assert_(serial > prevserial)
+        prevserial = serial
+
+  def _CheckLogMessages(self, job, count):
+    # Check serial
+    self.assertEqual(job.log_serial, count)
+
+    # No filter
+    self.assertEqual(job.GetLogEntries(None),
+                     [entry for entries in job.GetInfo(["oplog"])[0] if entries
+                      for entry in entries])
+
+    # Filter with serial
+    assert count > 3
+    self.assert_(job.GetLogEntries(3))
+    self.assertEqual(job.GetLogEntries(3),
+                     [entry for entries in job.GetInfo(["oplog"])[0] if entries
+                      for entry in entries][3:])
+
+    # No log message after highest serial
+    self.assertFalse(job.GetLogEntries(count))
+    self.assertFalse(job.GetLogEntries(count + 3))
+
+
+class _FakeTimeoutStrategy:
+  def __init__(self, timeouts):
+    self.timeouts = timeouts
+    self.attempts = 0
+    self.last_timeout = None
+
+  def NextAttempt(self):
+    self.attempts += 1
+    if self.timeouts:
+      timeout = self.timeouts.pop(0)
+    else:
+      timeout = None
+    self.last_timeout = timeout
+    return timeout
+
+
+class TestJobProcessorTimeouts(unittest.TestCase, _JobProcessorTestUtils):
+  def setUp(self):
+    self.queue = _FakeQueueForProc()
+    self.job = None
+    self.curop = None
+    self.opcounter = None
+    self.timeout_strategy = None
+    self.retries = 0
+    self.prev_tsop = None
+    self.prev_prio = None
+    self.gave_lock = None
+    self.done_lock_before_blocking = False
+
+  def _BeforeStart(self, timeout, priority):
+    job = self.job
+
+    self.assertFalse(self.queue.IsAcquired())
+    self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_WAITLOCK)
+
+    ts = self.timeout_strategy
+
+    self.assert_(timeout is None or isinstance(timeout, (int, float)))
+    self.assertEqual(timeout, ts.last_timeout)
+    self.assertEqual(priority, job.ops[self.curop].priority)
+
+    self.gave_lock = True
+
+    if (self.curop == 3 and
+        job.ops[self.curop].priority == constants.OP_PRIO_HIGHEST + 3):
+      # Give locks before running into blocking acquire
+      assert self.retries == 7
+      self.retries = 0
+      self.done_lock_before_blocking = True
+      return
+
+    if self.retries > 0:
+      self.assert_(timeout is not None)
+      self.retries -= 1
+      self.gave_lock = False
+      raise mcpu.LockAcquireTimeout()
+
+    if job.ops[self.curop].priority == constants.OP_PRIO_HIGHEST:
+      assert self.retries == 0, "Didn't exhaust all retries at highest priority"
+      assert not ts.timeouts
+      self.assert_(timeout is None)
+
+  def _AfterStart(self, op, cbs):
+    job = self.job
+
+    self.assertFalse(self.queue.IsAcquired())
+    self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_RUNNING)
+
+    # Job is running, cancelling shouldn't be possible
+    (success, _) = job.Cancel()
+    self.assertFalse(success)
+
+  def _NextOpcode(self):
+    self.curop = self.opcounter.next()
+    self.prev_prio = self.job.ops[self.curop].priority
+
+  def _NewTimeoutStrategy(self):
+    job = self.job
+
+    self.assertEqual(self.retries, 0)
+
+    if self.prev_tsop == self.curop:
+      # Still on the same opcode, priority must've been increased
+      self.assertEqual(self.prev_prio, job.ops[self.curop].priority + 1)
+
+    if self.curop == 1:
+      # Normal retry
+      timeouts = range(10, 31, 10)
+      self.retries = len(timeouts) - 1
+
+    elif self.curop == 2:
+      # Let this run into a blocking acquire
+      timeouts = range(11, 61, 12)
+      self.retries = len(timeouts)
+
+    elif self.curop == 3:
+      # Wait for priority to increase, but give lock before blocking acquire
+      timeouts = range(12, 100, 14)
+      self.retries = len(timeouts)
+
+      self.assertFalse(self.done_lock_before_blocking)
+
+    elif self.curop == 4:
+      self.assert_(self.done_lock_before_blocking)
+
+      # Timeouts, but no need to retry
+      timeouts = range(10, 31, 10)
+      self.retries = 0
+
+    elif self.curop == 5:
+      # Normal retry
+      timeouts = range(19, 100, 11)
+      self.retries = len(timeouts)
+
+    else:
+      timeouts = []
+      self.retries = 0
+
+    assert len(job.ops) == 10
+    assert self.retries <= len(timeouts)
+
+    ts = _FakeTimeoutStrategy(timeouts)
+
+    self.timeout_strategy = ts
+    self.prev_tsop = self.curop
+    self.prev_prio = job.ops[self.curop].priority
+
+    return ts
+
+  def testTimeout(self):
+    ops = [opcodes.OpTestDummy(result="Res%s" % i, fail=False)
+           for i in range(10)]
+
+    # Create job
+    job_id = 15801
+    job = self._CreateJob(self.queue, job_id, ops)
+    self.job = job
+
+    self.opcounter = itertools.count(0)
+
+    opexec = _FakeExecOpCodeForProc(self._BeforeStart, self._AfterStart)
+    tsf = self._NewTimeoutStrategy
+
+    self.assertFalse(self.done_lock_before_blocking)
+
+    for i in itertools.count(0):
+      proc = jqueue._JobProcessor(self.queue, opexec, job,
+                                  _timeout_strategy_factory=tsf)
+
+      result = proc(_nextop_fn=self._NextOpcode)
+      if result:
+        self.assertFalse(job.cur_opctx)
+        break
+
+      self.assertFalse(result)
+
+      if self.gave_lock:
+        self.assertFalse(job.cur_opctx)
+      else:
+        self.assert_(job.cur_opctx)
+        self.assertEqual(job.cur_opctx._timeout_strategy._fn,
+                         self.timeout_strategy.NextAttempt)
+
+      self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_QUEUED)
+      self.assert_(job.start_timestamp)
+      self.assertFalse(job.end_timestamp)
+
+    self.assertEqual(self.curop, len(job.ops) - 1)
+    self.assertEqual(self.job, job)
+    self.assertEqual(self.opcounter.next(), len(job.ops))
+    self.assert_(self.done_lock_before_blocking)
+
+    self.assertEqual(job.CalcStatus(), constants.JOB_STATUS_SUCCESS)
+    self.assertEqual(job.GetInfo(["status"]), [constants.JOB_STATUS_SUCCESS])
+    self.assertEqual(job.GetInfo(["opresult"]),
+                     [[op.input.result for op in job.ops]])
+    self.assertEqual(job.GetInfo(["opstatus"]),
+                     [len(job.ops) * [constants.OP_STATUS_SUCCESS]])
+    self.assert_(compat.all(op.start_timestamp and op.end_timestamp
+                            for op in job.ops))
+
+    # Finished jobs can't be processed any further
+    self.assertRaises(errors.ProgrammerError,
+                      jqueue._JobProcessor(self.queue, opexec, job))
+
+
 if __name__ == "__main__":
   testutils.GanetiTestProgram()
index 273e817..b6416aa 100755 (executable)
@@ -28,10 +28,12 @@ import time
 import Queue
 import threading
 import random
+import itertools
 
 from ganeti import locking
 from ganeti import errors
 from ganeti import utils
+from ganeti import compat
 
 import testutils
 
@@ -701,6 +703,106 @@ class TestSharedLock(_ThreadedTestCase):
 
     self.assertRaises(Queue.Empty, self.done.get_nowait)
 
+  def testPriority(self):
+    # Acquire in exclusive mode
+    self.assert_(self.sl.acquire(shared=0))
+
+    # Queue acquires
+    def _Acquire(prev, next, shared, priority, result):
+      prev.wait()
+      self.sl.acquire(shared=shared, priority=priority, test_notify=next.set)
+      try:
+        self.done.put(result)
+      finally:
+        self.sl.release()
+
+    counter = itertools.count(0)
+    priorities = range(-20, 30)
+    first = threading.Event()
+    prev = first
+
+    # Data structure:
+    # {
+    #   priority:
+    #     [(shared/exclusive, set(acquire names), set(pending threads)),
+    #      (shared/exclusive, ...),
+    #      ...,
+    #     ],
+    # }
+    perprio = {}
+
+    # References shared acquire per priority in L{perprio}. Data structure:
+    # {
+    #   priority: (shared=1, set(acquire names), set(pending threads)),
+    # }
+    prioshared = {}
+
+    for seed in [4979, 9523, 14902, 32440]:
+      # Use a deterministic random generator
+      rnd = random.Random(seed)
+      for priority in [rnd.choice(priorities) for _ in range(30)]:
+        modes = [0, 1]
+        rnd.shuffle(modes)
+        for shared in modes:
+          # Unique name
+          acqname = "%s/shr=%s/prio=%s" % (counter.next(), shared, priority)
+
+          ev = threading.Event()
+          thread = self._addThread(target=_Acquire,
+                                   args=(prev, ev, shared, priority, acqname))
+          prev = ev
+
+          # Record expected aqcuire, see above for structure
+          data = (shared, set([acqname]), set([thread]))
+          priolist = perprio.setdefault(priority, [])
+          if shared:
+            priosh = prioshared.get(priority, None)
+            if priosh:
+              # Shared acquires are merged
+              for i, j in zip(priosh[1:], data[1:]):
+                i.update(j)
+              assert data[0] == priosh[0]
+            else:
+              prioshared[priority] = data
+              priolist.append(data)
+          else:
+            priolist.append(data)
+
+    # Start all acquires and wait for them
+    first.set()
+    prev.wait()
+
+    # Check lock information
+    self.assertEqual(self.sl.GetInfo(["name"]), [self.sl.name])
+    self.assertEqual(self.sl.GetInfo(["mode", "owner"]),
+                     ["exclusive", [threading.currentThread().getName()]])
+    self.assertEqual(self.sl.GetInfo(["name", "pending"]),
+                     [self.sl.name,
+                      [(["exclusive", "shared"][int(bool(shared))],
+                        sorted([t.getName() for t in threads]))
+                       for acquires in [perprio[i]
+                                        for i in sorted(perprio.keys())]
+                       for (shared, _, threads) in acquires]])
+
+    # Let threads acquire the lock
+    self.sl.release()
+
+    # Wait for everything to finish
+    self._waitThreads()
+
+    self.assert_(self.sl._check_empty())
+
+    # Check acquires by priority
+    for acquires in [perprio[i] for i in sorted(perprio.keys())]:
+      for (_, names, _) in acquires:
+        # For shared acquires, the set will contain 1..n entries. For exclusive
+        # acquires only one.
+        while names:
+          names.remove(self.done.get_nowait())
+      self.assertFalse(compat.any(names for (_, names, _) in acquires))
+
+    self.assertRaises(Queue.Empty, self.done.get_nowait)
+
 
 class TestSharedLockInCondition(_ThreadedTestCase):
   """SharedLock as a condition lock tests"""
@@ -1259,6 +1361,57 @@ class TestLockSet(_ThreadedTestCase):
     self.assertEqual(self.done.get_nowait(), 'DONE')
     self._setUpLS()
 
+  def testPriority(self):
+    def _Acquire(prev, next, name, priority, success_fn):
+      prev.wait()
+      self.assert_(self.ls.acquire(name, shared=0,
+                                   priority=priority,
+                                   test_notify=lambda _: next.set()))
+      try:
+        success_fn()
+      finally:
+        self.ls.release()
+
+    # Get all in exclusive mode
+    self.assert_(self.ls.acquire(locking.ALL_SET, shared=0))
+
+    done_two = Queue.Queue(0)
+
+    first = threading.Event()
+    prev = first
+
+    acquires = [("one", prio, self.done) for prio in range(1, 33)]
+    acquires.extend([("two", prio, done_two) for prio in range(1, 33)])
+
+    # Use a deterministic random generator
+    random.Random(741).shuffle(acquires)
+
+    for (name, prio, done) in acquires:
+      ev = threading.Event()
+      self._addThread(target=_Acquire,
+                      args=(prev, ev, name, prio,
+                            compat.partial(done.put, "Prio%s" % prio)))
+      prev = ev
+
+    # Start acquires
+    first.set()
+
+    # Wait for last acquire to start
+    prev.wait()
+
+    # Let threads acquire locks
+    self.ls.release()
+
+    # Wait for threads to finish
+    self._waitThreads()
+
+    for i in range(1, 33):
+      self.assertEqual(self.done.get_nowait(), "Prio%s" % i)
+      self.assertEqual(done_two.get_nowait(), "Prio%s" % i)
+
+    self.assertRaises(Queue.Empty, self.done.get_nowait)
+    self.assertRaises(Queue.Empty, done_two.get_nowait)
+
 
 class TestGanetiLockManager(_ThreadedTestCase):
 
@@ -1266,8 +1419,7 @@ class TestGanetiLockManager(_ThreadedTestCase):
     _ThreadedTestCase.setUp(self)
     self.nodes=['n1', 'n2']
     self.instances=['i1', 'i2', 'i3']
-    self.GL = locking.GanetiLockManager(nodes=self.nodes,
-                                        instances=self.instances)
+    self.GL = locking.GanetiLockManager(self.nodes, self.instances)
 
   def tearDown(self):
     # Don't try this at home...
@@ -1282,7 +1434,7 @@ class TestGanetiLockManager(_ThreadedTestCase):
       self.assertEqual(i, locking.LEVELS[i])
 
   def testDoubleGLFails(self):
-    self.assertRaises(AssertionError, locking.GanetiLockManager)
+    self.assertRaises(AssertionError, locking.GanetiLockManager, [], [])
 
   def testLockNames(self):
     self.assertEqual(self.GL._names(locking.LEVEL_CLUSTER), set(['BGL']))
@@ -1382,6 +1534,20 @@ class TestGanetiLockManager(_ThreadedTestCase):
     self.assertRaises(AssertionError, self.GL.acquire,
                       locking.LEVEL_INSTANCE, ['i2'])
 
+  def testModifiableLevels(self):
+    self.assertRaises(AssertionError, self.GL.add, locking.LEVEL_CLUSTER,
+                      ['BGL2'])
+    self.GL.acquire(locking.LEVEL_CLUSTER, ['BGL'])
+    self.GL.add(locking.LEVEL_INSTANCE, ['i4'])
+    self.GL.remove(locking.LEVEL_INSTANCE, ['i3'])
+    self.GL.remove(locking.LEVEL_INSTANCE, ['i1'])
+    self.assertEqual(self.GL._names(locking.LEVEL_INSTANCE), set(['i2', 'i4']))
+    self.GL.add(locking.LEVEL_NODE, ['n3'])
+    self.GL.remove(locking.LEVEL_NODE, ['n1'])
+    self.assertEqual(self.GL._names(locking.LEVEL_NODE), set(['n2', 'n3']))
+    self.assertRaises(AssertionError, self.GL.remove, locking.LEVEL_CLUSTER,
+                      ['BGL2'])
+
   # Helper function to run as a thread that shared the BGL and then acquires
   # some locks at another level.
   def _doLock(self, level, names, shared):
index ec54a47..66116d1 100755 (executable)
@@ -24,6 +24,8 @@
 
 import unittest
 
+from ganeti import constants
+from ganeti import errors
 from ganeti import luxi
 from ganeti import serializer
 
@@ -38,7 +40,7 @@ class TestLuxiParsing(testutils.GanetiTestCase):
       })
 
     self.assertEqualValues(luxi.ParseRequest(msg),
-                           ("foo", ["bar", "baz", 123]))
+                           ("foo", ["bar", "baz", 123], None))
 
     self.assertRaises(luxi.ProtocolError, luxi.ParseRequest,
                       "this\"is {invalid, ]json data")
@@ -59,13 +61,27 @@ class TestLuxiParsing(testutils.GanetiTestCase):
     self.assertRaises(luxi.ProtocolError, luxi.ParseRequest,
                       serializer.DumpJson({ luxi.KEY_ARGS: [], }))
 
+    # No method or arguments
+    self.assertRaises(luxi.ProtocolError, luxi.ParseRequest,
+                      serializer.DumpJson({ luxi.KEY_VERSION: 1, }))
+
+  def testParseRequestWithVersion(self):
+    msg = serializer.DumpJson({
+      luxi.KEY_METHOD: "version",
+      luxi.KEY_ARGS: (["some"], "args", 0, "here"),
+      luxi.KEY_VERSION: 20100101,
+      })
+
+    self.assertEqualValues(luxi.ParseRequest(msg),
+                           ("version", [["some"], "args", 0, "here"], 20100101))
+
   def testParseResponse(self):
     msg = serializer.DumpJson({
       luxi.KEY_SUCCESS: True,
       luxi.KEY_RESULT: None,
       })
 
-    self.assertEqual(luxi.ParseResponse(msg), (True, None))
+    self.assertEqual(luxi.ParseResponse(msg), (True, None, None))
 
     self.assertRaises(luxi.ProtocolError, luxi.ParseResponse,
                       "this\"is {invalid, ]json data")
@@ -86,6 +102,19 @@ class TestLuxiParsing(testutils.GanetiTestCase):
     self.assertRaises(luxi.ProtocolError, luxi.ParseResponse,
                       serializer.DumpJson({ luxi.KEY_SUCCESS: True, }))
 
+    # No result or success
+    self.assertRaises(luxi.ProtocolError, luxi.ParseResponse,
+                      serializer.DumpJson({ luxi.KEY_VERSION: 123, }))
+
+  def testParseResponseWithVersion(self):
+    msg = serializer.DumpJson({
+      luxi.KEY_SUCCESS: True,
+      luxi.KEY_RESULT: "Hello World",
+      luxi.KEY_VERSION: 19991234,
+      })
+
+    self.assertEqual(luxi.ParseResponse(msg), (True, "Hello World", 19991234))
+
   def testFormatResponse(self):
     for success, result in [(False, "error"), (True, "abc"),
                             (True, { "a": 123, "b": None, })]:
@@ -93,9 +122,24 @@ class TestLuxiParsing(testutils.GanetiTestCase):
       msgdata = serializer.LoadJson(msg)
       self.assert_(luxi.KEY_SUCCESS in msgdata)
       self.assert_(luxi.KEY_RESULT in msgdata)
+      self.assert_(luxi.KEY_VERSION not in msgdata)
+      self.assertEqualValues(msgdata,
+                             { luxi.KEY_SUCCESS: success,
+                               luxi.KEY_RESULT: result,
+                             })
+
+  def testFormatResponseWithVersion(self):
+    for success, result, version in [(False, "error", 123), (True, "abc", 999),
+                                     (True, { "a": 123, "b": None, }, 2010)]:
+      msg = luxi.FormatResponse(success, result, version=version)
+      msgdata = serializer.LoadJson(msg)
+      self.assert_(luxi.KEY_SUCCESS in msgdata)
+      self.assert_(luxi.KEY_RESULT in msgdata)
+      self.assert_(luxi.KEY_VERSION in msgdata)
       self.assertEqualValues(msgdata,
                              { luxi.KEY_SUCCESS: success,
                                luxi.KEY_RESULT: result,
+                               luxi.KEY_VERSION: version,
                              })
 
   def testFormatRequest(self):
@@ -104,11 +148,106 @@ class TestLuxiParsing(testutils.GanetiTestCase):
       msgdata = serializer.LoadJson(msg)
       self.assert_(luxi.KEY_METHOD in msgdata)
       self.assert_(luxi.KEY_ARGS in msgdata)
+      self.assert_(luxi.KEY_VERSION not in msgdata)
+      self.assertEqualValues(msgdata,
+                             { luxi.KEY_METHOD: method,
+                               luxi.KEY_ARGS: args,
+                             })
+
+  def testFormatRequestWithVersion(self):
+    for method, args, version in [("fn1", [], 123), ("fn2", [1, 2, 3], 999)]:
+      msg = luxi.FormatRequest(method, args, version=version)
+      msgdata = serializer.LoadJson(msg)
+      self.assert_(luxi.KEY_METHOD in msgdata)
+      self.assert_(luxi.KEY_ARGS in msgdata)
+      self.assert_(luxi.KEY_VERSION in msgdata)
       self.assertEqualValues(msgdata,
                              { luxi.KEY_METHOD: method,
                                luxi.KEY_ARGS: args,
+                               luxi.KEY_VERSION: version,
                              })
 
 
+class TestCallLuxiMethod(unittest.TestCase):
+  MY_LUXI_VERSION = 1234
+  assert constants.LUXI_VERSION != MY_LUXI_VERSION
+
+  def testSuccessNoVersion(self):
+    def _Cb(msg):
+      (method, args, version) = luxi.ParseRequest(msg)
+      self.assertEqual(method, "fn1")
+      self.assertEqual(args, "Hello World")
+      return luxi.FormatResponse(True, "x")
+
+    result = luxi.CallLuxiMethod(_Cb, "fn1", "Hello World")
+
+  def testServerVersionOnly(self):
+    def _Cb(msg):
+      (method, args, version) = luxi.ParseRequest(msg)
+      self.assertEqual(method, "fn1")
+      self.assertEqual(args, "Hello World")
+      return luxi.FormatResponse(True, "x", version=self.MY_LUXI_VERSION)
+
+    self.assertRaises(errors.LuxiError, luxi.CallLuxiMethod,
+                      _Cb, "fn1", "Hello World")
+
+  def testWithVersion(self):
+    def _Cb(msg):
+      (method, args, version) = luxi.ParseRequest(msg)
+      self.assertEqual(method, "fn99")
+      self.assertEqual(args, "xyz")
+      return luxi.FormatResponse(True, "y", version=self.MY_LUXI_VERSION)
+
+    self.assertEqual("y", luxi.CallLuxiMethod(_Cb, "fn99", "xyz",
+                                              version=self.MY_LUXI_VERSION))
+
+  def testVersionMismatch(self):
+    def _Cb(msg):
+      (method, args, version) = luxi.ParseRequest(msg)
+      self.assertEqual(method, "fn5")
+      self.assertEqual(args, "xyz")
+      return luxi.FormatResponse(True, "F", version=self.MY_LUXI_VERSION * 2)
+
+    self.assertRaises(errors.LuxiError, luxi.CallLuxiMethod,
+                      _Cb, "fn5", "xyz", version=self.MY_LUXI_VERSION)
+
+  def testError(self):
+    def _Cb(msg):
+      (method, args, version) = luxi.ParseRequest(msg)
+      self.assertEqual(method, "fnErr")
+      self.assertEqual(args, [])
+      err = errors.OpPrereqError("Test")
+      return luxi.FormatResponse(False, errors.EncodeException(err))
+
+    self.assertRaises(errors.OpPrereqError, luxi.CallLuxiMethod,
+                      _Cb, "fnErr", [])
+
+  def testErrorWithVersionMismatch(self):
+    def _Cb(msg):
+      (method, args, version) = luxi.ParseRequest(msg)
+      self.assertEqual(method, "fnErr")
+      self.assertEqual(args, [])
+      err = errors.OpPrereqError("TestVer")
+      return luxi.FormatResponse(False, errors.EncodeException(err),
+                                 version=self.MY_LUXI_VERSION * 2)
+
+    self.assertRaises(errors.LuxiError, luxi.CallLuxiMethod,
+                      _Cb, "fnErr", [],
+                      version=self.MY_LUXI_VERSION)
+
+  def testErrorWithVersion(self):
+    def _Cb(msg):
+      (method, args, version) = luxi.ParseRequest(msg)
+      self.assertEqual(method, "fn9")
+      self.assertEqual(args, [])
+      err = errors.OpPrereqError("TestVer")
+      return luxi.FormatResponse(False, errors.EncodeException(err),
+                                 version=self.MY_LUXI_VERSION)
+
+    self.assertRaises(errors.OpPrereqError, luxi.CallLuxiMethod,
+                      _Cb, "fn9", [],
+                      version=self.MY_LUXI_VERSION)
+
+
 if __name__ == "__main__":
   testutils.GanetiTestProgram()
index 80d4088..bdfd3a2 100755 (executable)
@@ -31,32 +31,27 @@ import testutils
 
 class TestLockAttemptTimeoutStrategy(unittest.TestCase):
   def testConstants(self):
-    tpa = mcpu._LockAttemptTimeoutStrategy._TIMEOUT_PER_ATTEMPT
+    tpa = mcpu.LockAttemptTimeoutStrategy._TIMEOUT_PER_ATTEMPT
     self.assert_(len(tpa) > 10)
     self.assert_(sum(tpa) >= 150.0)
 
   def testSimple(self):
-    strat = mcpu._LockAttemptTimeoutStrategy(_random_fn=lambda: 0.5,
-                                             _time_fn=lambda: 0.0)
-
-    self.assertEqual(strat._attempt, 0)
+    strat = mcpu.LockAttemptTimeoutStrategy(_random_fn=lambda: 0.5,
+                                            _time_fn=lambda: 0.0)
 
     prev = None
-    for i in range(len(mcpu._LockAttemptTimeoutStrategy._TIMEOUT_PER_ATTEMPT)):
-      timeout = strat.CalcRemainingTimeout()
+    for i in range(len(strat._TIMEOUT_PER_ATTEMPT)):
+      timeout = strat.NextAttempt()
       self.assert_(timeout is not None)
 
       self.assert_(timeout <= 10.0)
       self.assert_(timeout >= 0.0)
       self.assert_(prev is None or timeout >= prev)
 
-      strat = strat.NextAttempt()
-      self.assertEqual(strat._attempt, i + 1)
-
       prev = timeout
 
     for _ in range(10):
-      self.assert_(strat.CalcRemainingTimeout() is None)
+      self.assert_(strat.NextAttempt() is None)
 
 
 if __name__ == "__main__":
index bedbdca..e9ed0db 100755 (executable)
@@ -102,21 +102,21 @@ class TestGetSocketCredentials(unittest.TestCase):
     self.assertEqual(gid, os.getgid())
 
 
-class TestHostInfo(unittest.TestCase):
-  """Testing case for HostInfo"""
+class TestHostname(unittest.TestCase):
+  """Testing case for Hostname"""
 
   def testUppercase(self):
     data = "AbC.example.com"
-    self.failUnlessEqual(netutils.HostInfo.NormalizeName(data), data.lower())
+    self.assertEqual(netutils.Hostname.GetNormalizedName(data), data.lower())
 
   def testTooLongName(self):
     data = "a.b." + "c" * 255
-    self.failUnlessRaises(errors.OpPrereqError,
-                          netutils.HostInfo.NormalizeName, data)
+    self.assertRaises(errors.OpPrereqError,
+                      netutils.Hostname.GetNormalizedName, data)
 
   def testTrailingDot(self):
     data = "a.b.c"
-    self.failUnlessEqual(netutils.HostInfo.NormalizeName(data + "."), data)
+    self.assertEqual(netutils.Hostname.GetNormalizedName(data + "."), data)
 
   def testInvalidName(self):
     data = [
@@ -126,8 +126,8 @@ class TestHostInfo(unittest.TestCase):
       "a..b",
       ]
     for value in data:
-      self.failUnlessRaises(errors.OpPrereqError,
-                            netutils.HostInfo.NormalizeName, value)
+      self.assertRaises(errors.OpPrereqError,
+                        netutils.Hostname.GetNormalizedName, value)
 
   def testValidName(self):
     data = [
@@ -137,55 +137,116 @@ class TestHostInfo(unittest.TestCase):
       "a.b.c",
       ]
     for value in data:
-      netutils.HostInfo.NormalizeName(value)
+      self.assertEqual(netutils.Hostname.GetNormalizedName(value), value)
 
 
-class TestIsValidIP4(unittest.TestCase):
-  def test(self):
-    self.assert_(netutils.IsValidIP4("127.0.0.1"))
-    self.assert_(netutils.IsValidIP4("0.0.0.0"))
-    self.assert_(netutils.IsValidIP4("255.255.255.255"))
-    self.assertFalse(netutils.IsValidIP4("0"))
-    self.assertFalse(netutils.IsValidIP4("1"))
-    self.assertFalse(netutils.IsValidIP4("1.1.1"))
-    self.assertFalse(netutils.IsValidIP4("255.255.255.256"))
-    self.assertFalse(netutils.IsValidIP4("::1"))
-
+class TestIPAddress(unittest.TestCase):
+  def testIsValid(self):
+    self.assert_(netutils.IPAddress.IsValid("0.0.0.0"))
+    self.assert_(netutils.IPAddress.IsValid("127.0.0.1"))
+    self.assert_(netutils.IPAddress.IsValid("::"))
+    self.assert_(netutils.IPAddress.IsValid("::1"))
 
-class TestIsValidIP6(unittest.TestCase):
-  def test(self):
-    self.assert_(netutils.IsValidIP6("::"))
-    self.assert_(netutils.IsValidIP6("::1"))
-    self.assert_(netutils.IsValidIP6("1" + (":1" * 7)))
-    self.assert_(netutils.IsValidIP6("ffff" + (":ffff" * 7)))
-    self.assertFalse(netutils.IsValidIP6("0"))
-    self.assertFalse(netutils.IsValidIP6(":1"))
-    self.assertFalse(netutils.IsValidIP6("f" + (":f" * 6)))
-    self.assertFalse(netutils.IsValidIP6("fffg" + (":ffff" * 7)))
-    self.assertFalse(netutils.IsValidIP6("fffff" + (":ffff" * 7)))
-    self.assertFalse(netutils.IsValidIP6("1" + (":1" * 8)))
-    self.assertFalse(netutils.IsValidIP6("127.0.0.1"))
-
-
-class TestIsValidIP(unittest.TestCase):
-  def test(self):
-    self.assert_(netutils.IsValidIP("0.0.0.0"))
-    self.assert_(netutils.IsValidIP("127.0.0.1"))
-    self.assert_(netutils.IsValidIP("::"))
-    self.assert_(netutils.IsValidIP("::1"))
-    self.assertFalse(netutils.IsValidIP("0"))
-    self.assertFalse(netutils.IsValidIP("1.1.1.256"))
-    self.assertFalse(netutils.IsValidIP("a:g::1"))
+  def testNotIsValid(self):
+    self.assertFalse(netutils.IPAddress.IsValid("0"))
+    self.assertFalse(netutils.IPAddress.IsValid("1.1.1.256"))
+    self.assertFalse(netutils.IPAddress.IsValid("a:g::1"))
 
+  def testGetAddressFamily(self):
+    fn = netutils.IPAddress.GetAddressFamily
+    self.assertEqual(fn("127.0.0.1"), socket.AF_INET)
+    self.assertEqual(fn("10.2.0.127"), socket.AF_INET)
+    self.assertEqual(fn("::1"), socket.AF_INET6)
+    self.assertEqual(fn("2001:db8::1"), socket.AF_INET6)
+    self.assertRaises(errors.IPAddressError, fn, "0")
 
-class TestGetAddressFamily(unittest.TestCase):
-  def test(self):
-    self.assertEqual(netutils.GetAddressFamily("127.0.0.1"), socket.AF_INET)
-    self.assertEqual(netutils.GetAddressFamily("10.2.0.127"), socket.AF_INET)
-    self.assertEqual(netutils.GetAddressFamily("::1"), socket.AF_INET6)
-    self.assertEqual(netutils.GetAddressFamily("fe80::a00:27ff:fe08:5048"),
-                     socket.AF_INET6)
-    self.assertRaises(errors.GenericError, netutils.GetAddressFamily, "0")
+  def testOwnLoopback(self):
+    # FIXME: In a pure IPv6 environment this is no longer true
+    self.assert_(netutils.IPAddress.Own("127.0.0.1"),
+                 "Should own 127.0.0.1 address")
+
+  def testNotOwnAddress(self):
+    self.assertFalse(netutils.IPAddress.Own("2001:db8::1"),
+                     "Should not own IP address 2001:db8::1")
+    self.assertFalse(netutils.IPAddress.Own("192.0.2.1"),
+                     "Should not own IP address 192.0.2.1")
+
+
+class TestIP4Address(unittest.TestCase):
+  def testGetIPIntFromString(self):
+    fn = netutils.IP4Address._GetIPIntFromString
+    self.assertEqual(fn("0.0.0.0"), 0)
+    self.assertEqual(fn("0.0.0.1"), 1)
+    self.assertEqual(fn("127.0.0.1"), 2130706433)
+    self.assertEqual(fn("192.0.2.129"), 3221226113)
+    self.assertEqual(fn("255.255.255.255"), 2**32 - 1)
+    self.assertNotEqual(fn("0.0.0.0"), 1)
+    self.assertNotEqual(fn("0.0.0.0"), 1)
+
+  def testIsValid(self):
+    self.assert_(netutils.IP4Address.IsValid("0.0.0.0"))
+    self.assert_(netutils.IP4Address.IsValid("127.0.0.1"))
+    self.assert_(netutils.IP4Address.IsValid("192.0.2.199"))
+    self.assert_(netutils.IP4Address.IsValid("255.255.255.255"))
+
+  def testNotIsValid(self):
+    self.assertFalse(netutils.IP4Address.IsValid("0"))
+    self.assertFalse(netutils.IP4Address.IsValid("1"))
+    self.assertFalse(netutils.IP4Address.IsValid("1.1.1"))
+    self.assertFalse(netutils.IP4Address.IsValid("255.255.255.256"))
+    self.assertFalse(netutils.IP4Address.IsValid("::1"))
+
+  def testInNetwork(self):
+    self.assert_(netutils.IP4Address.InNetwork("127.0.0.0/8", "127.0.0.1"))
+
+  def testNotInNetwork(self):
+    self.assertFalse(netutils.IP4Address.InNetwork("192.0.2.0/24",
+                                                   "127.0.0.1"))
+
+  def testIsLoopback(self):
+    self.assert_(netutils.IP4Address.IsLoopback("127.0.0.1"))
+
+  def testNotIsLoopback(self):
+    self.assertFalse(netutils.IP4Address.IsLoopback("192.0.2.1"))
+
+
+class TestIP6Address(unittest.TestCase):
+  def testGetIPIntFromString(self):
+    fn = netutils.IP6Address._GetIPIntFromString
+    self.assertEqual(fn("::"), 0)
+    self.assertEqual(fn("::1"), 1)
+    self.assertEqual(fn("2001:db8::1"),
+                     42540766411282592856903984951653826561L)
+    self.assertEqual(fn("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"), 2**128-1)
+    self.assertNotEqual(netutils.IP6Address._GetIPIntFromString("::2"), 1)
+
+  def testIsValid(self):
+    self.assert_(netutils.IP6Address.IsValid("::"))
+    self.assert_(netutils.IP6Address.IsValid("::1"))
+    self.assert_(netutils.IP6Address.IsValid("1" + (":1" * 7)))
+    self.assert_(netutils.IP6Address.IsValid("ffff" + (":ffff" * 7)))
+    self.assert_(netutils.IP6Address.IsValid("::"))
+
+  def testNotIsValid(self):
+    self.assertFalse(netutils.IP6Address.IsValid("0"))
+    self.assertFalse(netutils.IP6Address.IsValid(":1"))
+    self.assertFalse(netutils.IP6Address.IsValid("f" + (":f" * 6)))
+    self.assertFalse(netutils.IP6Address.IsValid("fffg" + (":ffff" * 7)))
+    self.assertFalse(netutils.IP6Address.IsValid("fffff" + (":ffff" * 7)))
+    self.assertFalse(netutils.IP6Address.IsValid("1" + (":1" * 8)))
+    self.assertFalse(netutils.IP6Address.IsValid("127.0.0.1"))
+
+  def testInNetwork(self):
+    self.assert_(netutils.IP6Address.InNetwork("::1/128", "::1"))
+
+  def testNotInNetwork(self):
+    self.assertFalse(netutils.IP6Address.InNetwork("2001:db8::1/128", "::1"))
+
+  def testIsLoopback(self):
+    self.assert_(netutils.IP6Address.IsLoopback("::1"))
+
+  def testNotIsLoopback(self):
+    self.assertFalse(netutils.IP6Address.IsLoopback("2001:db8::1"))
 
 
 class _BaseTcpPingTest:
@@ -323,23 +384,42 @@ class TestIP6TcpPingDeaf(unittest.TestCase, _BaseTcpPingDeafTest):
     _BaseTcpPingDeafTest.tearDown(self)
 
 
-class TestOwnIpAddress(unittest.TestCase):
-  """Testcase for OwnIpAddress"""
-
-  def testOwnLoopback(self):
-    """check having the loopback ip"""
-    self.failUnless(netutils.OwnIpAddress(constants.IP4_ADDRESS_LOCALHOST),
-                    "Should own the loopback address")
-
-  def testNowOwnAddress(self):
-    """check that I don't own an address"""
-
-    # Network 192.0.2.0/24 is reserved for test/documentation as per
-    # RFC 5737, so we *should* not have an address of this range... if
-    # this fails, we should extend the test to multiple addresses
-    DST_IP = "192.0.2.1"
-    self.failIf(netutils.OwnIpAddress(DST_IP),
-                "Should not own IP address %s" % DST_IP)
+class TestFormatAddress(unittest.TestCase):
+  """Testcase for FormatAddress"""
+
+  def testFormatAddressUnixSocket(self):
+    res1 = netutils.FormatAddress(("12352", 0, 0), family=socket.AF_UNIX)
+    self.assertEqual(res1, "pid=12352, uid=0, gid=0")
+
+  def testFormatAddressIP4(self):
+    res1 = netutils.FormatAddress(("127.0.0.1", 1234), family=socket.AF_INET)
+    self.assertEqual(res1, "127.0.0.1:1234")
+    res2 = netutils.FormatAddress(("192.0.2.32", None), family=socket.AF_INET)
+    self.assertEqual(res2, "192.0.2.32")
+
+  def testFormatAddressIP6(self):
+    res1 = netutils.FormatAddress(("::1", 1234), family=socket.AF_INET6)
+    self.assertEqual(res1, "[::1]:1234")
+    res2 = netutils.FormatAddress(("::1", None), family=socket.AF_INET6)
+    self.assertEqual(res2, "[::1]")
+    res2 = netutils.FormatAddress(("2001:db8::beef", "80"),
+                                  family=socket.AF_INET6)
+    self.assertEqual(res2, "[2001:db8::beef]:80")
+
+  def testFormatAddressWithoutFamily(self):
+    res1 = netutils.FormatAddress(("127.0.0.1", 1234))
+    self.assertEqual(res1, "127.0.0.1:1234")
+    res2 = netutils.FormatAddress(("::1", 1234))
+    self.assertEqual(res2, "[::1]:1234")
+
+
+  def testInvalidFormatAddress(self):
+    self.assertRaises(errors.ParameterError, netutils.FormatAddress,
+                      "127.0.0.1")
+    self.assertRaises(errors.ParameterError, netutils.FormatAddress,
+                      "127.0.0.1", family=socket.AF_INET)
+    self.assertRaises(errors.ParameterError, netutils.FormatAddress,
+                      ("::1"), family=socket.AF_INET )
 
 
 if __name__ == "__main__":
index acbd1ab..ce09d8b 100755 (executable)
@@ -56,7 +56,18 @@ class FakeHttpPool:
       self._response_fn(req)
 
 
+def GetFakeSimpleStoreClass(fn):
+  class FakeSimpleStore:
+    GetNodePrimaryIPList = fn
+    GetPrimaryIPFamily = lambda _: None
+
+  return FakeSimpleStore
+
+
 class TestClient(unittest.TestCase):
+  def _FakeAddressLookup(self, map):
+    return lambda node_list: [map.get(node) for node in node_list]
+
   def _GetVersionResponse(self, req):
     self.assertEqual(req.host, "localhost")
     self.assertEqual(req.port, 24094)
@@ -66,7 +77,8 @@ class TestClient(unittest.TestCase):
     req.resp_body = serializer.DumpJson((True, 123))
 
   def testVersionSuccess(self):
-    client = rpc.Client("version", None, 24094)
+    fn = self._FakeAddressLookup({"localhost": "localhost"})
+    client = rpc.Client("version", None, 24094, address_lookup_fn=fn)
     client.ConnectNode("localhost")
     pool = FakeHttpPool(self._GetVersionResponse)
     result = client.GetResults(http_pool=pool)
@@ -90,7 +102,8 @@ class TestClient(unittest.TestCase):
 
   def testMultiVersionSuccess(self):
     nodes = ["node%s" % i for i in range(50)]
-    client = rpc.Client("version", None, 23245)
+    fn = self._FakeAddressLookup(dict(zip(nodes, nodes)))
+    client = rpc.Client("version", None, 23245, address_lookup_fn=fn)
     client.ConnectList(nodes)
 
     pool = FakeHttpPool(self._GetMultiVersionResponse)
@@ -115,7 +128,9 @@ class TestClient(unittest.TestCase):
     req.resp_body = serializer.DumpJson((False, "Unknown error"))
 
   def testVersionFailure(self):
-    client = rpc.Client("version", None, 5903)
+    lookup_map = {"aef9ur4i.example.com": "aef9ur4i.example.com"}
+    fn = self._FakeAddressLookup(lookup_map)
+    client = rpc.Client("version", None, 5903, address_lookup_fn=fn)
     client.ConnectNode("aef9ur4i.example.com")
     pool = FakeHttpPool(self._GetVersionResponseFail)
     result = client.GetResults(http_pool=pool)
@@ -152,6 +167,7 @@ class TestClient(unittest.TestCase):
 
   def testHttpError(self):
     nodes = ["uaf6pbbv%s" % i for i in range(50)]
+    fn = self._FakeAddressLookup(dict(zip(nodes, nodes)))
 
     httperrnodes = set(nodes[1::7])
     self.assertEqual(len(httperrnodes), 7)
@@ -161,7 +177,7 @@ class TestClient(unittest.TestCase):
 
     self.assertEqual(len(set(nodes) - failnodes - httperrnodes), 29)
 
-    client = rpc.Client("vg_list", None, 15165)
+    client = rpc.Client("vg_list", None, 15165, address_lookup_fn=fn)
     client.ConnectList(nodes)
 
     pool = FakeHttpPool(compat.partial(self._GetHttpErrorResponse,
@@ -203,7 +219,9 @@ class TestClient(unittest.TestCase):
     req.resp_body = serializer.DumpJson("invalid response")
 
   def testInvalidResponse(self):
-    client = rpc.Client("version", None, 19978)
+    lookup_map = {"oqo7lanhly.example.com": "oqo7lanhly.example.com"}
+    fn = self._FakeAddressLookup(lookup_map)
+    client = rpc.Client("version", None, 19978, address_lookup_fn=fn)
     for fn in [self._GetInvalidResponseA, self._GetInvalidResponseB]:
       client.ConnectNode("oqo7lanhly.example.com")
       pool = FakeHttpPool(fn)
@@ -218,6 +236,42 @@ class TestClient(unittest.TestCase):
       self.assertRaises(errors.OpExecError, lhresp.Raise, "failed")
       self.assertEqual(pool.reqcount, 1)
 
+  def testAddressLookupSimpleStore(self):
+    addr_list = ["192.0.2.%d" % n for n in range(0, 255, 13)]
+    node_list = ["node%d.example.com" % n for n in range(0, 255, 13)]
+    node_addr_list = [ " ".join(t) for t in zip(node_list, addr_list)]
+    ssc = GetFakeSimpleStoreClass(lambda _: node_addr_list)
+    result = rpc._AddressLookup(node_list, ssc=ssc)
+    self.assertEqual(result, addr_list)
+
+  def testAddressLookupNSLookup(self):
+    addr_list = ["192.0.2.%d" % n for n in range(0, 255, 13)]
+    node_list = ["node%d.example.com" % n for n in range(0, 255, 13)]
+    ssc = GetFakeSimpleStoreClass(lambda _: [])
+    node_addr_map = dict(zip(node_list, addr_list))
+    nslookup_fn = lambda name, family=None: node_addr_map.get(name)
+    result = rpc._AddressLookup(node_list, ssc=ssc, nslookup_fn=nslookup_fn)
+    self.assertEqual(result, addr_list)
+
+  def testAddressLookupBoth(self):
+    addr_list = ["192.0.2.%d" % n for n in range(0, 255, 13)]
+    node_list = ["node%d.example.com" % n for n in range(0, 255, 13)]
+    n = len(addr_list) / 2
+    node_addr_list = [ " ".join(t) for t in zip(node_list[n:], addr_list[n:])]
+    ssc = GetFakeSimpleStoreClass(lambda _: node_addr_list)
+    node_addr_map = dict(zip(node_list[:n], addr_list[:n]))
+    nslookup_fn = lambda name, family=None: node_addr_map.get(name)
+    result = rpc._AddressLookup(node_list, ssc=ssc, nslookup_fn=nslookup_fn)
+    self.assertEqual(result, addr_list)
+
+  def testAddressLookupIPv6(self):
+    addr_list = ["2001:db8::%d" % n for n in range(0, 255, 13)]
+    node_list = ["node%d.example.com" % n for n in range(0, 255, 13)]
+    node_addr_list = [ " ".join(t) for t in zip(node_list, addr_list)]
+    ssc = GetFakeSimpleStoreClass(lambda _: node_addr_list)
+    result = rpc._AddressLookup(node_list, ssc=ssc)
+    self.assertEqual(result, addr_list)
+
 
 if __name__ == "__main__":
   testutils.GanetiTestProgram()
diff --git a/test/ganeti.runtime_unittest.py b/test/ganeti.runtime_unittest.py
new file mode 100755 (executable)
index 0000000..3ba9ac8
--- /dev/null
@@ -0,0 +1,109 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2010 Google Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+
+"""Script for testing ganeti.runtime"""
+
+from ganeti import constants
+from ganeti import errors
+from ganeti import runtime
+
+import testutils
+
+
+class _EntStub:
+  def __init__(self, uid=None, gid=None):
+    self.pw_uid = uid
+    self.gr_gid = gid
+
+
+def _StubGetpwnam(user):
+  users = {
+    constants.MASTERD_USER: _EntStub(uid=0),
+    constants.CONFD_USER: _EntStub(uid=1),
+    constants.RAPI_USER: _EntStub(uid=2),
+    constants.NODED_USER: _EntStub(uid=3),
+    }
+  return users[user]
+
+
+def _StubGetgrnam(group):
+  groups = {
+    constants.MASTERD_GROUP: _EntStub(gid=0),
+    constants.CONFD_GROUP: _EntStub(gid=1),
+    constants.RAPI_GROUP: _EntStub(gid=2),
+    constants.DAEMONS_GROUP: _EntStub(gid=3),
+    constants.ADMIN_GROUP: _EntStub(gid=4),
+    }
+  return groups[group]
+
+
+def _RaisingStubGetpwnam(user):
+  raise KeyError("user not found")
+
+
+def _RaisingStubGetgrnam(group):
+  raise KeyError("group not found")
+
+
+class ResolverStubRaising(object):
+  def __init__(self):
+    raise errors.ConfigurationError("No entries")
+
+
+class TestErrors(testutils.GanetiTestCase):
+  def testEverythingSuccessful(self):
+    resolver = runtime.GetentResolver(_getpwnam=_StubGetpwnam,
+                                      _getgrnam=_StubGetgrnam)
+
+    self.assertEqual(resolver.masterd_uid,
+                     _StubGetpwnam(constants.MASTERD_USER).pw_uid)
+    self.assertEqual(resolver.masterd_gid,
+                     _StubGetgrnam(constants.MASTERD_GROUP).gr_gid)
+    self.assertEqual(resolver.confd_uid,
+                     _StubGetpwnam(constants.CONFD_USER).pw_uid)
+    self.assertEqual(resolver.confd_gid,
+                     _StubGetgrnam(constants.CONFD_GROUP).gr_gid)
+    self.assertEqual(resolver.rapi_uid,
+                     _StubGetpwnam(constants.RAPI_USER).pw_uid)
+    self.assertEqual(resolver.rapi_gid,
+                     _StubGetgrnam(constants.RAPI_GROUP).gr_gid)
+    self.assertEqual(resolver.noded_uid,
+                     _StubGetpwnam(constants.NODED_USER).pw_uid)
+
+    self.assertEqual(resolver.daemons_gid,
+                     _StubGetgrnam(constants.DAEMONS_GROUP).gr_gid)
+    self.assertEqual(resolver.admin_gid,
+                     _StubGetgrnam(constants.ADMIN_GROUP).gr_gid)
+
+  def testUserNotFound(self):
+    self.assertRaises(errors.ConfigurationError, runtime.GetentResolver,
+                      _getpwnam=_RaisingStubGetpwnam, _getgrnam=_StubGetgrnam)
+
+  def testGroupNotFound(self):
+    self.assertRaises(errors.ConfigurationError, runtime.GetentResolver,
+                      _getpwnam=_StubGetpwnam, _getgrnam=_RaisingStubGetgrnam)
+
+  def testUserNotFoundGetEnts(self):
+    self.assertRaises(errors.ConfigurationError, runtime.GetEnts,
+                      resolver=ResolverStubRaising)
+
+
+if __name__ == "__main__":
+  testutils.GanetiTestProgram()
index 5a70af2..2c46afc 100755 (executable)
@@ -177,13 +177,15 @@ class TestPidFileFunctions(unittest.TestCase):
 
   def testPidFileFunctions(self):
     pid_file = self.f_dpn('test')
-    utils.WritePidFile('test')
+    fd = utils.WritePidFile(self.f_dpn('test'))
     self.failUnless(os.path.exists(pid_file),
                     "PID file should have been created")
     read_pid = utils.ReadPidFile(pid_file)
     self.failUnlessEqual(read_pid, os.getpid())
     self.failUnless(utils.IsProcessAlive(read_pid))
-    self.failUnlessRaises(errors.GenericError, utils.WritePidFile, 'test')
+    self.failUnlessRaises(errors.LockError, utils.WritePidFile,
+                          self.f_dpn('test'))
+    os.close(fd)
     utils.RemovePidFile('test')
     self.failIf(os.path.exists(pid_file),
                 "PID file should not exist anymore")
@@ -194,6 +196,9 @@ class TestPidFileFunctions(unittest.TestCase):
     fh.close()
     self.failUnlessEqual(utils.ReadPidFile(pid_file), 0,
                          "ReadPidFile should return 0 for invalid pid file")
+    # but now, even with the file existing, we should be able to lock it
+    fd = utils.WritePidFile(self.f_dpn('test'))
+    os.close(fd)
     utils.RemovePidFile('test')
     self.failIf(os.path.exists(pid_file),
                 "PID file should not exist anymore")
@@ -203,7 +208,7 @@ class TestPidFileFunctions(unittest.TestCase):
     r_fd, w_fd = os.pipe()
     new_pid = os.fork()
     if new_pid == 0: #child
-      utils.WritePidFile('child')
+      utils.WritePidFile(self.f_dpn('child'))
       os.write(w_fd, 'a')
       signal.pause()
       os._exit(0)
@@ -1244,11 +1249,8 @@ class TestListVisibleFiles(unittest.TestCase):
 class TestNewUUID(unittest.TestCase):
   """Test case for NewUUID"""
 
-  _re_uuid = re.compile('^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-'
-                        '[a-f0-9]{4}-[a-f0-9]{12}$')
-
   def runTest(self):
-    self.failUnless(self._re_uuid.match(utils.NewUUID()))
+    self.failUnless(utils.UUID_RE.match(utils.NewUUID()))
 
 
 class TestUniqueSequence(unittest.TestCase):
@@ -2324,5 +2326,69 @@ class TestCommaJoin(unittest.TestCase):
                      "Hello, World, 99")
 
 
+class TestFindMatch(unittest.TestCase):
+  def test(self):
+    data = {
+      "aaaa": "Four A",
+      "bb": {"Two B": True},
+      re.compile(r"^x(foo|bar|bazX)([0-9]+)$"): (1, 2, 3),
+      }
+
+    self.assertEqual(utils.FindMatch(data, "aaaa"), ("Four A", []))
+    self.assertEqual(utils.FindMatch(data, "bb"), ({"Two B": True}, []))
+
+    for i in ["foo", "bar", "bazX"]:
+      for j in range(1, 100, 7):
+        self.assertEqual(utils.FindMatch(data, "x%s%s" % (i, j)),
+                         ((1, 2, 3), [i, str(j)]))
+
+  def testNoMatch(self):
+    self.assert_(utils.FindMatch({}, "") is None)
+    self.assert_(utils.FindMatch({}, "foo") is None)
+    self.assert_(utils.FindMatch({}, 1234) is None)
+
+    data = {
+      "X": "Hello World",
+      re.compile("^(something)$"): "Hello World",
+      }
+
+    self.assert_(utils.FindMatch(data, "") is None)
+    self.assert_(utils.FindMatch(data, "Hello World") is None)
+
+
+class TestFileID(testutils.GanetiTestCase):
+  def testEquality(self):
+    name = self._CreateTempFile()
+    oldi = utils.GetFileID(path=name)
+    self.failUnless(utils.VerifyFileID(oldi, oldi))
+
+  def testUpdate(self):
+    name = self._CreateTempFile()
+    oldi = utils.GetFileID(path=name)
+    os.utime(name, None)
+    fd = os.open(name, os.O_RDWR)
+    try:
+      newi = utils.GetFileID(fd=fd)
+      self.failUnless(utils.VerifyFileID(oldi, newi))
+      self.failUnless(utils.VerifyFileID(newi, oldi))
+    finally:
+      os.close(fd)
+
+  def testWriteFile(self):
+    name = self._CreateTempFile()
+    oldi = utils.GetFileID(path=name)
+    mtime = oldi[2]
+    os.utime(name, (mtime + 10, mtime + 10))
+    self.assertRaises(errors.LockError, utils.SafeWriteFile, name,
+                      oldi, data="")
+    os.utime(name, (mtime - 10, mtime - 10))
+    utils.SafeWriteFile(name, oldi, data="")
+    oldi = utils.GetFileID(path=name)
+    mtime = oldi[2]
+    os.utime(name, (mtime + 10, mtime + 10))
+    # this doesn't raise, since we passed None
+    utils.SafeWriteFile(name, None, data="")
+
+
 if __name__ == '__main__':
   testutils.GanetiTestProgram()
index 549edba..89b3b1a 100755 (executable)
@@ -1,7 +1,7 @@
 #!/usr/bin/python
 #
 
-# Copyright (C) 2008 Google Inc.
+# Copyright (C) 2008, 2009, 2010 Google Inc.
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -26,13 +26,15 @@ import threading
 import time
 import sys
 import zlib
+import random
 
 from ganeti import workerpool
+from ganeti import errors
 
 import testutils
 
-class CountingContext(object):
 
+class CountingContext(object):
   def __init__(self):
     self._lock = threading.Condition(threading.Lock())
     self.done = 0
@@ -57,7 +59,6 @@ class CountingContext(object):
 
 
 class CountingBaseWorker(workerpool.BaseWorker):
-
   def RunTask(self, ctx, text):
     ctx.DoneTask()
 
@@ -90,6 +91,46 @@ class ChecksumBaseWorker(workerpool.BaseWorker):
       ctx.lock.release()
 
 
+class ListBuilderContext:
+  def __init__(self):
+    self.lock = threading.Lock()
+    self.result = []
+    self.prioresult = {}
+
+
+class ListBuilderWorker(workerpool.BaseWorker):
+  def RunTask(self, ctx, data):
+    ctx.lock.acquire()
+    try:
+      ctx.result.append((self.GetCurrentPriority(), data))
+      ctx.prioresult.setdefault(self.GetCurrentPriority(), []).append(data)
+    finally:
+      ctx.lock.release()
+
+
+class DeferringTaskContext:
+  def __init__(self):
+    self.lock = threading.Lock()
+    self.prioresult = {}
+    self.samepriodefer = {}
+
+
+class DeferringWorker(workerpool.BaseWorker):
+  def RunTask(self, ctx, num, targetprio):
+    ctx.lock.acquire()
+    try:
+      if num in ctx.samepriodefer:
+        del ctx.samepriodefer[num]
+        raise workerpool.DeferTask()
+
+      if self.GetCurrentPriority() > targetprio:
+        raise workerpool.DeferTask(priority=self.GetCurrentPriority() - 1)
+
+      ctx.prioresult.setdefault(self.GetCurrentPriority(), set()).add(num)
+    finally:
+      ctx.lock.release()
+
+
 class TestWorkerpool(unittest.TestCase):
   """Workerpool tests"""
 
@@ -213,6 +254,220 @@ class TestWorkerpool(unittest.TestCase):
     finally:
       wp._lock.release()
 
+  def testPriorityChecksum(self):
+    # Tests whether all tasks are run and, since we're only using a single
+    # thread, whether everything is started in order and respects the priority
+    wp = workerpool.WorkerPool("Test", 1, ChecksumBaseWorker)
+    try:
+      self._CheckWorkerCount(wp, 1)
+
+      ctx = ChecksumContext()
+
+      data = {}
+      tasks = []
+      priorities = []
+      for i in range(1, 333):
+        prio = i % 7
+        tasks.append((ctx, i))
+        priorities.append(prio)
+        data.setdefault(prio, []).append(i)
+
+      wp.AddManyTasks(tasks, priority=priorities)
+
+      wp.Quiesce()
+
+      self._CheckNoTasks(wp)
+
+      # Check sum
+      ctx.lock.acquire()
+      try:
+        checksum = ChecksumContext.CHECKSUM_START
+        for priority in sorted(data.keys()):
+          for i in data[priority]:
+            checksum = ChecksumContext.UpdateChecksum(checksum, i)
+
+        self.assertEqual(checksum, ctx.checksum)
+      finally:
+        ctx.lock.release()
+
+      self._CheckWorkerCount(wp, 1)
+    finally:
+      wp.TerminateWorkers()
+      self._CheckWorkerCount(wp, 0)
+
+  def testPriorityListManyTasks(self):
+    # Tests whether all tasks are run and, since we're only using a single
+    # thread, whether everything is started in order and respects the priority
+    wp = workerpool.WorkerPool("Test", 1, ListBuilderWorker)
+    try:
+      self._CheckWorkerCount(wp, 1)
+
+      ctx = ListBuilderContext()
+
+      # Use static seed for this test
+      rnd = random.Random(0)
+
+      data = {}
+      tasks = []
+      priorities = []
+      for i in range(1, 333):
+        prio = int(rnd.random() * 10)
+        tasks.append((ctx, i))
+        priorities.append(prio)
+        data.setdefault(prio, []).append((prio, i))
+
+      wp.AddManyTasks(tasks, priority=priorities)
+
+      self.assertRaises(errors.ProgrammerError, wp.AddManyTasks,
+                        [("x", ), ("y", )], priority=[1] * 5)
+
+      wp.Quiesce()
+
+      self._CheckNoTasks(wp)
+
+      # Check result
+      ctx.lock.acquire()
+      try:
+        expresult = []
+        for priority in sorted(data.keys()):
+          expresult.extend(data[priority])
+
+        self.assertEqual(expresult, ctx.result)
+      finally:
+        ctx.lock.release()
+
+      self._CheckWorkerCount(wp, 1)
+    finally:
+      wp.TerminateWorkers()
+      self._CheckWorkerCount(wp, 0)
+
+  def testPriorityListSingleTasks(self):
+    # Tests whether all tasks are run and, since we're only using a single
+    # thread, whether everything is started in order and respects the priority
+    wp = workerpool.WorkerPool("Test", 1, ListBuilderWorker)
+    try:
+      self._CheckWorkerCount(wp, 1)
+
+      ctx = ListBuilderContext()
+
+      # Use static seed for this test
+      rnd = random.Random(26279)
+
+      data = {}
+      for i in range(1, 333):
+        prio = int(rnd.random() * 30)
+        wp.AddTask((ctx, i), priority=prio)
+        data.setdefault(prio, []).append(i)
+
+        # Cause some distortion
+        if i % 11 == 0:
+          time.sleep(.001)
+        if i % 41 == 0:
+          wp.Quiesce()
+
+      wp.Quiesce()
+
+      self._CheckNoTasks(wp)
+
+      # Check result
+      ctx.lock.acquire()
+      try:
+        self.assertEqual(data, ctx.prioresult)
+      finally:
+        ctx.lock.release()
+
+      self._CheckWorkerCount(wp, 1)
+    finally:
+      wp.TerminateWorkers()
+      self._CheckWorkerCount(wp, 0)
+
+  def testPriorityListSingleTasks(self):
+    # Tests whether all tasks are run and, since we're only using a single
+    # thread, whether everything is started in order and respects the priority
+    wp = workerpool.WorkerPool("Test", 1, ListBuilderWorker)
+    try:
+      self._CheckWorkerCount(wp, 1)
+
+      ctx = ListBuilderContext()
+
+      # Use static seed for this test
+      rnd = random.Random(26279)
+
+      data = {}
+      for i in range(1, 333):
+        prio = int(rnd.random() * 30)
+        wp.AddTask((ctx, i), priority=prio)
+        data.setdefault(prio, []).append(i)
+
+        # Cause some distortion
+        if i % 11 == 0:
+          time.sleep(.001)
+        if i % 41 == 0:
+          wp.Quiesce()
+
+      wp.Quiesce()
+
+      self._CheckNoTasks(wp)
+
+      # Check result
+      ctx.lock.acquire()
+      try:
+        self.assertEqual(data, ctx.prioresult)
+      finally:
+        ctx.lock.release()
+
+      self._CheckWorkerCount(wp, 1)
+    finally:
+      wp.TerminateWorkers()
+      self._CheckWorkerCount(wp, 0)
+
+  def testDeferTask(self):
+    # Tests whether all tasks are run and, since we're only using a single
+    # thread, whether everything is started in order and respects the priority
+    wp = workerpool.WorkerPool("Test", 1, DeferringWorker)
+    try:
+      self._CheckWorkerCount(wp, 1)
+
+      ctx = DeferringTaskContext()
+
+      # Use static seed for this test
+      rnd = random.Random(14921)
+
+      data = {}
+      for i in range(1, 333):
+        ctx.lock.acquire()
+        try:
+          if i % 5 == 0:
+            ctx.samepriodefer[i] = True
+        finally:
+          ctx.lock.release()
+
+        prio = int(rnd.random() * 30)
+        wp.AddTask((ctx, i, prio), priority=50)
+        data.setdefault(prio, set()).add(i)
+
+        # Cause some distortion
+        if i % 24 == 0:
+          time.sleep(.001)
+        if i % 31 == 0:
+          wp.Quiesce()
+
+      wp.Quiesce()
+
+      self._CheckNoTasks(wp)
+
+      # Check result
+      ctx.lock.acquire()
+      try:
+        self.assertEqual(data, ctx.prioresult)
+      finally:
+        ctx.lock.release()
+
+      self._CheckWorkerCount(wp, 1)
+    finally:
+      wp.TerminateWorkers()
+      self._CheckWorkerCount(wp, 0)
+
 
 if __name__ == '__main__':
   testutils.GanetiTestProgram()
index 1450ad1..a5140fb 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
-# Copyright (C) 2006, 2007 Google Inc.
+# Copyright (C) 2006, 2007, 2010 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
@@ -21,6 +21,9 @@
 
 """Module implementing a fake ConfigWriter"""
 
+
+import os
+
 from ganeti import utils
 from ganeti import netutils
 
@@ -50,7 +53,7 @@ class FakeConfig:
         return "test.cluster"
 
     def GetMasterNode(self):
-        return netutils.HostInfo().name
+        return netutils.Hostname.GetSysName()
 
     def GetDefaultIAllocator(Self):
         return "testallocator"
@@ -79,3 +82,24 @@ class FakeContext:
         self.cfg = FakeConfig()
         # TODO: decide what features a mock Ganeti Lock Manager must have
         self.GLM = None
+
+
+class FakeGetentResolver:
+    """Fake runtime.GetentResolver"""
+
+    def __init__(self):
+        # As we nomally don't run under root we use our own uid/gid for all
+        # fields. This way we don't run into permission denied problems.
+        uid = os.getuid()
+        gid = os.getgid()
+
+        self.masterd_uid = uid
+        self.masterd_gid = gid
+        self.confd_uid = uid
+        self.confd_gid = gid
+        self.rapi_uid = uid
+        self.rapi_gid = gid
+        self.noded_uid = uid
+
+        self.daemons_gid = gid
+        self.admin_gid = gid
index fd4655a..3c46a0c 100644 (file)
@@ -35,12 +35,32 @@ def GetSourceDir():
   return os.environ.get("TOP_SRCDIR", ".")
 
 
+def _SetupLogging(verbose):
+  """Setupup logging infrastructure.
+
+  """
+  fmt = logging.Formatter("%(asctime)s: %(threadName)s"
+                          " %(levelname)s %(message)s")
+
+  if verbose:
+    handler = logging.StreamHandler()
+  else:
+    handler = logging.FileHandler(os.devnull, "a")
+
+  handler.setLevel(logging.NOTSET)
+  handler.setFormatter(fmt)
+
+  root_logger = logging.getLogger("")
+  root_logger.setLevel(logging.NOTSET)
+  root_logger.addHandler(handler)
+
+
 class GanetiTestProgram(unittest.TestProgram):
   def runTests(self):
-    """
+    """Runs all tests.
 
     """
-    logging.basicConfig(filename=os.devnull)
+    _SetupLogging("LOGTOSTDERR" in os.environ)
 
     sys.stderr.write("Running %s\n" % self.progName)
     sys.stderr.flush()
index 00dbb22..0b135ce 100755 (executable)
@@ -141,14 +141,14 @@ def main():
     raise Error("Inconsistent configuration: found config_version in"
                 " configuration file")
 
-  # Upgrade from 2.0/2.1 to 2.2
-  if config_major == 2 and config_minor in (0, 1):
+  # Upgrade from 2.0/2.1/2.2 to 2.3
+  if config_major == 2 and config_minor in (0, 1, 2):
     if config_revision != 0:
       logging.warning("Config revision is %s, not 0", config_revision)
 
-    config_data["version"] = constants.BuildVersion(2, 2, 0)
+    config_data["version"] = constants.BuildVersion(2, 3, 0)
 
-  elif config_major == 2 and config_minor == 2:
+  elif config_major == 2 and config_minor == 3:
     logging.info("No changes necessary")
 
   else:
index 4c8f9e9..112caf6 100755 (executable)
@@ -38,6 +38,7 @@ from ganeti import cli
 from ganeti import constants
 from ganeti import errors
 from ganeti import netutils
+from ganeti import ssconf
 from ganeti import ssh
 from ganeti import utils
 
@@ -48,6 +49,59 @@ class RemoteCommandError(errors.GenericError):
   """
 
 
+class JoinCheckError(errors.GenericError):
+  """Exception raised if join check fails.
+
+  """
+
+
+class HostKeyVerificationError(errors.GenericError):
+  """Exception if host key do not match.
+
+  """
+
+
+class AuthError(errors.GenericError):
+  """Exception for authentication errors to hosts.
+
+  """
+
+
+def _CheckJoin(transport):
+  """Checks if a join is safe or dangerous.
+
+  Note: This function relies on the fact, that all
+  hosts have the same configuration at compile time of
+  Ganeti. So that the constants do not mismatch.
+
+  @param transport: The paramiko transport instance
+  @return: True if the join is safe; False otherwise
+
+  """
+  sftp = transport.open_sftp_client()
+  ss = ssconf.SimpleStore()
+  ss_cluster_name_path = ss.KeyToFilename(constants.SS_CLUSTER_NAME)
+
+  cluster_files = [
+    (constants.NODED_CERT_FILE, utils.ReadFile(constants.NODED_CERT_FILE)),
+    (ss_cluster_name_path, utils.ReadFile(ss_cluster_name_path)),
+    ]
+
+  for (filename, local_content) in cluster_files:
+    try:
+      remote_content = _ReadSftpFile(sftp, filename)
+    except IOError, err:
+      # Assume file does not exist. Paramiko's error reporting is lacking.
+      logging.debug("Failed to read %s: %s", filename, err)
+      continue
+
+    if remote_content != local_content:
+      logging.error("File %s doesn't match local version", filename)
+      return False
+
+  return True
+
+
 def _RunRemoteCommand(transport, command):
   """Invokes and wait for the command over SSH.
 
@@ -84,6 +138,21 @@ def _InvokeDaemonUtil(transport, command):
   _RunRemoteCommand(transport, "%s %s" % (constants.DAEMON_UTIL, command))
 
 
+def _ReadSftpFile(sftp, filename):
+  """Reads a file over sftp.
+
+  @param sftp: An open paramiko SFTP client
+  @param filename: The filename of the file to read
+  @return: The content of the file
+
+  """
+  remote_file = sftp.open(filename, "r")
+  try:
+    return remote_file.read()
+  finally:
+    remote_file.close()
+
+
 def _WriteSftpFile(sftp, name, perm, data):
   """SFTPs data to a remote file.
 
@@ -126,11 +195,11 @@ def SetupSSH(transport):
 
   try:
     sftp.mkdir(auth_path, 0700)
-  except IOError:
+  except IOError, err:
     # Sadly paramiko doesn't provide errno or similiar
     # so we can just assume that the path already exists
-    logging.info("Path %s seems already to exist on remote node. Ignoring.",
-                 auth_path)
+    logging.info("Assuming directory %s on remote node exists: %s",
+                 auth_path, err)
 
   for name, (data, perm) in filemap.iteritems():
     _WriteSftpFile(sftp, name, perm, data)
@@ -153,28 +222,14 @@ def SetupSSH(transport):
   _InvokeDaemonUtil(transport, "reload-ssh-keys")
 
 
-def SetupNodeDaemon(transport):
-  """Sets the node daemon up on the other side.
-
-  @param transport: The paramiko transport instance
-
-  """
-  noded_cert = utils.ReadFile(constants.NODED_CERT_FILE)
-
-  sftp = transport.open_sftp_client()
-  _WriteSftpFile(sftp, constants.NODED_CERT_FILE, 0400, noded_cert)
-
-  _InvokeDaemonUtil(transport, "start %s" % constants.NODED)
-
-
 def ParseOptions():
   """Parses options passed to program.
 
   """
   program = os.path.basename(sys.argv[0])
 
-  parser = optparse.OptionParser(usage=("%prog [--debug|--verbose] <node>"
-                                        " <node...>"), prog=program)
+  parser = optparse.OptionParser(usage=("%prog [--debug|--verbose] [--force]"
+                                        " <node> <node...>"), prog=program)
   parser.add_option(cli.DEBUG_OPT)
   parser.add_option(cli.VERBOSE_OPT)
   parser.add_option(cli.NOSSH_KEYCHECK_OPT)
@@ -186,6 +241,9 @@ def ParseOptions():
   parser.add_option(optparse.Option("--key-type", dest="key_type",
                                     choices=("rsa", "dsa"), default="dsa",
                                     help="The private key type (rsa or dsa)"))
+  parser.add_option(optparse.Option("-j", "--force-join", dest="force_join",
+                                    action="store_true", default=False,
+                                    help="Force the join of the host"))
 
   (options, args) = parser.parse_args()
 
@@ -269,6 +327,16 @@ def LoadPrivateKeys(options):
   return [private_key] + list(agent_keys)
 
 
+def _FormatFingerprint(fpr):
+  """Formats a paramiko.PKey.get_fingerprint() human readable.
+
+  @param fpr: The fingerprint to be formatted
+  @return: A human readable fingerprint
+
+  """
+  return ssh.FormatParamikoFingerprint(paramiko.util.hexify(fpr))
+
+
 def LoginViaKeys(transport, username, keys):
   """Try to login on the given transport via a list of keys.
 
@@ -284,7 +352,7 @@ def LoginViaKeys(transport, username, keys):
   for private_key in keys:
     try:
       transport.auth_publickey(username, private_key)
-      fpr = ":".join("%02x" % ord(i) for i in private_key.get_fingerprint())
+      fpr = _FormatFingerprint(private_key.get_fingerprint())
       if isinstance(private_key, paramiko.AgentKey):
         logging.debug("Authentication via the ssh-agent key %s", fpr)
       else:
@@ -309,10 +377,36 @@ def LoadKnownHosts():
   try:
     return paramiko.util.load_host_keys(known_hosts)
   except EnvironmentError:
-    # We didn't found the path, silently ignore and return an empty dict
+    # We didn't find the path, silently ignore and return an empty dict
     return {}
 
 
+def _VerifyServerKey(transport, host, host_keys):
+  """Verify the server keys.
+
+  @param transport: A paramiko.transport instance
+  @param host: Name of the host we verify
+  @param host_keys: Loaded host keys
+  @raises HostkeyVerificationError: When the host identify couldn't be verified
+
+  """
+
+  server_key = transport.get_remote_server_key()
+  keytype = server_key.get_name()
+
+  our_server_key = host_keys.get(host, {}).get(keytype, None)
+  if not our_server_key:
+    hexified_key = _FormatFingerprint(server_key.get_fingerprint())
+    msg = ("Unable to verify hostkey of host %s: %s. Do you want to accept"
+           " it?" % (host, hexified_key))
+
+    if cli.AskUser(msg):
+      our_server_key = server_key
+
+  if our_server_key != server_key:
+    raise HostKeyVerificationError("Unable to verify host identity")
+
+
 def main():
   """Main routine.
 
@@ -321,8 +415,6 @@ def main():
 
   SetupLogging(options)
 
-  errs = 0
-
   all_keys = LoadPrivateKeys(options)
 
   passwd = None
@@ -339,65 +431,65 @@ def main():
   #   wants to log one more message, which fails as the file is closed
   #   now
 
+  success = True
+
   for host in args:
-    transport = paramiko.Transport((host, ssh_port))
-    transport.start_client()
-    server_key = transport.get_remote_server_key()
-    keytype = server_key.get_name()
-
-    our_server_key = host_keys.get(host, {}).get(keytype, None)
-    if options.ssh_key_check:
-      if not our_server_key:
-        hexified_key = ssh.FormatParamikoFingerprint(
-            paramiko.util.hexify(server_key.get_fingerprint()))
-        msg = ("Unable to verify hostkey of host %s: %s. Do you want to accept"
-               " it?" % (host, hexified_key))
-
-        if cli.AskUser(msg):
-          our_server_key = server_key
-
-      if our_server_key != server_key:
-        logging.error("Unable to verify identity of host. Aborting")
-        transport.close()
-        transport.join()
-        # TODO: Run over all hosts, fetch the keys and let them verify from the
-        #       user beforehand then proceed with actual work later on
-        raise paramiko.SSHException("Unable to verify identity of host")
+    logging.info("Configuring %s", host)
 
-    try:
-      if LoginViaKeys(transport, username, all_keys):
-        logging.info("Authenticated to %s via public key", host)
-      else:
-        logging.warning("Authentication to %s via public key failed, trying"
-                        " password", host)
-        if passwd is None:
-          passwd = getpass.getpass(prompt="%s password:" % username)
-        transport.auth_password(username=username, password=passwd)
-        logging.info("Authenticated to %s via password", host)
-    except paramiko.SSHException, err:
-      logging.error("Connection or authentication failed to host %s: %s",
-                    host, err)
-      transport.close()
-      # this is needed for compatibility with older Paramiko or Python
-      # versions
-      transport.join()
-      continue
+    transport = paramiko.Transport((host, ssh_port))
     try:
       try:
+        transport.start_client()
+
+        if options.ssh_key_check:
+          _VerifyServerKey(transport, host, host_keys)
+
+        try:
+          if LoginViaKeys(transport, username, all_keys):
+            logging.info("Authenticated to %s via public key", host)
+          else:
+            if all_keys:
+              logging.warning("Authentication to %s via public key failed,"
+                              " trying password", host)
+            if passwd is None:
+              passwd = getpass.getpass(prompt="%s password:" % username)
+            transport.auth_password(username=username, password=passwd)
+            logging.info("Authenticated to %s via password", host)
+        except paramiko.SSHException, err:
+          raise AuthError("Auth error TODO" % err)
+
+        if not _CheckJoin(transport):
+          if not options.force_join:
+            raise JoinCheckError(("Host %s failed join check; Please verify"
+                                  " that the host was not previously joined"
+                                  " to another cluster and use --force-join"
+                                  " to continue") % host)
+
+          logging.warning("Host %s failed join check, forced to continue",
+                          host)
+
         SetupSSH(transport)
-        SetupNodeDaemon(transport)
-      except errors.GenericError, err:
-        logging.error("While doing setup on host %s an error occurred: %s",
-                      host, err)
-        errs += 1
-    finally:
-      transport.close()
-      # this is needed for compatibility with older Paramiko or Python
-      # versions
-      transport.join()
-
-    if errs > 0:
-      sys.exit(1)
+        logging.info("%s successfully configured", host)
+      finally:
+        transport.close()
+        # this is needed for compatibility with older Paramiko or Python
+        # versions
+        transport.join()
+    except AuthError, err:
+      logging.error("Authentication error: %s", err)
+      success = False
+      break
+    except HostKeyVerificationError, err:
+      logging.error("Host key verification error: %s", err)
+      success = False
+    except Exception, err:
+      logging.exception("During setup of %s: %s", host, err)
+      success = False
+
+  if success:
+    sys.exit(constants.EXIT_SUCCESS)
+
+  sys.exit(constants.EXIT_FAILURE)
 
 
 if __name__ == "__main__":