Initial commit.
authorIustin Pop <iustin@google.com>
Mon, 16 Jul 2007 13:39:48 +0000 (13:39 +0000)
committerIustin Pop <iustin@google.com>
Mon, 16 Jul 2007 13:39:48 +0000 (13:39 +0000)
54 files changed:
COPYING [new file with mode: 0644]
INSTALL [new file with mode: 0644]
Makefile.am [new file with mode: 0644]
README [new file with mode: 0644]
configure.ac [new file with mode: 0644]
daemons/Makefile.am [new file with mode: 0644]
daemons/ganeti-noded [new file with mode: 0755]
daemons/ganeti-watcher [new file with mode: 0755]
docs/Makefile.am [new file with mode: 0644]
docs/hooks.sgml [new file with mode: 0644]
ganeti.initd [new file with mode: 0755]
lib/Makefile.am [new file with mode: 0644]
lib/__init__.py [new file with mode: 0644]
lib/backend.py [new file with mode: 0644]
lib/bdev.py [new file with mode: 0644]
lib/cli.py [new file with mode: 0644]
lib/cmdlib.py [new file with mode: 0644]
lib/config.py [new file with mode: 0644]
lib/constants.py [new file with mode: 0644]
lib/errors.py [new file with mode: 0644]
lib/hypervisor.py [new file with mode: 0644]
lib/logger.py [new file with mode: 0644]
lib/mcpu.py [new file with mode: 0644]
lib/objects.py [new file with mode: 0644]
lib/opcodes.py [new file with mode: 0644]
lib/rpc.py [new file with mode: 0644]
lib/ssconf.py [new file with mode: 0644]
lib/ssh.py [new file with mode: 0644]
lib/utils.py [new file with mode: 0644]
man/Makefile.am [new file with mode: 0644]
man/footer.sgml [new file with mode: 0644]
man/ganeti-noded.sgml [new file with mode: 0644]
man/ganeti-os-interface.sgml [new file with mode: 0644]
man/ganeti-watcher.sgml [new file with mode: 0644]
man/ganeti.sgml [new file with mode: 0644]
man/gnt-cluster.sgml [new file with mode: 0644]
man/gnt-instance.sgml [new file with mode: 0644]
man/gnt-node.sgml [new file with mode: 0644]
man/gnt-os.sgml [new file with mode: 0644]
scripts/Makefile.am [new file with mode: 0644]
scripts/gnt-cluster [new file with mode: 0755]
scripts/gnt-instance [new file with mode: 0755]
scripts/gnt-node [new file with mode: 0755]
scripts/gnt-os [new file with mode: 0755]
testing/Makefile.am [new file with mode: 0644]
testing/fake_config.py [new file with mode: 0644]
testing/ganeti.hooks_unittest.py [new file with mode: 0755]
testing/ganeti.qa.py [new file with mode: 0755]
testing/ganeti.utils_unittest.py [new file with mode: 0755]
testing/qa-sample.yaml [new file with mode: 0644]
tools/Makefile.am [new file with mode: 0644]
tools/burnin [new file with mode: 0755]
tools/cfgshell [new file with mode: 0755]
tools/lvmstrap [new file with mode: 0755]

diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..623b625
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,340 @@
+                   GNU GENERAL PUBLIC LICENSE
+                      Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+     51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                           Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+\f
+                   GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+\f
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+\f
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+\f
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                           NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                    END OF TERMS AND CONDITIONS
+\f
+           How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    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
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year  name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/INSTALL b/INSTALL
new file mode 100644 (file)
index 0000000..a2b85e6
--- /dev/null
+++ b/INSTALL
@@ -0,0 +1,28 @@
+Installation of the software
+============================
+
+Before installing, please verify that you have the following programs:
+  - lvm 2
+  - ssh
+  - fping
+  - python twisted library (the core is enough)
+  - python openssl bindings
+
+To install, simply do ./configure && make && make install
+
+This will install the software under /usr/local. You then need to copy
+ganeti.init to /etc/init.d and integrate it into your boot sequence
+(``chkconfig``, ``update-rc.d``, etc.).
+
+Cluster initialisation
+======================
+
+Before initialising the cluster, on each node you need to create the following
+directories:
+
+  - /etc/ganeti
+  - /var/log/ganeti
+  - /var/lib/ganeti
+  - /srv/ganeti and /srv/ganeti/os
+
+After this, use ``gnt-cluster init``.
diff --git a/Makefile.am b/Makefile.am
new file mode 100644 (file)
index 0000000..754e142
--- /dev/null
@@ -0,0 +1,16 @@
+# standard automake rules
+
+SUBDIRS = man lib scripts daemons docs testing tools
+EXTRA_DIST = ganeti.initd
+
+# custom rules
+depgraph: depgraph.png
+
+depgraph.png: depgraph.dot
+       dot -Tpng -o $@ $<
+
+depgraph.ps: depgraph.dot
+       dot -Tps -o $@ $<
+
+depgraph.dot: ganeti/*.py
+       pylint.python2.4 --indent-string '  ' --rcfile=/dev/null --reports y --int-import-graph $@ --persistent n ganeti >/dev/null
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..c3bba3d
--- /dev/null
+++ b/README
@@ -0,0 +1,7 @@
+Ganeti 1.2
+==========
+
+For installation instructions, read the INSTALL file.
+
+For a brief introduction, read the ganeti(7) manpage and the other pages
+it suggests.
diff --git a/configure.ac b/configure.ac
new file mode 100644 (file)
index 0000000..eb82373
--- /dev/null
@@ -0,0 +1,25 @@
+#                                               -*- Autoconf -*-
+# Process this file with autoconf to produce a configure script.
+
+AC_PREREQ(2.59)
+AC_INIT(ganeti, 1.2a, ganeti@googlegroups.com)
+AM_INIT_AUTOMAKE(foreign)
+
+# Checks for programs.
+AC_PROG_INSTALL
+
+# Checks for python
+AM_PATH_PYTHON(2.4)
+
+# Checks for libraries.
+
+# Checks for header files.
+
+# Checks for typedefs, structures, and compiler characteristics.
+
+# Checks for library functions.
+
+AC_CONFIG_FILES([Makefile man/Makefile docs/Makefile 
+               testing/Makefile tools/Makefile
+               lib/Makefile scripts/Makefile daemons/Makefile])
+AC_OUTPUT
diff --git a/daemons/Makefile.am b/daemons/Makefile.am
new file mode 100644 (file)
index 0000000..82baa7d
--- /dev/null
@@ -0,0 +1 @@
+dist_sbin_SCRIPTS = ganeti-noded ganeti-watcher
diff --git a/daemons/ganeti-noded b/daemons/ganeti-noded
new file mode 100755 (executable)
index 0000000..de1f438
--- /dev/null
@@ -0,0 +1,401 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2006, 2007 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.
+
+
+"""Ganeti node daemon"""
+
+import os
+import sys
+import resource
+import traceback
+
+from optparse import OptionParser
+
+
+from ganeti import backend
+from ganeti import logger
+from ganeti import constants
+from ganeti import objects
+from ganeti import errors
+from ganeti import ssconf
+
+from twisted.spread import pb
+from twisted.internet import reactor
+from twisted.cred import checkers, portal
+from OpenSSL import SSL
+
+
+class ServerContextFactory:
+  def getContext(self):
+    ctx = SSL.Context(SSL.TLSv1_METHOD)
+    ctx.use_certificate_file(constants.SSL_CERT_FILE)
+    ctx.use_privatekey_file(constants.SSL_CERT_FILE)
+    return ctx
+
+class ServerObject(pb.Avatar):
+  def __init__(self, name):
+    self.name = name
+
+  def perspectiveMessageReceived(self, broker, message, args, kw):
+    """This method is called when a network message is received.
+
+    I will call::
+
+      |  self.perspective_%(message)s(*broker.unserialize(args),
+      |                               **broker.unserialize(kw))
+
+    to handle the method; subclasses of Avatar are expected to
+    implement methods of this naming convention.
+    """
+
+    args = broker.unserialize(args, self)
+    kw = broker.unserialize(kw, self)
+    method = getattr(self, "perspective_%s" % message)
+    tb = None
+    state = None
+    try:
+      state = method(*args, **kw)
+    except:
+      tb = traceback.format_exc()
+
+    return broker.serialize((tb, state), self, method, args, kw)
+
+  # the new block devices  --------------------------
+
+  def perspective_blockdev_create(self,params):
+    bdev_s, size, on_primary = params
+    bdev = objects.ConfigObject.Loads(bdev_s)
+    if bdev is None:
+      raise ValueError("can't unserialize data!")
+    return backend.CreateBlockDevice(bdev, size, on_primary)
+
+
+  def perspective_blockdev_remove(self,params):
+    bdev_s = params[0]
+    bdev = objects.ConfigObject.Loads(bdev_s)
+    return backend.RemoveBlockDevice(bdev)
+
+
+  def perspective_blockdev_assemble(self,params):
+    bdev_s, on_primary = params
+    bdev = objects.ConfigObject.Loads(bdev_s)
+    if bdev is None:
+      raise ValueError("can't unserialize data!")
+    return backend.AssembleBlockDevice(bdev, on_primary)
+
+
+  def perspective_blockdev_shutdown(self,params):
+    bdev_s = params[0]
+    bdev = objects.ConfigObject.Loads(bdev_s)
+    if bdev is None:
+      raise ValueError("can't unserialize data!")
+    return backend.ShutdownBlockDevice(bdev)
+
+
+  def perspective_blockdev_addchild(self,params):
+    bdev_s, ndev_s = params
+    bdev = objects.ConfigObject.Loads(bdev_s)
+    ndev = objects.ConfigObject.Loads(ndev_s)
+    if bdev is None or ndev is None:
+      raise ValueError("can't unserialize data!")
+    return backend.MirrorAddChild(bdev, ndev)
+
+
+  def perspective_blockdev_removechild(self,params):
+    bdev_s, ndev_s = params
+    bdev = objects.ConfigObject.Loads(bdev_s)
+    ndev = objects.ConfigObject.Loads(ndev_s)
+    if bdev is None or ndev is None:
+      raise ValueError("can't unserialize data!")
+    return backend.MirrorRemoveChild(bdev, ndev)
+
+  def perspective_blockdev_getmirrorstatus(self, params):
+    disks = [objects.ConfigObject.Loads(dsk_s)
+            for dsk_s in params]
+    return backend.GetMirrorStatus(disks)
+
+  def perspective_blockdev_find(self, params):
+    disk = objects.ConfigObject.Loads(params[0])
+    return backend.FindBlockDevice(disk)
+
+  def perspective_blockdev_snapshot(self,params):
+    cfbd = objects.ConfigObject.Loads(params[0])
+    return backend.SnapshotBlockDevice(cfbd)
+
+  # export/import  --------------------------
+
+  def perspective_snapshot_export(self,params):
+    disk = objects.ConfigObject.Loads(params[0])
+    dest_node = params[1]
+    instance = objects.ConfigObject.Loads(params[2])
+    return backend.ExportSnapshot(disk,dest_node,instance)
+
+  def perspective_finalize_export(self,params):
+    instance = objects.ConfigObject.Loads(params[0])
+    snap_disks = [objects.ConfigObject.Loads(str_data)
+                  for str_data in params[1]]
+    return backend.FinalizeExport(instance, snap_disks)
+
+  def perspective_export_info(self,params):
+    dir = params[0]
+    einfo = backend.ExportInfo(dir)
+    if einfo is None:
+      return einfo
+    return einfo.Dumps()
+
+  def perspective_export_list(self, params):
+    return backend.ListExports()
+
+  def perspective_export_remove(self, params):
+    export = params[0]
+    return backend.RemoveExport(export)
+
+  # volume  --------------------------
+
+  def perspective_volume_list(self,params):
+    vgname = params[0]
+    return backend.GetVolumeList(vgname)
+
+  def perspective_vg_list(self,params):
+    return backend.ListVolumeGroups()
+
+  # bridge  --------------------------
+
+  def perspective_bridges_exist(self,params):
+    bridges_list = params[0]
+    return backend.BridgesExist(bridges_list)
+
+  # instance  --------------------------
+
+  def perspective_instance_os_add(self,params):
+    inst_s, os_disk, swap_disk = params
+    inst = objects.ConfigObject.Loads(inst_s)
+    return backend.AddOSToInstance(inst, os_disk, swap_disk)
+
+  def perspective_instance_os_import(self, params):
+    inst_s, os_disk, swap_disk, src_node, src_image = params
+    inst = objects.ConfigObject.Loads(inst_s)
+    return backend.ImportOSIntoInstance(inst, os_disk, swap_disk,
+                                        src_node, src_image)
+
+  def perspective_instance_shutdown(self,params):
+    instance = objects.ConfigObject.Loads(params[0])
+    return backend.ShutdownInstance(instance)
+
+  def perspective_instance_start(self,params):
+    instance = objects.ConfigObject.Loads(params[0])
+    extra_args = params[1]
+    return backend.StartInstance(instance, extra_args)
+
+  def perspective_instance_info(self,params):
+    return backend.GetInstanceInfo(params[0])
+
+  def perspective_all_instances_info(self,params):
+    return backend.GetAllInstancesInfo()
+
+  def perspective_instance_list(self,params):
+    return backend.GetInstanceList()
+
+  # node --------------------------
+
+  def perspective_node_info(self,params):
+    vgname = params[0]
+    return backend.GetNodeInfo(vgname)
+
+  def perspective_node_add(self,params):
+    return backend.AddNode(params[0], params[1], params[2],
+                           params[3], params[4], params[5])
+
+  def perspective_node_verify(self,params):
+    return backend.VerifyNode(params[0])
+
+  def perspective_node_start_master(self, params):
+    return backend.StartMaster()
+
+  def perspective_node_stop_master(self, params):
+    return backend.StopMaster()
+
+  def perspective_node_leave_cluster(self, params):
+    return backend.LeaveCluster()
+
+  # cluster --------------------------
+
+  def perspective_version(self,params):
+    return constants.PROTOCOL_VERSION
+
+  def perspective_configfile_list(self,params):
+    return backend.ListConfigFiles()
+
+  def perspective_upload_file(self,params):
+    return backend.UploadFile(*params)
+
+
+  # os -----------------------
+
+  def perspective_os_diagnose(self, params):
+    os_list = backend.DiagnoseOS()
+    if not os_list:
+      # this catches also return values of 'False',
+      # for which we can't iterate over
+      return os_list
+    result = []
+    for data in os_list:
+      if isinstance(data, objects.OS):
+        result.append(data.Dumps())
+      elif isinstance(data, errors.InvalidOS):
+        result.append(data.args)
+      else:
+        raise errors.ProgrammerError, ("Invalid result from backend.DiagnoseOS"
+                                       " (class %s, %s)" %
+                                       (str(data.__class__), data))
+
+    return result
+
+  def perspective_os_get(self, params):
+    name = params[0]
+    try:
+      os = backend.OSFromDisk(name).Dumps()
+    except errors.InvalidOS, err:
+      os = err.args
+    return os
+
+  # hooks -----------------------
+
+  def perspective_hooks_runner(self, params):
+    hpath, phase, env = params
+    hr = backend.HooksRunner()
+    return hr.RunHooks(hpath, phase, env)
+
+
+class MyRealm:
+  __implements__ = portal.IRealm
+  def requestAvatar(self, avatarId, mind, *interfaces):
+    if pb.IPerspective not in interfaces:
+      raise NotImplementedError
+    return pb.IPerspective, ServerObject(avatarId), lambda:None
+
+
+def ParseOptions():
+  """Parse the command line options.
+
+  Returns:
+    (options, args) as from OptionParser.parse_args()
+
+  """
+  parser = OptionParser(description="Ganeti node daemon",
+                        usage="%prog [-f] [-d]",
+                        version="%%prog (ganeti) %s" %
+                        constants.RELEASE_VERSION)
+
+  parser.add_option("-f", "--foreground", dest="fork",
+                    help="Don't detach from the current terminal",
+                    default=True, action="store_false")
+  parser.add_option("-d", "--debug", dest="debug",
+                    help="Enable some debug messages",
+                    default=False, action="store_true")
+  options, args = parser.parse_args()
+  return options, args
+
+
+def main():
+  options, args = ParseOptions()
+  for fname in (constants.SSL_CERT_FILE,):
+    if not os.path.isfile(fname):
+      print "config %s not there, will not run." % fname
+      sys.exit(5)
+
+  try:
+    ss = ssconf.SimpleStore()
+    port = ss.GetNodeDaemonPort()
+    pwdata = ss.GetNodeDaemonPassword()
+  except errors.ConfigurationError, err:
+    print "Cluster configuration incomplete: '%s'" % str(err)
+    sys.exit(5)
+
+  # become a daemon
+  if options.fork:
+    createDaemon()
+
+  logger.SetupLogging(twisted_workaround=True, debug=options.debug,
+                      program="ganeti-noded")
+
+  p = portal.Portal(MyRealm())
+  p.registerChecker(
+    checkers.InMemoryUsernamePasswordDatabaseDontUse(master_node=pwdata))
+  reactor.listenSSL(port, pb.PBServerFactory(p), ServerContextFactory())
+  reactor.run()
+
+
+def createDaemon():
+  """Detach a process from the controlling terminal and run it in the
+  background as a daemon.
+  """
+  UMASK = 077
+  WORKDIR = "/"
+  # Default maximum for the number of available file descriptors.
+  if 'SC_OPEN_MAX' in os.sysconf_names:
+    try:
+      MAXFD = os.sysconf('SC_OPEN_MAX')
+      if MAXFD < 0:
+        MAXFD = 1024
+    except OSError:
+      MAXFD = 1024
+  else:
+    MAXFD = 1024
+  # The standard I/O file descriptors are redirected to /dev/null by default.
+  #REDIRECT_TO = getattr(os, "devnull", "/dev/null")
+  REDIRECT_TO = constants.LOG_NODESERVER
+  try:
+    pid = os.fork()
+  except OSError, e:
+    raise Exception, "%s [%d]" % (e.strerror, e.errno)
+  if (pid == 0):       # The first child.
+    os.setsid()
+    try:
+      pid = os.fork()  # Fork a second child.
+    except OSError, e:
+      raise Exception, "%s [%d]" % (e.strerror, e.errno)
+    if (pid == 0):     # The second child.
+      os.chdir(WORKDIR)
+      os.umask(UMASK)
+    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.
+  maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
+  if (maxfd == resource.RLIM_INFINITY):
+    maxfd = MAXFD
+
+  # Iterate through and close all file descriptors.
+  for fd in range(0, maxfd):
+    try:
+      os.close(fd)
+    except OSError:    # ERROR, fd wasn't open to begin with (ignored)
+      pass
+  os.open(REDIRECT_TO, os.O_RDWR|os.O_CREAT|os.O_APPEND) # standard input (0)
+  # Duplicate standard input to standard output and standard error.
+  os.dup2(0, 1)                        # standard output (1)
+  os.dup2(0, 2)                        # standard error (2)
+  return(0)
+
+
+if __name__=='__main__':
+  main()
diff --git a/daemons/ganeti-watcher b/daemons/ganeti-watcher
new file mode 100755 (executable)
index 0000000..39250e5
--- /dev/null
@@ -0,0 +1,333 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2006, 2007 Google Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+
+
+"""Tool to restart erronously downed virtual machines.
+
+This program and set of classes implement a watchdog to restart
+virtual machines in a Ganeti cluster that have crashed or been killed
+by a node reboot.  Run from cron or similar.
+"""
+
+
+LOGFILE = '/var/log/ganeti/watcher.log'
+MAXTRIES = 5
+BAD_STATES = ['stopped']
+HELPLESS_STATES = ['(node down)']
+NOTICE = 'NOTICE'
+ERROR = 'ERROR'
+
+import os
+import sys
+import time
+import fcntl
+import errno
+from optparse import OptionParser
+
+
+from ganeti import utils
+from ganeti import constants
+
+
+class Error(Exception):
+  """Generic custom error class."""
+  pass
+
+
+def Indent(s, prefix='| '):
+  """Indent a piece of text with a given prefix before each line.
+
+  Args:
+    s: The string to indent
+    prefix: The string to prepend each line.
+  """
+  return "%s%s\n" % (prefix, ('\n' + prefix).join(s.splitlines()))
+
+
+def DoCmd(cmd):
+  """Run a shell command.
+
+  Args:
+    cmd: the command to run.
+
+  Raises CommandError with verbose commentary on error.
+  """
+  res = utils.RunCmd(cmd)
+
+  if res.failed:
+    raise Error("Command %s failed:\n%s\nstdout:\n%sstderr:\n%s" %
+                (repr(cmd),
+                 Indent(res.fail_reason),
+                 Indent(res.stdout),
+                 Indent(res.stderr)))
+
+  return res
+
+
+class RestarterState(object):
+  """Interface to a state file recording restart attempts.
+
+  Methods:
+    Open(): open, lock, read and parse the file.
+            Raises StandardError on lock contention.
+
+    NumberOfAttempts(name): returns the number of times in succession
+                            a restart has been attempted of the named instance.
+
+    RecordAttempt(name, when): records one restart attempt of name at
+                               time in when.
+
+    Remove(name): remove record given by name, if exists.
+
+    Save(name): saves all records to file, releases lock and closes file.
+  """
+  def __init__(self):
+    # The two-step dance below is necessary to allow both opening existing
+    # file read/write and creating if not existing.  Vanilla open will truncate
+    # an existing file -or- allow creating if not existing.
+    f = os.open(constants.WATCHER_STATEFILE, os.O_RDWR | os.O_CREAT)
+    f = os.fdopen(f, 'w+')
+
+    try:
+      fcntl.flock(f.fileno(), fcntl.LOCK_EX|fcntl.LOCK_NB)
+    except IOError, x:
+      if x.errno == errno.EAGAIN:
+        raise StandardError('State file already locked')
+      raise
+
+    self.statefile = f
+    self.inst_map = {}
+
+    for line in f:
+      name, when, count = line.rstrip().split(':')
+
+      when = int(when)
+      count = int(count)
+
+      self.inst_map[name] = (when, count)
+
+  def NumberOfAttempts(self, instance):
+    """Returns number of previous restart attempts.
+
+    Args:
+      instance - the instance to look up.
+    """
+    assert self.statefile
+
+    if instance.name in self.inst_map:
+      return self.inst_map[instance.name][1]
+
+    return 0
+
+  def RecordAttempt(self, instance):
+    """Record a restart attempt.
+
+    Args:
+      instance - the instance being restarted
+    """
+    assert self.statefile
+
+    when = time.time()
+
+    self.inst_map[instance.name] = (when, 1 + self.NumberOfAttempts(instance))
+
+  def Remove(self, instance):
+    """Update state to reflect that a machine is running, i.e. remove record
+
+    Args:
+      instance - the instance to remove from books
+
+    This method removes the record for a named instance
+    """
+    assert self.statefile
+
+    if instance.name in self.inst_map:
+      del self.inst_map[instance.name]
+
+  def Save(self):
+    """Save records to file, then unlock and close file.
+    """
+    assert self.statefile
+
+    self.statefile.seek(0)
+    self.statefile.truncate()
+
+    for name in self.inst_map:
+      print >> self.statefile, "%s:%d:%d" % ((name,) + self.inst_map[name])
+
+    fcntl.flock(self.statefile.fileno(), fcntl.LOCK_UN)
+
+    self.statefile.close()
+    self.statefile = None
+
+
+class Instance(object):
+  """Abstraction for a Virtual Machine instance.
+
+  Methods:
+    Restart(): issue a command to restart the represented machine.
+  """
+  def __init__(self, name, state):
+    self.name = name
+    self.state = state
+
+  def Restart(self):
+    DoCmd(['gnt-instance', 'startup', '--lock-retries=15', self.name])
+
+
+class InstanceList(object):
+  """The set of Virtual Machine instances on a cluster.
+  """
+  cmd = ['gnt-instance', 'list', '--lock-retries=15',
+         '-o', 'name,admin_state,oper_state', '--no-headers', '--separator=:']
+
+  def __init__(self):
+    res = DoCmd(self.cmd)
+
+    lines = res.stdout.splitlines()
+
+    self.instances = []
+    for line in lines:
+      fields = [fld.strip() for fld in line.split(':')]
+
+      if len(fields) != 3:
+        continue
+      if fields[1] == "no": #no autostart, we don't care about this instance
+        continue
+      name, status = fields[0], fields[2]
+
+      self.instances.append(Instance(name, status))
+
+  def __iter__(self):
+    return self.instances.__iter__()
+
+
+class Message(object):
+  """Encapsulation of a notice or error message.
+  """
+  def __init__(self, level, msg):
+    self.level = level
+    self.msg = msg
+    self.when = time.time()
+
+  def __str__(self):
+    return self.level + ' ' + time.ctime(self.when) + '\n' + Indent(self.msg)
+
+
+class Restarter(object):
+  """Encapsulate the logic for restarting erronously halted virtual machines.
+
+  The calling program should periodically instantiate me and call Run().
+  This will traverse the list of instances, and make up to MAXTRIES attempts
+  to restart machines that are down.
+  """
+  def __init__(self):
+    self.instances = InstanceList()
+    self.messages = []
+
+  def Run(self):
+    """Make a pass over the list of instances, restarting downed ones.
+    """
+    notepad = RestarterState()
+
+    for instance in self.instances:
+      if instance.state in BAD_STATES:
+        n = notepad.NumberOfAttempts(instance)
+
+        if n > MAXTRIES:
+          # stay quiet.
+          continue
+        elif n < MAXTRIES:
+          last = " (Attempt #%d)" % (n + 1)
+        else:
+          notepad.RecordAttempt(instance)
+          self.messages.append(Message(ERROR, "Could not restart %s for %d"
+                                       " times, giving up..." %
+                                       (instance.name, MAXTRIES)))
+          continue
+        try:
+          self.messages.append(Message(NOTICE,
+                                       "Restarting %s%s." %
+                                       (instance.name, last)))
+          instance.Restart()
+        except Error, x:
+          self.messages.append(Message(ERROR, str(x)))
+
+        notepad.RecordAttempt(instance)
+      elif instance.state in HELPLESS_STATES:
+        if notepad.NumberOfAttempts(instance):
+          notepad.Remove(instance)
+      else:
+        if notepad.NumberOfAttempts(instance):
+          notepad.Remove(instance)
+          msg = Message(NOTICE,
+                        "Restart of %s succeeded." % instance.name)
+          self.messages.append(msg)
+
+    notepad.Save()
+
+  def WriteReport(self, logfile):
+    """
+    Log all messages to file.
+
+    Args:
+      logfile: file object open for writing (the log file)
+    """
+    for msg in self.messages:
+      print >> logfile, str(msg)
+
+
+def ParseOptions():
+  """Parse the command line options.
+
+  Returns:
+    (options, args) as from OptionParser.parse_args()
+
+  """
+  parser = OptionParser(description="Ganeti cluster watcher",
+                        usage="%prog [-d]",
+                        version="%%prog (ganeti) %s" %
+                        constants.RELEASE_VERSION)
+
+  parser.add_option("-d", "--debug", dest="debug",
+                    help="Don't redirect messages to the log file",
+                    default=False, action="store_true")
+  options, args = parser.parse_args()
+  return options, args
+
+
+def main():
+  """Main function.
+
+  """
+  options, args = ParseOptions()
+
+  if not options.debug:
+    sys.stderr = sys.stdout = open(LOGFILE, 'a')
+
+  try:
+    restarter = Restarter()
+    restarter.Run()
+    restarter.WriteReport(sys.stdout)
+  except Error, err:
+    print err
+
+if __name__ == '__main__':
+  main()
diff --git a/docs/Makefile.am b/docs/Makefile.am
new file mode 100644 (file)
index 0000000..7a499c7
--- /dev/null
@@ -0,0 +1,10 @@
+docdir = $(datadir)/doc/$(PACKAGE)
+
+dist_doc_DATA = hooks.html hooks.pdf
+EXTRA_DIST = hooks.sgml
+
+%.html: %.sgml
+       docbook2html --nochunks $<
+
+%.pdf: %.sgml
+       docbook2pdf $<
diff --git a/docs/hooks.sgml b/docs/hooks.sgml
new file mode 100644 (file)
index 0000000..daa1d07
--- /dev/null
@@ -0,0 +1,566 @@
+<!DOCTYPE article PUBLIC "-//OASIS//DTD DocBook V4.2//EN" [
+]>
+  <article class="specification">
+  <articleinfo>
+    <title>Ganeti customisation using hooks</title>
+  </articleinfo>
+  <para>Documents ganeti version 1.2</para>
+  <section>
+    <title>Introduction</title>
+
+    <para>
+      In order to allow customisation of operations, ganeti will run
+      scripts under <filename
+      class="directory">/etc/ganeti/hooks</filename> based on certain
+      rules.
+    </para>
+
+      <para>This is similar to the <filename
+      class="directory">/etc/network/</filename> structure present in
+      Debian for network interface handling.</para>
+
+    </section>
+
+
+    <section>
+      <title>Organisation</title>
+
+      <para>For every operation, two sets of scripts are run:
+
+      <itemizedlist>
+          <listitem>
+            <simpara>pre phase (for authorization/checking)</simpara>
+          </listitem>
+          <listitem>
+            <simpara>post phase (for logging)</simpara>
+          </listitem>
+        </itemizedlist>
+      </para>
+
+      <para>Also, for each operation, the scripts are run on one or
+      more nodes, depending on the operation type.</para>
+
+      <para>Note that, even though we call them scripts, we are
+      actually talking about any executable.</para>
+
+      <section>
+        <title><emphasis>pre</emphasis> scripts</title>
+
+        <para>The <emphasis>pre</emphasis> scripts have a definite
+        target: to check that the operation is allowed given the
+        site-specific constraints. You could have, for example, a rule
+        that says every new instance is required to exists in a
+        database; to implement this, you could write a script that
+        checks the new instance parameters against your
+        database.</para>
+
+        <para>The objective of these scripts should be their return
+        code (zero or non-zero for success and failure). However, if
+        they modify the environment in any way, they should be
+        idempotent, as failed executions could be restarted and thus
+        the script(s) run again with exactly the same
+        parameters.</para>
+
+      </section>
+
+      <section>
+        <title><emphasis>post</emphasis> scripts</title>
+
+        <para>These scripts should do whatever you need as a reaction
+        to the completion of an operation. Their return code is not
+        checked (but logged), and they should not depend on the fact
+        that the <emphasis>pre</emphasis> scripts have been
+        run.</para>
+
+      </section>
+
+      <section>
+        <title>Naming</title>
+
+        <para>The allowed names for the scripts consist of (similar to
+        <citerefentry> <refentrytitle>run-parts</refentrytitle>
+        <manvolnum>8</manvolnum> </citerefentry>) upper and lower
+        case, digits, underscores and hyphens. In other words, the
+        regexp
+        <computeroutput>^[a-zA-Z0-9_-]+$</computeroutput>. Also,
+        non-executable scripts will be ignored.
+        </para>
+      </section>
+
+      <section>
+        <title>Order of execution</title>
+
+        <para>On a single node, the scripts in a directory are run in
+        lexicographic order (more exactly, the python string
+        comparison order). It is advisable to implement the usual
+        <emphasis>NN-name</emphasis> convention where
+        <emphasis>NN</emphasis> is a two digit number.</para>
+
+        <para>For an operation whose hooks are run on multiple nodes,
+        there is no specific ordering of nodes with regard to hooks
+        execution; you should assume that the scripts are run in
+        parallel on the target nodes (keeping on each node the above
+        specified ordering).  If you need any kind of inter-node
+        synchronisation, you have to implement it yourself in the
+        scripts.</para>
+
+      </section>
+
+      <section>
+        <title>Execution environment</title>
+
+        <para>The scripts will be run as follows:
+          <itemizedlist>
+          <listitem>
+            <simpara>no command line arguments</simpara>
+          </listitem>
+            <listitem>
+              <simpara>no controlling <acronym>tty</acronym></simpara>
+            </listitem>
+            <listitem>
+              <simpara><varname>stdin</varname> is
+              actually <filename>/dev/null</filename></simpara>
+            </listitem>
+            <listitem>
+              <simpara><varname>stdout</varname> and
+              <varname>stderr</varname> are directed to
+              files</simpara>
+            </listitem>
+          <listitem>
+            <simpara>the <varname>PATH</varname> is reset to
+            <literal>/sbin:/bin:/usr/sbin:/usr/bin</literal></simpara>
+          </listitem>
+          <listitem>
+            <simpara>the environment is cleared, and only
+            ganeti-specific variables will be left</simpara>
+          </listitem>
+          </itemizedlist>
+
+        </para>
+
+      <para>All informations about the cluster is passed using
+      environment variables. Different operations will have sligthly
+      different environments, but most of the variables are
+      common.</para>
+
+    </section>
+
+
+    <section>
+      <title>Operation list</title>
+      <table>
+        <title>Operation list</title>
+        <tgroup cols="7">
+          <colspec>
+          <colspec>
+          <colspec>
+          <colspec>
+          <colspec>
+          <colspec colname="prehooks">
+          <colspec colname="posthooks">
+          <spanspec namest="prehooks" nameend="posthooks"
+            spanname="bothhooks">
+          <thead>
+            <row>
+              <entry>Operation ID</entry>
+              <entry>Directory prefix</entry>
+              <entry>Description</entry>
+              <entry>Command</entry>
+              <entry>Supported env. variables</entry>
+              <entry><emphasis>pre</emphasis> hooks</entry>
+              <entry><emphasis>post</emphasis> hooks</entry>
+            </row>
+          </thead>
+          <tbody>
+            <row>
+              <entry>OP_INIT_CLUSTER</entry>
+              <entry><filename class="directory">cluster-init</filename></entry>
+              <entry>Initialises the cluster</entry>
+              <entry><computeroutput>gnt-cluster init</computeroutput></entry>
+              <entry><constant>CLUSTER</constant>, <constant>MASTER</constant></entry>
+              <entry spanname="bothhooks">master node, cluster name</entry>
+            </row>
+            <row>
+              <entry>OP_MASTER_FAILOVER</entry>
+              <entry><filename class="directory">master-failover</filename></entry>
+              <entry>Changes the master</entry>
+              <entry><computeroutput>gnt-cluster master-failover</computeroutput></entry>
+              <entry><constant>OLD_MASTER</constant>, <constant>NEW_MASTER</constant></entry>
+              <entry>the new master</entry>
+              <entry>all nodes</entry>
+            </row>
+            <row>
+              <entry>OP_ADD_NODE</entry>
+              <entry><filename class="directory">node-add</filename></entry>
+              <entry>Adds a new node to the cluster</entry>
+              <entry><computeroutput>gnt-node add</computeroutput></entry>
+              <entry><constant>NODE_NAME</constant>, <constant>NODE_PIP</constant>, <constant>NODE_SIP</constant></entry>
+              <entry>all existing nodes</entry>
+              <entry>all existing nodes plus the new node</entry>
+            </row>
+            <row>
+              <entry>OP_REMOVE_NODE</entry>
+              <entry><filename class="directory">node-remove</filename></entry>
+              <entry>Removes a node from the cluster</entry>
+              <entry><computeroutput>gnt-node remove</computeroutput></entry>
+              <entry><constant>NODE_NAME</constant></entry>
+              <entry spanname="bothhooks">all existing nodes except the removed node</entry>
+            </row>
+            <row>
+              <entry>OP_INSTANCE_ADD</entry>
+              <entry><filename class="directory">instance-add</filename></entry>
+              <entry>Creates a new instance</entry>
+              <entry><computeroutput>gnt-instance add</computeroutput></entry>
+              <entry><constant>INSTANCE_NAME</constant>, <constant>INSTANCE_PRIMARY</constant>, <constant>INSTANCE_SECONDARIES</constant>, <constant>DISK_TEMPLATE</constant>, <constant>MEM_SIZE</constant>, <constant>DISK_SIZE</constant>, <constant>SWAP_SIZE</constant>, <constant>VCPUS</constant>, <constant>INSTANCE_IP</constant>, <constant>INSTANCE_ADD_MODE</constant>, <constant>SRC_NODE</constant>, <constant>SRC_PATH</constant>, <constant>SRC_IMAGE</constant></entry>
+              <entry spanname="bothhooks" morerows="4">master node, primary and
+                   secondary nodes</entry>
+            </row>
+            <row>
+              <entry>OP_BACKUP_EXPORT</entry>
+              <entry><filename class="directory">instance-export</filename></entry>
+              <entry>Export the instance</entry>
+              <entry><computeroutput>gnt-backup export</computeroutput></entry>
+              <entry><constant>INSTANCE_NAME</constant>, <constant>EXPORT_NODE</constant>, <constant>EXPORT_DO_SHUTDOWN</constant></entry>
+            </row>
+            <row>
+              <entry>OP_INSTANCE_START</entry>
+              <entry><filename class="directory">instance-start</filename></entry>
+              <entry>Starts an instance</entry>
+              <entry><computeroutput>gnt-instance start</computeroutput></entry>
+              <entry><constant>INSTANCE_NAME</constant>, <constant>INSTANCE_PRIMARY</constant>, <constant>INSTANCE_SECONDARIES</constant>, <constant>FORCE</constant></entry>
+            </row>
+            <row>
+              <entry>OP_INSTANCE_SHUTDOWN</entry>
+              <entry><filename class="directory">instance-shutdown</filename></entry>
+              <entry>Stops an instance</entry>
+              <entry><computeroutput>gnt-instance shutdown</computeroutput></entry>
+              <entry><constant>INSTANCE_NAME</constant>, <constant>INSTANCE_PRIMARY</constant>, <constant>INSTANCE_SECONDARIES</constant></entry>
+            </row>
+            <row>
+              <entry>OP_INSTANCE_MODIFY</entry>
+              <entry><filename class="directory">instance-modify</filename></entry>
+              <entry>Modifies the instance parameters.</entry>
+              <entry><computeroutput>gnt-instance modify</computeroutput></entry>
+              <entry><constant>INSTANCE_NAME</constant>, <constant>MEM_SIZE</constant>, <constant>VCPUS</constant>, <constant>INSTANCE_IP</constant></entry>
+            </row>
+            <row>
+              <entry>OP_INSTANCE_FAILOVER</entry>
+              <entry><filename class="directory">instance-failover</filename></entry>
+              <entry>Failover an instance</entry>
+              <entry><computeroutput>gnt-instance start</computeroutput></entry>
+              <entry><constant>INSTANCE_NAME</constant>, <constant>INSTANCE_PRIMARY</constant>, <constant>INSTANCE_SECONDARIES</constant>, <constant>IGNORE_CONSISTENCY</constant></entry>
+            </row>
+            <row>
+              <entry>OP_INSTANCE_REMOVE</entry>
+              <entry><filename class="directory">instance-remove</filename></entry>
+              <entry>Remove an instance</entry>
+              <entry><computeroutput>gnt-instance remove</computeroutput></entry>
+              <entry><constant>INSTANCE_NAME</constant>, <constant>INSTANCE_PRIMARY</constant>, <constant>INSTANCE_SECONDARIES</constant></entry>
+            </row>
+            <row>
+              <entry>OP_INSTANCE_ADD_MDDRBD</entry>
+              <entry><filename class="directory">mirror-add</filename></entry>
+              <entry>Adds a mirror component</entry>
+              <entry><computeroutput>gnt-instance add-mirror</computeroutput></entry>
+              <entry><constant>INSTANCE_NAME</constant>, <constant>NEW_SECONDARY</constant>, <constant>DISK_NAME</constant></entry>
+            </row>
+            <row>
+              <entry>OP_INSTANCE_REMOVE_MDDRBD</entry>
+              <entry><filename class="directory">mirror-remove</filename></entry>
+              <entry>Removes a mirror component</entry>
+              <entry><computeroutput>gnt-instance remove-mirror</computeroutput></entry>
+              <entry><constant>INSTANCE_NAME</constant>, <constant>OLD_SECONDARY</constant>, <constant>DISK_NAME</constant>, <constant>DISK_ID</constant></entry>
+            </row>
+            <row>
+              <entry>OP_INSTANCE_REPLACE_DISKS</entry>
+              <entry><filename class="directory">mirror-replace</filename></entry>
+              <entry>Replace all mirror components</entry>
+              <entry><computeroutput>gnt-instance replace-disks</computeroutput></entry>
+              <entry><constant>INSTANCE_NAME</constant>, <constant>OLD_SECONDARY</constant>, <constant>NEW_SECONDARY</constant></entry>
+
+            </row>
+          </tbody>
+        </tgroup>
+      </table>
+    </section>
+
+    <section>
+      <title>Environment variables</title>
+
+      <para>Note that all variables listed here are actually prefixed
+      with <constant>GANETI_</constant> in order to provide a
+      different namespace.</para>
+
+      <section>
+        <title>Common variables</title>
+
+        <para>This is the list of environment variables supported by
+        all operations:</para>
+
+        <variablelist>
+          <varlistentry>
+            <term>HOOKS_VERSION</term>
+            <listitem>
+              <para>Documents the hooks interface version. In case this
+            doesnt match what the script expects, it should not
+            run. The documents conforms to the version
+            <literal>1</literal>.</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>HOOKS_PHASE</term>
+            <listitem>
+              <para>one of <constant>PRE</constant> or
+              <constant>POST</constant> denoting which phase are we
+              in.</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>CLUSTER</term>
+            <listitem>
+              <para>the cluster name</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>MASTER</term>
+            <listitem>
+              <para>the master node</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>OP_ID</term>
+            <listitem>
+              <para>one of the <constant>OP_*</constant> values from
+              the table of operations</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>OBJECT_TYPE</term>
+            <listitem>
+              <para>one of <simplelist type="inline">
+                  <member><constant>INSTANCE</constant></member>
+                  <member><constant>NODE</constant></member>
+                  <member><constant>CLUSTER</constant></member>
+                </simplelist>, showing the target of the operation.
+             </para>
+            </listitem>
+          </varlistentry>
+          <!-- commented out since it causes problems in our rpc
+               multi-node optimised calls
+          <varlistentry>
+            <term>HOST_NAME</term>
+            <listitem>
+              <para>The name of the node the hook is run on as known by
+            the cluster.</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>HOST_TYPE</term>
+            <listitem>
+              <para>one of <simplelist type="inline">
+                  <member><constant>MASTER</constant></member>
+                  <member><constant>NODE</constant></member>
+                </simplelist>, showing the role of this node in the cluster.
+             </para>
+            </listitem>
+          </varlistentry>
+          -->
+        </variablelist>
+      </section>
+
+      <section>
+        <title>Specialised variables</title>
+
+        <para>This is the list of variables which are specific to one
+        or more operations.</para>
+        <variablelist>
+          <varlistentry>
+            <term>INSTANCE_NAME</term>
+            <listitem>
+              <para>The name of the instance which is the target of
+              the operation.</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>INSTANCE_DISK_TYPE</term>
+            <listitem>
+              <para>The disk type for the instance.</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>INSTANCE_DISK_SIZE</term>
+            <listitem>
+              <para>The (OS) disk size for the instance.</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>INSTANCE_OS</term>
+            <listitem>
+              <para>The name of the instance OS.</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>INSTANCE_PRIMARY</term>
+            <listitem>
+              <para>The name of the node which is the primary for the
+              instance.</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>INSTANCE_SECONDARIES</term>
+            <listitem>
+              <para>Space-separated list of secondary nodes for the
+              instance.</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>NODE_NAME</term>
+            <listitem>
+              <para>The target node of this operation (not the node on
+              which the hook runs).</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>NODE_PIP</term>
+            <listitem>
+              <para>The primary IP of the target node (the one over
+              which inter-node communication is done).</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>NODE_SIP</term>
+            <listitem>
+              <para>The secondary IP of the target node (the one over
+              which drbd replication is done). This can be equal to
+              the primary ip, in case the cluster is not
+              dual-homed.</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>OLD_MASTER</term>
+            <term>NEW_MASTER</term>
+            <listitem>
+              <para>The old, respectively the new master for the
+              master failover operation.</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>FORCE</term>
+            <listitem>
+              <para>This is provided by some operations when the user
+              gave this flag.</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>IGNORE_CONSISTENCY</term>
+            <listitem>
+              <para>The user has specified this flag. It is used when
+              failing over instances in case the primary node is
+              down.</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>MEM_SIZE, DISK_SIZE, SWAP_SIZE, VCPUS</term>
+            <listitem>
+              <para>The memory, disk, swap size and the number of
+              processor selected for the instance (in
+              <command>gnt-instance add</command> or
+              <command>gnt-instance modify</command>).</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>INSTANCE_IP</term>
+            <listitem>
+              <para>If defined, the instance IP in the
+              <command>gnt-instance add</command> and
+              <command>gnt-instance set</command> commands. If not
+              defined, it means that no IP has been defined.</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>DISK_TEMPLATE</term>
+            <listitem>
+              <para>The disk template type when creating the instance.</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>INSTANCE_ADD_MODE</term>
+            <listitem>
+              <para>The mode of the create: either
+              <constant>create</constant> for create from scratch or
+              <constant>import</constant> for restoring from an
+              exported image.</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>SRC_NODE, SRC_PATH, SRC_IMAGE</term>
+            <listitem>
+              <para>In case the instance has been added by import,
+              these variables are defined and point to the source
+              node, source path (the directory containing the image
+              and the config file) and the source disk image
+              file.</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>DISK_NAME</term>
+            <listitem>
+              <para>The disk name (either <filename>sda</filename> or
+              <filename>sdb</filename>) in mirror operations
+              (add/remove mirror).</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>DISK_ID</term>
+            <listitem>
+              <para>The disk id for mirror remove operations. You can
+              look this up using <command>gnt-instance
+              info</command>.</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>NEW_SECONDARY</term>
+            <listitem>
+              <para>The name of the node on which the new mirror
+              componet is being added. This can be the name of the
+              current secondary, if the new mirror is on the same
+              secondary.</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>OLD_SECONDARY</term>
+            <listitem>
+              <para>The name of the old secondary. This is used in
+              both <command>replace-disks</command> and
+              <command>remove-mirror</command>. Note that this can be
+              equal to the new secondary (only
+              <command>replace-disks</command> has both variables) if
+              the secondary node hasn't actually changed).</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>EXPORT_NODE</term>
+            <listitem>
+              <para>The node on which the exported image of the
+              instance was done.</para>
+            </listitem>
+          </varlistentry>
+          <varlistentry>
+            <term>EXPORT_DO_SHUTDOWN</term>
+            <listitem>
+              <para>This variable tells if the instance has been
+              shutdown or not while doing the export. In the "was
+              shutdown" case, it's likely that the filesystem is
+              consistent, whereas in the "did not shutdown" case, the
+              filesystem would need a check (journal replay or full
+              fsck) in order to guarantee consistency.</para>
+            </listitem>
+          </varlistentry>
+        </variablelist>
+
+      </section>
+
+    </section>
+
+  </section>
+  </article>
diff --git a/ganeti.initd b/ganeti.initd
new file mode 100755 (executable)
index 0000000..a10fe99
--- /dev/null
@@ -0,0 +1,52 @@
+#! /bin/sh
+# ganeti node daemon starter script
+# based on skeleton from Debian GNU/Linux
+
+PATH=/sbin:/bin:/usr/sbin:/usr/bin
+DAEMON=/usr/local/sbin/ganeti-noded
+NAME=ganeti-noded
+SCRIPTNAME=/etc/init.d/ganeti
+DESC="Ganeti node daemon"
+
+test -f $DAEMON || exit 0
+
+set -e
+
+. /lib/lsb/init-functions
+
+check_config() {
+       for fname in /var/lib/ganeti/ssconf_node_pass /var/lib/ganeti/server.pem; do
+               if ! [ -f "$fname" ]; then
+                       log_end_msg 0
+                       log_warning_msg "Config $fname not there, will not run."
+                       exit 0
+               fi
+       done
+}
+
+case "$1" in
+  start)
+       log_begin_msg "Starting $DESC..."
+       check_config
+       start-stop-daemon --start --quiet --exec $DAEMON || log_end_msg 1
+       log_end_msg 0
+       ;;
+  stop)
+       log_begin_msg "Stopping $DESC..."
+       start-stop-daemon --stop --quiet --name $NAME || log_end_msg 1
+       log_end_msg 0
+       ;;
+  restart|force-reload)
+       log_begin_msg "Reloading $DESC..."
+       start-stop-daemon --stop --quiet --oknodo --retry 30 --name $NAME
+       check_config
+       start-stop-daemon --start --quiet --exec $DAEMON || log_end_msg 1
+       log_end_msg 0
+       ;;
+  *)
+       log_success_msg "Usage: $SCRIPTNAME {start|stop|force-reload|restart}"
+       exit 1
+       ;;
+esac
+
+exit 0
diff --git a/lib/Makefile.am b/lib/Makefile.am
new file mode 100644 (file)
index 0000000..0282359
--- /dev/null
@@ -0,0 +1,4 @@
+pkgpython_PYTHON = __init__.py backend.py cli.py cmdlib.py config.py \
+       objects.py errors.py logger.py ssh.py utils.py rpc.py \
+       bdev.py hypervisor.py opcodes.py mcpu.py constants.py \
+       ssconf.py
diff --git a/lib/__init__.py b/lib/__init__.py
new file mode 100644 (file)
index 0000000..d0292bb
--- /dev/null
@@ -0,0 +1,22 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2006, 2007 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
diff --git a/lib/backend.py b/lib/backend.py
new file mode 100644 (file)
index 0000000..7b36e37
--- /dev/null
@@ -0,0 +1,1337 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2006, 2007 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.
+
+
+"""Functions used by the node daemon"""
+
+
+import os
+import os.path
+import shutil
+import time
+import tempfile
+import stat
+import errno
+import re
+import subprocess
+
+from ganeti import logger
+from ganeti import errors
+from ganeti import utils
+from ganeti import ssh
+from ganeti import hypervisor
+from ganeti import constants
+from ganeti import bdev
+from ganeti import objects
+
+
+def ListConfigFiles():
+  """Return a list of the config files present on the local node.
+  """
+
+  configfiles = []
+
+  for testfile in constants.MASTER_CONFIGFILES:
+    if os.path.exists(testfile):
+      configfiles.append(testfile)
+
+  for testfile in constants.NODE_CONFIGFILES:
+    if os.path.exists(testfile):
+      configfiles.append(testfile)
+
+  return configfiles
+
+
+def StartMaster():
+  """Activate local node as master node.
+
+  There are two needed steps for this:
+    - register the master init script, and also run it now
+    - register the cron script
+
+  """
+  result = utils.RunCmd(["update-rc.d", constants.MASTER_INITD_NAME,
+                         "defaults", "21", "79"])
+
+  if result.failed:
+    logger.Error("could not register the master init.d script with command"
+                 " %s, error %s" % (result.cmd, result.output))
+    return False
+
+  result = utils.RunCmd([constants.MASTER_INITD_SCRIPT, "start"])
+
+  if result.failed:
+    logger.Error("could not activate cluster interface with command %s,"
+                 " error %s" % (result.cmd, result.output))
+    return False
+
+  utils.RemoveFile(constants.MASTER_CRON_LINK)
+  os.symlink(constants.MASTER_CRON_FILE, constants.MASTER_CRON_LINK)
+  return True
+
+
+def StopMaster():
+  """Deactivate this node as master.
+
+  This does two things:
+    - remove links to master's startup script
+    - remove link to master cron script.
+
+  """
+  result = utils.RunCmd(["update-rc.d", "-f",
+                          constants.MASTER_INITD_NAME, "remove"])
+  if result.failed:
+    logger.Error("could not unregister the master script with command"
+                 " %s, error %s" % (result.cmd, result.output))
+    return False
+
+  output = utils.RunCmd([constants.MASTER_INITD_SCRIPT, "stop"])
+
+  if result.failed:
+    logger.Error("could not deactivate cluster interface with command %s,"
+                 " error %s" % (result.cmd, result.output))
+    return False
+
+  utils.RemoveFile(constants.MASTER_CRON_LINK)
+
+  return True
+
+
+def AddNode(dsa, dsapub, rsa, rsapub, ssh, sshpub):
+  """ adds the node to the cluster
+      - updates the hostkey
+      - adds the ssh-key
+      - sets the node id
+      - sets the node status to installed
+  """
+
+  f = open("/etc/ssh/ssh_host_rsa_key", 'w')
+  f.write(rsa)
+  f.close()
+
+  f = open("/etc/ssh/ssh_host_rsa_key.pub", 'w')
+  f.write(rsapub)
+  f.close()
+
+  f = open("/etc/ssh/ssh_host_dsa_key", 'w')
+  f.write(dsa)
+  f.close()
+
+  f = open("/etc/ssh/ssh_host_dsa_key.pub", 'w')
+  f.write(dsapub)
+  f.close()
+
+  if not os.path.isdir("/root/.ssh"):
+    os.mkdir("/root/.ssh")
+
+  f = open("/root/.ssh/id_dsa", 'w')
+  f.write(ssh)
+  f.close()
+
+  f = open("/root/.ssh/id_dsa.pub", 'w')
+  f.write(sshpub)
+  f.close()
+
+  f = open('/root/.ssh/id_dsa.pub', 'r')
+  try:
+    utils.AddAuthorizedKey('/root/.ssh/authorized_keys', f.read(8192))
+  finally:
+    f.close()
+
+  utils.RunCmd(["/etc/init.d/ssh", "restart"])
+
+  utils.RemoveFile("/root/.ssh/known_hosts")
+  return True
+
+
+def LeaveCluster():
+  """Cleans up the current node and prepares it to be removed from the cluster.
+
+  """
+  if os.path.exists(constants.DATA_DIR):
+    for dirpath, dirnames, filenames in os.walk(constants.DATA_DIR):
+      if dirpath == constants.DATA_DIR:
+        for i in filenames:
+          os.unlink(os.path.join(dirpath, i))
+  utils.RemoveFile(constants.CLUSTER_NAME_FILE)
+
+  f = open('/root/.ssh/id_dsa.pub', 'r')
+  try:
+    utils.RemoveAuthorizedKey('/root/.ssh/authorized_keys', f.read(8192))
+  finally:
+    f.close()
+
+  utils.RemoveFile('/root/.ssh/id_dsa')
+  utils.RemoveFile('/root/.ssh/id_dsa.pub')
+
+
+def GetNodeInfo(vgname):
+  """ gives back a hash with different informations
+  about the node
+
+  Returns:
+    { 'vg_size' : xxx,  'vg_free' : xxx, 'memory_domain0': xxx,
+      'memory_free' : xxx, 'memory_total' : xxx }
+    where
+    vg_size is the size of the configured volume group in MiB
+    vg_free is the free size of the volume group in MiB
+    memory_dom0 is the memory allocated for domain0 in MiB
+    memory_free is the currently available (free) ram in MiB
+    memory_total is the total number of ram in MiB
+  """
+
+  outputarray = {}
+  vginfo = _GetVGInfo(vgname)
+  outputarray['vg_size'] = vginfo['vg_size']
+  outputarray['vg_free'] = vginfo['vg_free']
+
+  hyper = hypervisor.GetHypervisor()
+  hyp_info = hyper.GetNodeInfo()
+  if hyp_info is not None:
+    outputarray.update(hyp_info)
+
+  return outputarray
+
+
+def VerifyNode(what):
+  """Verify the status of the local node.
+
+  Args:
+    what - a dictionary of things to check:
+      'filelist' : list of files for which to compute checksums
+      'nodelist' : list of nodes we should check communication with
+      'hypervisor': run the hypervisor-specific verify
+
+  Requested files on local node are checksummed and the result returned.
+
+  The nodelist is traversed, with the following checks being made
+  for each node:
+  - known_hosts key correct
+  - correct resolving of node name (target node returns its own hostname
+    by ssh-execution of 'hostname', result compared against name in list.
+
+  """
+
+  result = {}
+
+  if 'hypervisor' in what:
+    result['hypervisor'] = hypervisor.GetHypervisor().Verify()
+
+  if 'filelist' in what:
+    result['filelist'] = utils.FingerprintFiles(what['filelist'])
+
+  if 'nodelist' in what:
+    result['nodelist'] = {}
+    for node in what['nodelist']:
+      success, message = ssh.VerifyNodeHostname(node)
+      if not success:
+        result['nodelist'][node] = message
+  return result
+
+
+def GetVolumeList(vg_name):
+  """Compute list of logical volumes and their size.
+
+  Returns:
+    dictionary of all partions (key) with their size:
+    test1: 20.06MiB
+
+  """
+  result = utils.RunCmd(["lvs", "--noheadings", "--units=m",
+                         "-oname,size", vg_name])
+  if result.failed:
+    logger.Error("Failed to list logical volumes, lvs output: %s" %
+                 result.output)
+    return {}
+
+  lvlist = [line.split() for line in result.output.splitlines()]
+  return dict(lvlist)
+
+
+def ListVolumeGroups():
+  """List the volume groups and their size
+
+  Returns:
+    Dictionary with keys volume name and values the size of the volume
+
+  """
+  return utils.ListVolumeGroups()
+
+
+def BridgesExist(bridges_list):
+  """Check if a list of bridges exist on the current node
+
+  Returns:
+    True if all of them exist, false otherwise
+
+  """
+  for bridge in bridges_list:
+    if not utils.BridgeExists(bridge):
+      return False
+
+  return True
+
+
+def GetInstanceList():
+  """ provides a list of instances
+
+  Returns:
+    A list of all running instances on the current node
+    - instance1.example.com
+    - instance2.example.com
+  """
+
+  try:
+    names = hypervisor.GetHypervisor().ListInstances()
+  except errors.HypervisorError, err:
+    logger.Error("error enumerating instances: %s" % str(err))
+    raise
+
+  return names
+
+
+def GetInstanceInfo(instance):
+  """ gives back the informations about an instance
+  as a dictonary
+
+  Args:
+    instance: name of the instance (ex. instance1.example.com)
+
+  Returns:
+    { 'memory' : 511, 'state' : '-b---', 'time' : 3188.8, }
+    where
+    memory: memory size of instance (int)
+    state: xen state of instance (string)
+    time: cpu time of instance (float)
+  """
+
+  output = {}
+
+  iinfo = hypervisor.GetHypervisor().GetInstanceInfo(instance)
+  if iinfo is not None:
+    output['memory'] = iinfo[2]
+    output['state'] = iinfo[4]
+    output['time'] = iinfo[5]
+
+  return output
+
+
+def GetAllInstancesInfo():
+  """Gather data about all instances.
+
+  This is the equivalent of `GetInstanceInfo()`, except that it
+  computes data for all instances at once, thus being faster if one
+  needs data about more than one instance.
+
+  Returns: a dictionary of dictionaries, keys being the instance name,
+    and with values:
+    { 'memory' : 511, 'state' : '-b---', 'time' : 3188.8, }
+    where
+    memory: memory size of instance (int)
+    state: xen state of instance (string)
+    time: cpu time of instance (float)
+    vcpus: the number of cpus
+  """
+
+  output = {}
+
+  iinfo = hypervisor.GetHypervisor().GetAllInstancesInfo()
+  if iinfo:
+    for name, id, memory, vcpus, state, times in iinfo:
+      output[name] = {
+        'memory': memory,
+        'vcpus': vcpus,
+        'state': state,
+        'time': times,
+        }
+
+  return output
+
+
+def AddOSToInstance(instance, os_disk, swap_disk):
+  """Add an os to an instance.
+
+  Args:
+    instance: the instance object
+    os_disk: the instance-visible name of the os device
+    swap_disk: the instance-visible name of the swap device
+
+  """
+  inst_os = OSFromDisk(instance.os)
+
+  create_script = inst_os.create_script
+
+  for os_device in instance.disks:
+    if os_device.iv_name == os_disk:
+      break
+  else:
+    logger.Error("Can't find this device-visible name '%s'" % os_disk)
+    return False
+
+  for swap_device in instance.disks:
+    if swap_device.iv_name == swap_disk:
+      break
+  else:
+    logger.Error("Can't find this device-visible name '%s'" % swap_disk)
+    return False
+
+  real_os_dev = _RecursiveFindBD(os_device)
+  if real_os_dev is None:
+    raise errors.BlockDeviceError("Block device '%s' is not set up" %
+                                  str(os_device))
+  real_os_dev.Open()
+
+  real_swap_dev = _RecursiveFindBD(swap_device)
+  if real_swap_dev is None:
+    raise errors.BlockDeviceError("Block device '%s' is not set up" %
+                                  str(swap_device))
+  real_swap_dev.Open()
+
+  logfile = "%s/add-%s-%s-%d.log" % (constants.LOG_OS_DIR, instance.os,
+                                     instance.name, int(time.time()))
+  if not os.path.exists(constants.LOG_OS_DIR):
+    os.mkdir(constants.LOG_OS_DIR, 0750)
+
+  command = utils.BuildShellCmd("cd %s; %s -i %s -b %s -s %s &>%s",
+                                inst_os.path, create_script, instance.name,
+                                real_os_dev.dev_path, real_swap_dev.dev_path,
+                                logfile)
+
+  result = utils.RunCmd(command)
+
+  if result.failed:
+    logger.Error("os create command '%s' returned error: %s"
+                 " output: %s" %
+                 (command, result.fail_reason, result.output))
+    return False
+
+  return True
+
+
+def _GetVGInfo(vg_name):
+  """Get informations about the volume group.
+
+  Args:
+    vg_name: the volume group
+
+  Returns:
+    { 'vg_size' : xxx, 'vg_free' : xxx, 'pv_count' : xxx }
+    where
+    vg_size is the total size of the volume group in MiB
+    vg_free is the free size of the volume group in MiB
+    pv_count are the number of physical disks in that vg
+
+  """
+  retval = utils.RunCmd(["vgs", "-ovg_size,vg_free,pv_count", "--noheadings",
+                         "--nosuffix", "--units=m", "--separator=:", vg_name])
+
+  if retval.failed:
+    errmsg = "volume group %s not present" % vg_name
+    logger.Error(errmsg)
+    raise errors.LVMError(errmsg)
+  valarr = retval.stdout.strip().split(':')
+  retdic = {
+    "vg_size": int(round(float(valarr[0]), 0)),
+    "vg_free": int(round(float(valarr[1]), 0)),
+    "pv_count": int(valarr[2]),
+    }
+  return retdic
+
+
+def _GatherBlockDevs(instance):
+  """Set up an instance's block device(s).
+
+  This is run on the primary node at instance startup. The block
+  devices must be already assembled.
+
+  """
+  block_devices = []
+  for disk in instance.disks:
+    device = _RecursiveFindBD(disk)
+    if device is None:
+      raise errors.BlockDeviceError("Block device '%s' is not set up." %
+                                    str(disk))
+    device.Open()
+    block_devices.append((disk, device))
+  return block_devices
+
+
+def StartInstance(instance, extra_args):
+  """Start an instance.
+
+  Args:
+    instance - name of instance to start.
+  """
+
+  running_instances = GetInstanceList()
+
+  if instance.name in running_instances:
+    return True
+
+  block_devices = _GatherBlockDevs(instance)
+  hyper = hypervisor.GetHypervisor()
+
+  try:
+    hyper.StartInstance(instance, block_devices, extra_args)
+  except errors.HypervisorError, err:
+    logger.Error("Failed to start instance: %s" % err)
+    return False
+
+  return True
+
+
+def ShutdownInstance(instance):
+  """Shut an instance down.
+
+  Args:
+    instance - name of instance to shutdown.
+  """
+
+  running_instances = GetInstanceList()
+
+  if instance.name not in running_instances:
+    return True
+
+  hyper = hypervisor.GetHypervisor()
+  try:
+    hyper.StopInstance(instance)
+  except errors.HypervisorError, err:
+    logger.Error("Failed to stop instance: %s" % err)
+    return False
+
+  # test every 10secs for 2min
+  shutdown_ok = False
+
+  time.sleep(1)
+  for dummy in range(11):
+    if instance.name not in GetInstanceList():
+      break
+    time.sleep(10)
+  else:
+    # the shutdown did not succeed
+    logger.Error("shutdown of '%s' unsuccessful, using destroy" % instance)
+
+    try:
+      hyper.StopInstance(instance, force=True)
+    except errors.HypervisorError, err:
+      logger.Error("Failed to stop instance: %s" % err)
+      return False
+
+    time.sleep(1)
+    if instance.name in GetInstanceList():
+      logger.Error("could not shutdown instance '%s' even by destroy")
+      return False
+
+  return True
+
+
+def CreateBlockDevice(disk, size, on_primary):
+  """Creates a block device for an instance.
+
+  Args:
+   bdev: a ganeti.objects.Disk object
+   size: the size of the physical underlying devices
+   do_open: if the device should be `Assemble()`-d and
+            `Open()`-ed after creation
+
+  Returns:
+    the new unique_id of the device (this can sometime be
+    computed only after creation), or None. On secondary nodes,
+    it's not required to return anything.
+
+  """
+  clist = []
+  if disk.children:
+    for child in disk.children:
+      crdev = _RecursiveAssembleBD(child, on_primary)
+      if on_primary or disk.AssembleOnSecondary():
+        # we need the children open in case the device itself has to
+        # be assembled
+        crdev.Open()
+      else:
+        crdev.Close()
+      clist.append(crdev)
+  try:
+    device = bdev.FindDevice(disk.dev_type, disk.physical_id, clist)
+    if device is not None:
+      logger.Info("removing existing device %s" % disk)
+      device.Remove()
+  except errors.BlockDeviceError, err:
+    pass
+
+  device = bdev.Create(disk.dev_type, disk.physical_id,
+                       clist, size)
+  if device is None:
+    raise ValueError("Can't create child device for %s, %s" %
+                     (disk, size))
+  if on_primary or disk.AssembleOnSecondary():
+    device.Assemble()
+    device.SetSyncSpeed(30*1024)
+    if on_primary or disk.OpenOnSecondary():
+      device.Open(force=True)
+  physical_id = device.unique_id
+  return physical_id
+
+
+def RemoveBlockDevice(disk):
+  """Remove a block device.
+
+  This is intended to be called recursively.
+
+  """
+  try:
+    # since we are removing the device, allow a partial match
+    # this allows removal of broken mirrors
+    rdev = _RecursiveFindBD(disk, allow_partial=True)
+  except errors.BlockDeviceError, err:
+    # probably can't attach
+    logger.Info("Can't attach to device %s in remove" % disk)
+    rdev = None
+  if rdev is not None:
+    result = rdev.Remove()
+  else:
+    result = True
+  if disk.children:
+    for child in disk.children:
+      result = result and RemoveBlockDevice(child)
+  return result
+
+
+def _RecursiveAssembleBD(disk, as_primary):
+  """Activate a block device for an instance.
+
+  This is run on the primary and secondary nodes for an instance.
+
+  This function is called recursively.
+
+  Args:
+    disk: a objects.Disk object
+    as_primary: if we should make the block device read/write
+
+  Returns:
+    the assembled device or None (in case no device was assembled)
+
+  If the assembly is not successful, an exception is raised.
+
+  """
+  children = []
+  if disk.children:
+    for chld_disk in disk.children:
+      children.append(_RecursiveAssembleBD(chld_disk, as_primary))
+
+  if as_primary or disk.AssembleOnSecondary():
+    r_dev = bdev.AttachOrAssemble(disk.dev_type, disk.physical_id, children)
+    r_dev.SetSyncSpeed(30*1024)
+    result = r_dev
+    if as_primary or disk.OpenOnSecondary():
+      r_dev.Open()
+    else:
+      r_dev.Close()
+  else:
+    result = True
+  return result
+
+
+def AssembleBlockDevice(disk, as_primary):
+  """Activate a block device for an instance.
+
+  This is a wrapper over _RecursiveAssembleBD.
+
+  Returns:
+    a /dev path for primary nodes
+    True for secondary nodes
+
+  """
+  result = _RecursiveAssembleBD(disk, as_primary)
+  if isinstance(result, bdev.BlockDev):
+    result = result.dev_path
+  return result
+
+
+def ShutdownBlockDevice(disk):
+  """Shut down a block device.
+
+  First, if the device is assembled (can `Attach()`), then the device
+  is shutdown. Then the children of the device are shutdown.
+
+  This function is called recursively. Note that we don't cache the
+  children or such, as oppossed to assemble, shutdown of different
+  devices doesn't require that the upper device was active.
+
+  """
+  r_dev = _RecursiveFindBD(disk)
+  if r_dev is not None:
+    result = r_dev.Shutdown()
+  else:
+    result = True
+  if disk.children:
+    for child in disk.children:
+      result = result and ShutdownBlockDevice(child)
+  return result
+
+
+def MirrorAddChild(md_cdev, new_cdev):
+  """Extend an MD raid1 array.
+
+  """
+  md_bdev = _RecursiveFindBD(md_cdev, allow_partial=True)
+  if md_bdev is None:
+    logger.Error("Can't find md device")
+    return False
+  new_bdev = _RecursiveFindBD(new_cdev)
+  if new_bdev is None:
+    logger.Error("Can't find new device to add")
+    return False
+  new_bdev.Open()
+  md_bdev.AddChild(new_bdev)
+  return True
+
+
+def MirrorRemoveChild(md_cdev, new_cdev):
+  """Reduce an MD raid1 array.
+
+  """
+  md_bdev = _RecursiveFindBD(md_cdev)
+  if md_bdev is None:
+    return False
+  new_bdev = _RecursiveFindBD(new_cdev)
+  if new_bdev is None:
+    return False
+  new_bdev.Open()
+  md_bdev.RemoveChild(new_bdev.dev_path)
+  return True
+
+
+def GetMirrorStatus(disks):
+  """Get the mirroring status of a list of devices.
+
+  Args:
+    disks: list of `objects.Disk`
+
+  Returns:
+    list of (mirror_done, estimated_time) tuples, which
+    are the result of bdev.BlockDevice.CombinedSyncStatus()
+
+  """
+  stats = []
+  for dsk in disks:
+    rbd = _RecursiveFindBD(dsk)
+    if rbd is None:
+      raise errors.BlockDeviceError, "Can't find device %s" % str(dsk)
+    stats.append(rbd.CombinedSyncStatus())
+  return stats
+
+
+def _RecursiveFindBD(disk, allow_partial=False):
+  """Check if a device is activated.
+
+  If so, return informations about the real device.
+
+  Args:
+    disk: the objects.Disk instance
+    allow_partial: don't abort the find if a child of the
+                   device can't be found; this is intended to be
+                   used when repairing mirrors
+
+  Returns:
+    None if the device can't be found
+    otherwise the device instance
+
+  """
+  children = []
+  if disk.children:
+    for chdisk in disk.children:
+      children.append(_RecursiveFindBD(chdisk))
+
+  return bdev.FindDevice(disk.dev_type, disk.physical_id, children)
+
+
+def FindBlockDevice(disk):
+  """Check if a device is activated.
+
+  If so, return informations about the real device.
+
+  Args:
+    disk: the objects.Disk instance
+  Returns:
+    None if the device can't be found
+    (device_path, major, minor, sync_percent, estimated_time, is_degraded)
+
+  """
+  rbd = _RecursiveFindBD(disk)
+  if rbd is None:
+    return rbd
+  sync_p, est_t, is_degr = rbd.GetSyncStatus()
+  return rbd.dev_path, rbd.major, rbd.minor, sync_p, est_t, is_degr
+
+
+def UploadFile(file_name, data, mode, uid, gid, atime, mtime):
+  """Write a file to the filesystem.
+
+  This allows the master to overwrite(!) a file. It will only perform
+  the operation if the file belongs to a list of configuration files.
+
+  """
+  if not os.path.isabs(file_name):
+    logger.Error("Filename passed to UploadFile is not absolute: '%s'" %
+                 file_name)
+    return False
+
+  if file_name not in [constants.CLUSTER_CONF_FILE, "/etc/hosts",
+                       "/etc/ssh/ssh_known_hosts"]:
+    logger.Error("Filename passed to UploadFile not in allowed"
+                 " upload targets: '%s'" % file_name)
+    return False
+
+  dir_name, small_name = os.path.split(file_name)
+  fd, new_name = tempfile.mkstemp('.new', small_name, dir_name)
+  # here we need to make sure we remove the temp file, if any error
+  # leaves it in place
+  try:
+    os.chown(new_name, uid, gid)
+    os.chmod(new_name, mode)
+    os.write(fd, data)
+    os.fsync(fd)
+    os.utime(new_name, (atime, mtime))
+    os.rename(new_name, file_name)
+  finally:
+    os.close(fd)
+    utils.RemoveFile(new_name)
+  return True
+
+def _ErrnoOrStr(err):
+  """Format an EnvironmentError exception.
+
+  If the `err` argument has an errno attribute, it will be looked up
+  and converted into a textual EXXXX description. Otherwise the string
+  representation of the error will be returned.
+
+  """
+  if hasattr(err, 'errno'):
+    detail = errno.errorcode[err.errno]
+  else:
+    detail = str(err)
+  return detail
+
+
+def _OSOndiskVersion(name, os_dir=None):
+  """Compute and return the api version of a given OS.
+
+  This function will try to read the api version of the os given by
+  the 'name' parameter. By default, it wil use the constants.OS_DIR
+  as top-level directory for OSes, but this can be overriden by the
+  use of the os_dir parameter. Return value will be either an
+  integer denoting the version or None in the case when this is not
+  a valid OS name.
+
+  """
+  if os_dir is None:
+    os_dir = os.path.sep.join([constants.OS_DIR, name])
+
+  api_file = os.path.sep.join([os_dir, "ganeti_api_version"])
+
+  try:
+    st = os.stat(api_file)
+  except EnvironmentError, err:
+    raise errors.InvalidOS, (name, "'ganeti_api_version' file not"
+                             " found (%s)" % _ErrnoOrStr(err))
+
+  if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
+    raise errors.InvalidOS, (name, "'ganeti_api_version' file is not"
+                             " a regular file")
+
+  try:
+    f = open(api_file)
+    try:
+      api_version = f.read(256)
+    finally:
+      f.close()
+  except EnvironmentError, err:
+    raise errors.InvalidOS, (name, "error while reading the"
+                             " API version (%s)" % _ErrnoOrStr(err))
+
+  api_version = api_version.strip()
+  try:
+    api_version = int(api_version)
+  except (TypeError, ValueError), err:
+    raise errors.InvalidOS, (name, "API version is not integer (%s)" %
+                             str(err))
+
+  return api_version
+
+def DiagnoseOS(top_dir=None):
+  """Compute the validity for all OSes.
+
+  For each name in the give top_dir parameter (if not given, defaults
+  to constants.OS_DIR), it will return an object. If this is a valid
+  os, the object will be an instance of the object.OS class. If not,
+  it will be an instance of errors.InvalidOS and this signifies that
+  this name does not correspond to a valid OS.
+
+  Returns:
+    list of objects
+
+  """
+  if top_dir is None:
+    top_dir = constants.OS_DIR
+
+  try:
+    f_names = os.listdir(top_dir)
+  except EnvironmentError, err:
+    logger.Error("Can't list the OS directory: %s" % str(err))
+    return False
+  result = []
+  for name in f_names:
+    try:
+      os_inst = OSFromDisk(name, os.path.sep.join([top_dir, name]))
+      result.append(os_inst)
+    except errors.InvalidOS, err:
+      result.append(err)
+
+  return result
+
+
+def OSFromDisk(name, os_dir=None):
+  """Create an OS instance from disk.
+
+  This function will return an OS instance if the given name is a
+  valid OS name. Otherwise, it will raise an appropriate
+  `errors.InvalidOS` exception, detailing why this is not a valid
+  OS.
+
+  """
+  if os_dir is None:
+    os_dir = os.path.sep.join([constants.OS_DIR, name])
+
+  api_version = _OSOndiskVersion(name, os_dir)
+
+  if api_version != constants.OS_API_VERSION:
+    raise errors.InvalidOS, (name, "API version mismatch (found %s want %s)"
+                             % (api_version, constants.OS_API_VERSION))
+
+  # OS Scripts dictionary, we will populate it with the actual script names
+  os_scripts = {'create': '', 'export': '', 'import': ''}
+
+  for script in os_scripts:
+    os_scripts[script] = os.path.sep.join([os_dir, script])
+
+    try:
+      st = os.stat(os_scripts[script])
+    except EnvironmentError, err:
+      raise errors.InvalidOS, (name, "'%s' script missing (%s)" %
+                               (script, _ErrnoOrStr(err)))
+
+    if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
+      raise errors.InvalidOS, (name, "'%s' script not executable" % script)
+
+    if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
+      raise errors.InvalidOS, (name, "'%s' is not a regular file" % script)
+
+
+  return objects.OS(name=name, path=os_dir,
+                    create_script=os_scripts['create'],
+                    export_script=os_scripts['export'],
+                    import_script=os_scripts['import'],
+                    api_version=api_version)
+
+
+def SnapshotBlockDevice(disk):
+  """Create a snapshot copy of a block device.
+
+  This function is called recursively, and the snapshot is actually created
+  just for the leaf lvm backend device.
+
+  Args:
+    disk: the disk to be snapshotted
+
+  Returns:
+    a config entry for the actual lvm device snapshotted.
+  """
+
+  if disk.children:
+    if len(disk.children) == 1:
+      # only one child, let's recurse on it
+      return SnapshotBlockDevice(disk.children[0])
+    else:
+      # more than one child, choose one that matches
+      for child in disk.children:
+        if child.size == disk.size:
+          # return implies breaking the loop
+          return SnapshotBlockDevice(child)
+  elif disk.dev_type == "lvm":
+    r_dev = _RecursiveFindBD(disk)
+    if r_dev is not None:
+      # let's stay on the safe side and ask for the full size, for now
+      return r_dev.Snapshot(disk.size)
+    else:
+      return None
+  else:
+    raise errors.ProgrammerError, ("Cannot snapshot non-lvm block device"
+                                   "'%s' of type '%s'" %
+                                   (disk.unique_id, disk.dev_type))
+
+
+def ExportSnapshot(disk, dest_node, instance):
+  """Export a block device snapshot to a remote node.
+
+  Args:
+    disk: the snapshot block device
+    dest_node: the node to send the image to
+    instance: instance being exported
+
+  Returns:
+    True if successful, False otherwise.
+  """
+
+  inst_os = OSFromDisk(instance.os)
+  export_script = inst_os.export_script
+
+  logfile = "%s/exp-%s-%s-%s.log" % (constants.LOG_OS_DIR, inst_os.name,
+                                     instance.name, int(time.time()))
+  if not os.path.exists(constants.LOG_OS_DIR):
+    os.mkdir(constants.LOG_OS_DIR, 0750)
+
+  real_os_dev = _RecursiveFindBD(disk)
+  if real_os_dev is None:
+    raise errors.BlockDeviceError("Block device '%s' is not set up" %
+                                  str(disk))
+  real_os_dev.Open()
+
+  destdir = os.path.join(constants.EXPORT_DIR, instance.name + ".new")
+  destfile = disk.physical_id[1]
+
+  # the target command is built out of three individual commands,
+  # which are joined by pipes; we check each individual command for
+  # valid parameters
+
+  expcmd = utils.BuildShellCmd("cd %s; %s -i %s -b %s 2>%s", inst_os.path,
+                               export_script, instance.name,
+                               real_os_dev.dev_path, logfile)
+
+  comprcmd = "gzip"
+
+  remotecmd = utils.BuildShellCmd("ssh -q -oStrictHostKeyChecking=yes"
+                                  " -oBatchMode=yes -oEscapeChar=none"
+                                  " %s 'mkdir -p %s; cat > %s/%s'",
+                                  dest_node, destdir, destdir, destfile)
+
+  # all commands have been checked, so we're safe to combine them
+  command = '|'.join([expcmd, comprcmd, remotecmd])
+
+  result = utils.RunCmd(command)
+
+  if result.failed:
+    logger.Error("os snapshot export command '%s' returned error: %s"
+                 " output: %s" %
+                 (command, result.fail_reason, result.output))
+    return False
+
+  return True
+
+
+def FinalizeExport(instance, snap_disks):
+  """Write out the export configuration information.
+
+  Args:
+    instance: instance configuration
+    snap_disks: snapshot block devices
+
+  Returns:
+    False in case of error, True otherwise.
+  """
+
+  destdir = os.path.join(constants.EXPORT_DIR, instance.name + ".new")
+  finaldestdir = os.path.join(constants.EXPORT_DIR, instance.name)
+
+  config = objects.SerializableConfigParser()
+
+  config.add_section(constants.INISECT_EXP)
+  config.set(constants.INISECT_EXP, 'version', '0')
+  config.set(constants.INISECT_EXP, 'timestamp', '%d' % int(time.time()))
+  config.set(constants.INISECT_EXP, 'source', instance.primary_node)
+  config.set(constants.INISECT_EXP, 'os', instance.os)
+  config.set(constants.INISECT_EXP, 'compression', 'gzip')
+
+  config.add_section(constants.INISECT_INS)
+  config.set(constants.INISECT_INS, 'name', instance.name)
+  config.set(constants.INISECT_INS, 'memory', '%d' % instance.memory)
+  config.set(constants.INISECT_INS, 'vcpus', '%d' % instance.vcpus)
+  config.set(constants.INISECT_INS, 'disk_template', instance.disk_template)
+  for nic_count, nic in enumerate(instance.nics):
+    config.set(constants.INISECT_INS, 'nic%d_mac' %
+               nic_count, '%s' % nic.mac)
+    config.set(constants.INISECT_INS, 'nic%d_ip' % nic_count, '%s' % nic.ip)
+  # TODO: redundant: on load can read nics until it doesn't exist
+  config.set(constants.INISECT_INS, 'nic_count' , '%d' % nic_count)
+
+  for disk_count, disk in enumerate(snap_disks):
+    config.set(constants.INISECT_INS, 'disk%d_ivname' % disk_count,
+               ('%s' % disk.iv_name))
+    config.set(constants.INISECT_INS, 'disk%d_dump' % disk_count,
+               ('%s' % disk.physical_id[1]))
+    config.set(constants.INISECT_INS, 'disk%d_size' % disk_count,
+               ('%d' % disk.size))
+  config.set(constants.INISECT_INS, 'disk_count' , '%d' % disk_count)
+
+  cff = os.path.join(destdir, constants.EXPORT_CONF_FILE)
+  cfo = open(cff, 'w')
+  try:
+    config.write(cfo)
+  finally:
+    cfo.close()
+
+  shutil.rmtree(finaldestdir, True)
+  shutil.move(destdir, finaldestdir)
+
+  return True
+
+
+def ExportInfo(dest):
+  """Get export configuration information.
+
+  Args:
+    dest: directory containing the export
+
+  Returns:
+    A serializable config file containing the export info.
+
+  """
+
+  cff = os.path.join(dest, constants.EXPORT_CONF_FILE)
+
+  config = objects.SerializableConfigParser()
+  config.read(cff)
+
+  if (not config.has_section(constants.INISECT_EXP) or
+      not config.has_section(constants.INISECT_INS)):
+    return None
+
+  return config
+
+
+def ImportOSIntoInstance(instance, os_disk, swap_disk, src_node, src_image):
+  """Import an os image into an instance.
+
+  Args:
+    instance: the instance object
+    os_disk: the instance-visible name of the os device
+    swap_disk: the instance-visible name of the swap device
+    src_node: node holding the source image
+    src_image: path to the source image on src_node
+
+  Returns:
+    False in case of error, True otherwise.
+
+  """
+
+  inst_os = OSFromDisk(instance.os)
+  import_script = inst_os.import_script
+
+  for os_device in instance.disks:
+    if os_device.iv_name == os_disk:
+      break
+  else:
+    logger.Error("Can't find this device-visible name '%s'" % os_disk)
+    return False
+
+  for swap_device in instance.disks:
+    if swap_device.iv_name == swap_disk:
+      break
+  else:
+    logger.Error("Can't find this device-visible name '%s'" % swap_disk)
+    return False
+
+  real_os_dev = _RecursiveFindBD(os_device)
+  if real_os_dev is None:
+    raise errors.BlockDeviceError, ("Block device '%s' is not set up" %
+                                    str(os_device))
+  real_os_dev.Open()
+
+  real_swap_dev = _RecursiveFindBD(swap_device)
+  if real_swap_dev is None:
+    raise errors.BlockDeviceError, ("Block device '%s' is not set up" %
+                                    str(swap_device))
+  real_swap_dev.Open()
+
+  logfile = "%s/import-%s-%s-%s.log" % (constants.LOG_OS_DIR, instance.os,
+                                        instance.name, int(time.time()))
+  if not os.path.exists(constants.LOG_OS_DIR):
+    os.mkdir(constants.LOG_OS_DIR, 0750)
+
+  remotecmd = utils.BuildShellCmd("ssh -q -oStrictHostKeyChecking=yes"
+                                  " -oBatchMode=yes -oEscapeChar=none"
+                                  " %s 'cat %s'", src_node, src_image)
+
+  comprcmd = "gunzip"
+  impcmd = utils.BuildShellCmd("(cd %s; %s -i %s -b %s -s %s &>%s)",
+                               inst_os.path, import_script, instance.name,
+                               real_os_dev.dev_path, real_swap_dev.dev_path,
+                               logfile)
+
+  command = '|'.join([remotecmd, comprcmd, impcmd])
+
+  result = utils.RunCmd(command)
+
+  if result.failed:
+    logger.Error("os import command '%s' returned error: %s"
+                 " output: %s" %
+                 (command, result.fail_reason, result.output))
+    return False
+
+  return True
+
+
+def ListExports():
+  """Return a list of exports currently available on this machine.
+  """
+  if os.path.isdir(constants.EXPORT_DIR):
+    return os.listdir(constants.EXPORT_DIR)
+  else:
+    return []
+
+
+def RemoveExport(export):
+  """Remove an existing export from the node.
+
+  Args:
+    export: the name of the export to remove
+
+  Returns:
+    False in case of error, True otherwise.
+  """
+
+  target = os.path.join(constants.EXPORT_DIR, export)
+
+  shutil.rmtree(target)
+  # TODO: catch some of the relevant exceptions and provide a pretty
+  # error message if rmtree fails.
+
+  return True
+
+
+class HooksRunner(object):
+  """Hook runner.
+
+  This class is instantiated on the node side (ganeti-noded) and not on
+  the master side.
+
+  """
+  RE_MASK = re.compile("^[a-zA-Z0-9_-]+$")
+
+  def __init__(self, hooks_base_dir=None):
+    """Constructor for hooks runner.
+
+    Args:
+      - hooks_base_dir: if not None, this overrides the
+        constants.HOOKS_BASE_DIR (useful for unittests)
+      - logs_base_dir: if not None, this overrides the
+        constants.LOG_HOOKS_DIR (useful for unittests)
+      - logging: enable or disable logging of script output
+
+    """
+    if hooks_base_dir is None:
+      hooks_base_dir = constants.HOOKS_BASE_DIR
+    self._BASE_DIR = hooks_base_dir
+
+  @staticmethod
+  def ExecHook(script, env):
+    """Exec one hook script.
+
+    Args:
+     - phase: the phase
+     - script: the full path to the script
+     - env: the environment with which to exec the script
+
+    """
+    # exec the process using subprocess and log the output
+    fdstdin = None
+    try:
+      fdstdin = open("/dev/null", "r")
+      child = subprocess.Popen([script], stdin=fdstdin, stdout=subprocess.PIPE,
+                               stderr=subprocess.STDOUT, close_fds=True,
+                               shell=False, cwd="/",env=env)
+      output = ""
+      try:
+        output = child.stdout.read(4096)
+        child.stdout.close()
+      except EnvironmentError, err:
+        output += "Hook script error: %s" % str(err)
+
+      while True:
+        try:
+          result = child.wait()
+          break
+        except EnvironmentError, err:
+          if err.errno == errno.EINTR:
+            continue
+          raise
+    finally:
+      # try not to leak fds
+      for fd in (fdstdin, ):
+        if fd is not None:
+          try:
+            fd.close()
+          except EnvironmentError, err:
+            # just log the error
+            #logger.Error("While closing fd %s: %s" % (fd, err))
+            pass
+
+    return result == 0, output
+
+  def RunHooks(self, hpath, phase, env):
+    """Run the scripts in the hooks directory.
+
+    This method will not be usually overriden by child opcodes.
+
+    """
+    if phase == constants.HOOKS_PHASE_PRE:
+      suffix = "pre"
+    elif phase == constants.HOOKS_PHASE_POST:
+      suffix = "post"
+    else:
+      raise errors.ProgrammerError, ("Unknown hooks phase: '%s'" % phase)
+    rr = []
+
+    subdir = "%s-%s.d" % (hpath, suffix)
+    dir_name = "%s/%s" % (self._BASE_DIR, subdir)
+    try:
+      dir_contents = os.listdir(dir_name)
+    except OSError, err:
+      # must log
+      return rr
+
+    # we use the standard python sort order,
+    # so 00name is the recommended naming scheme
+    dir_contents.sort()
+    for relname in dir_contents:
+      fname = os.path.join(dir_name, relname)
+      if not (os.path.isfile(fname) and os.access(fname, os.X_OK) and
+          self.RE_MASK.match(relname) is not None):
+        rrval = constants.HKR_SKIP
+        output = ""
+      else:
+        result, output = self.ExecHook(fname, env)
+        if not result:
+          rrval = constants.HKR_FAIL
+        else:
+          rrval = constants.HKR_SUCCESS
+      rr.append(("%s/%s" % (subdir, relname), rrval, output))
+
+    return rr
diff --git a/lib/bdev.py b/lib/bdev.py
new file mode 100644 (file)
index 0000000..d3ff77c
--- /dev/null
@@ -0,0 +1,1492 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2006, 2007 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.
+
+
+"""Block device abstraction"""
+
+import re
+import time
+import errno
+
+from ganeti import utils
+from ganeti import logger
+from ganeti import errors
+
+
+class BlockDev(object):
+  """Block device abstract class.
+
+  A block device can be in the following states:
+    - not existing on the system, and by `Create()` it goes into:
+    - existing but not setup/not active, and by `Assemble()` goes into:
+    - active read-write and by `Open()` it goes into
+    - online (=used, or ready for use)
+
+  A device can also be online but read-only, however we are not using
+  the readonly state (MD and LV have it, if needed in the future)
+  and we are usually looking at this like at a stack, so it's easier
+  to conceptualise the transition from not-existing to online and back
+  like a linear one.
+
+  The many different states of the device are due to the fact that we
+  need to cover many device types:
+    - logical volumes are created, lvchange -a y $lv, and used
+    - md arrays are created or assembled and used
+    - drbd devices are attached to a local disk/remote peer and made primary
+
+  The status of the device can be examined by `GetStatus()`, which
+  returns a numerical value, depending on the position in the
+  transition stack of the device.
+
+  A block device is identified by three items:
+    - the /dev path of the device (dynamic)
+    - a unique ID of the device (static)
+    - it's major/minor pair (dynamic)
+
+  Not all devices implement both the first two as distinct items. LVM
+  logical volumes have their unique ID (the pair volume group, logical
+  volume name) in a 1-to-1 relation to the dev path. For MD devices,
+  the /dev path is dynamic and the unique ID is the UUID generated at
+  array creation plus the slave list. For DRBD devices, the /dev path
+  is again dynamic and the unique id is the pair (host1, dev1),
+  (host2, dev2).
+
+  You can get to a device in two ways:
+    - creating the (real) device, which returns you
+      an attached instance (lvcreate, mdadm --create)
+    - attaching of a python instance to an existing (real) device
+
+  The second point, the attachement to a device, is different
+  depending on whether the device is assembled or not. At init() time,
+  we search for a device with the same unique_id as us. If found,
+  good. It also means that the device is already assembled. If not,
+  after assembly we'll have our correct major/minor.
+
+  """
+
+  STATUS_UNKNOWN = 0
+  STATUS_EXISTING = 1
+  STATUS_STANDBY = 2
+  STATUS_ONLINE = 3
+
+  STATUS_MAP = {
+    STATUS_UNKNOWN: "unknown",
+    STATUS_EXISTING: "existing",
+    STATUS_STANDBY: "ready for use",
+    STATUS_ONLINE: "online",
+    }
+
+
+  def __init__(self, unique_id, children):
+    self._children = children
+    self.dev_path = None
+    self.unique_id = unique_id
+    self.major = None
+    self.minor = None
+
+
+  def Assemble(self):
+    """Assemble the device from its components.
+
+    If this is a plain block device (e.g. LVM) than assemble does
+    nothing, as the LVM has no children and we don't put logical
+    volumes offline.
+
+    One guarantee is that after the device has been assembled, it
+    knows its major/minor numbers. This allows other devices (usually
+    parents) to probe correctly for their children.
+
+    """
+    status = True
+    for child in self._children:
+      if not isinstance(child, BlockDev):
+        raise TypeError("Invalid child passed of type '%s'" % type(child))
+      if not status:
+        break
+      status = status and child.Assemble()
+      if not status:
+        break
+      status = status and child.Open()
+
+    if not status:
+      for child in self._children:
+        child.Shutdown()
+    return status
+
+
+  def Attach(self):
+    """Find a device which matches our config and attach to it.
+
+    """
+    raise NotImplementedError
+
+
+  def Close(self):
+    """Notifies that the device will no longer be used for I/O.
+
+    """
+    raise NotImplementedError
+
+
+  @classmethod
+  def Create(cls, unique_id, children, size):
+    """Create the device.
+
+    If the device cannot be created, it will return None
+    instead. Error messages go to the logging system.
+
+    Note that for some devices, the unique_id is used, and for other,
+    the children. The idea is that these two, taken together, are
+    enough for both creation and assembly (later).
+
+    """
+    raise NotImplementedError
+
+
+  def Remove(self):
+    """Remove this device.
+
+    This makes sense only for some of the device types: LV and to a
+    lesser degree, md devices. Also note that if the device can't
+    attach, the removal can't be completed.
+
+    """
+    raise NotImplementedError
+
+
+  def GetStatus(self):
+    """Return the status of the device.
+
+    """
+    raise NotImplementedError
+
+
+  def Open(self, force=False):
+    """Make the device ready for use.
+
+    This makes the device ready for I/O. For now, just the DRBD
+    devices need this.
+
+    The force parameter signifies that if the device has any kind of
+    --force thing, it should be used, we know what we are doing.
+
+    """
+    raise NotImplementedError
+
+
+  def Shutdown(self):
+    """Shut down the device, freeing its children.
+
+    This undoes the `Assemble()` work, except for the child
+    assembling; as such, the children on the device are still
+    assembled after this call.
+
+    """
+    raise NotImplementedError
+
+
+  def SetSyncSpeed(self, speed):
+    """Adjust the sync speed of the mirror.
+
+    In case this is not a mirroring device, this is no-op.
+
+    """
+    result = True
+    if self._children:
+      for child in self._children:
+        result = result and child.SetSyncSpeed(speed)
+    return result
+
+
+  def GetSyncStatus(self):
+    """Returns the sync status of the device.
+
+    If this device is a mirroring device, this function returns the
+    status of the mirror.
+
+    Returns:
+     (sync_percent, estimated_time, is_degraded)
+
+    If sync_percent is None, it means all is ok
+    If estimated_time is None, it means we can't estimate
+    the time needed, otherwise it's the time left in seconds
+    If is_degraded is True, it means the device is missing
+    redundancy. This is usually a sign that something went wrong in
+    the device setup, if sync_percent is None.
+
+    """
+    return None, None, False
+
+
+  def CombinedSyncStatus(self):
+    """Calculate the mirror status recursively for our children.
+
+    The return value is the same as for `GetSyncStatus()` except the
+    minimum percent and maximum time are calculated across our
+    children.
+
+    """
+    min_percent, max_time, is_degraded = self.GetSyncStatus()
+    if self._children:
+      for child in self._children:
+        c_percent, c_time, c_degraded = child.GetSyncStatus()
+        if min_percent is None:
+          min_percent = c_percent
+        elif c_percent is not None:
+          min_percent = min(min_percent, c_percent)
+        if max_time is None:
+          max_time = c_time
+        elif c_time is not None:
+          max_time = max(max_time, c_time)
+        is_degraded = is_degraded or c_degraded
+    return min_percent, max_time, is_degraded
+
+
+  def __repr__(self):
+    return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
+            (self.__class__, self.unique_id, self._children,
+             self.major, self.minor, self.dev_path))
+
+
+class LogicalVolume(BlockDev):
+  """Logical Volume block device.
+
+  """
+  def __init__(self, unique_id, children):
+    """Attaches to a LV device.
+
+    The unique_id is a tuple (vg_name, lv_name)
+
+    """
+    super(LogicalVolume, self).__init__(unique_id, children)
+    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
+      raise ValueError("Invalid configuration data %s" % str(unique_id))
+    self._vg_name, self._lv_name = unique_id
+    self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
+    self.Attach()
+
+
+  @classmethod
+  def Create(cls, unique_id, children, size):
+    """Create a new logical volume.
+
+    """
+    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
+      raise ValueError("Invalid configuration data %s" % str(unique_id))
+    vg_name, lv_name = unique_id
+    pvs_info = cls.GetPVInfo(vg_name)
+    if not pvs_info:
+      raise errors.BlockDeviceError, ("Can't compute PV info for vg %s" %
+                                      vg_name)
+    pvs_info.sort()
+    pvs_info.reverse()
+    free_size, pv_name = pvs_info[0]
+    if free_size < size:
+      raise errors.BlockDeviceError, ("Not enough free space: required %s,"
+                                      " available %s" % (size, free_size))
+    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-n%s" % lv_name,
+                           vg_name, pv_name])
+    if result.failed:
+      raise errors.BlockDeviceError(result.fail_reason)
+    return LogicalVolume(unique_id, children)
+
+  @staticmethod
+  def GetPVInfo(vg_name):
+    """Get the free space info for PVs in a volume group.
+
+    Args:
+      vg_name: the volume group name
+
+    Returns:
+      list of (free_space, name) with free_space in mebibytes
+    """
+    command = ["pvs", "--noheadings", "--nosuffix", "--units=m",
+               "-opv_name,vg_name,pv_free,pv_attr", "--unbuffered",
+               "--separator=:"]
+    result = utils.RunCmd(command)
+    if result.failed:
+      logger.Error("Can't get the PV information: %s" % result.fail_reason)
+      return None
+    data = []
+    for line in result.stdout.splitlines():
+      fields = line.strip().split(':')
+      if len(fields) != 4:
+        logger.Error("Can't parse pvs output: line '%s'" % line)
+        return None
+      # skip over pvs from another vg or ones which are not allocatable
+      if fields[1] != vg_name or fields[3][0] != 'a':
+        continue
+      data.append((float(fields[2]), fields[0]))
+
+    return data
+
+  def Remove(self):
+    """Remove this logical volume.
+
+    """
+    if not self.minor and not self.Attach():
+      # the LV does not exist
+      return True
+    result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
+                           (self._vg_name, self._lv_name)])
+    if result.failed:
+      logger.Error("Can't lvremove: %s" % result.fail_reason)
+
+    return not result.failed
+
+
+  def Attach(self):
+    """Attach to an existing LV.
+
+    This method will try to see if an existing and active LV exists
+    which matches the our name. If so, its major/minor will be
+    recorded.
+
+    """
+    result = utils.RunCmd(["lvdisplay", self.dev_path])
+    if result.failed:
+      logger.Error("Can't find LV %s: %s" %
+                   (self.dev_path, result.fail_reason))
+      return False
+    match = re.compile("^ *Block device *([0-9]+):([0-9]+).*$")
+    for line in result.stdout.splitlines():
+      match_result = match.match(line)
+      if match_result:
+        self.major = int(match_result.group(1))
+        self.minor = int(match_result.group(2))
+        return True
+    return False
+
+
+  def Assemble(self):
+    """Assemble the device.
+
+    This is a no-op for the LV device type. Eventually, we could
+    lvchange -ay here if we see that the LV is not active.
+
+    """
+    return True
+
+
+  def Shutdown(self):
+    """Shutdown the device.
+
+    This is a no-op for the LV device type, as we don't deactivate the
+    volumes on shutdown.
+
+    """
+    return True
+
+
+  def GetStatus(self):
+    """Return the status of the device.
+
+    Logical volumes will can be in all four states, although we don't
+    deactivate (lvchange -an) them when shutdown, so STATUS_EXISTING
+    should not be seen for our devices.
+
+    """
+    result = utils.RunCmd(["lvs", "--noheadings", "-olv_attr", self.dev_path])
+    if result.failed:
+      logger.Error("Can't display lv: %s" % result.fail_reason)
+      return self.STATUS_UNKNOWN
+    out = result.stdout.strip()
+    # format: type/permissions/alloc/fixed_minor/state/open
+    if len(out) != 6:
+      return self.STATUS_UNKNOWN
+    #writable = (out[1] == "w")
+    active = (out[4] == "a")
+    online = (out[5] == "o")
+    if online:
+      retval = self.STATUS_ONLINE
+    elif active:
+      retval = self.STATUS_STANDBY
+    else:
+      retval = self.STATUS_EXISTING
+
+    return retval
+
+
+  def Open(self, force=False):
+    """Make the device ready for I/O.
+
+    This is a no-op for the LV device type.
+
+    """
+    return True
+
+
+  def Close(self):
+    """Notifies that the device will no longer be used for I/O.
+
+    This is a no-op for the LV device type.
+
+    """
+    return True
+
+
+  def Snapshot(self, size):
+    """Create a snapshot copy of an lvm block device.
+
+    """
+
+    snap_name = self._lv_name + ".snap"
+
+    # remove existing snapshot if found
+    snap = LogicalVolume((self._vg_name, snap_name), None)
+    snap.Remove()
+
+    pvs_info = self.GetPVInfo(self._vg_name)
+    if not pvs_info:
+      raise errors.BlockDeviceError, ("Can't compute PV info for vg %s" %
+                                      self._vg_name)
+    pvs_info.sort()
+    pvs_info.reverse()
+    free_size, pv_name = pvs_info[0]
+    if free_size < size:
+      raise errors.BlockDeviceError, ("Not enough free space: required %s,"
+                                      " available %s" % (size, free_size))
+
+    result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
+                           "-n%s" % snap_name, self.dev_path])
+    if result.failed:
+      raise errors.BlockDeviceError, ("command: %s error: %s" %
+                                      (result.cmd, result.fail_reason))
+
+    return snap_name
+
+
+class MDRaid1(BlockDev):
+  """raid1 device implemented via md.
+
+  """
+  def __init__(self, unique_id, children):
+    super(MDRaid1, self).__init__(unique_id, children)
+    self.major = 9
+    self.Attach()
+
+
+  def Attach(self):
+    """Find an array which matches our config and attach to it.
+
+    This tries to find a MD array which has the same UUID as our own.
+
+    """
+    minor = self._FindMDByUUID(self.unique_id)
+    if minor is not None:
+      self._SetFromMinor(minor)
+    else:
+      self.minor = None
+      self.dev_path = None
+
+    return (minor is not None)
+
+
+  @staticmethod
+  def _GetUsedDevs():
+    """Compute the list of in-use MD devices.
+
+    It doesn't matter if the used device have other raid level, just
+    that they are in use.
+
+    """
+    mdstat = open("/proc/mdstat", "r")
+    data = mdstat.readlines()
+    mdstat.close()
+
+    used_md = {}
+    valid_line = re.compile("^md([0-9]+) : .*$")
+    for line in data:
+      match = valid_line.match(line)
+      if match:
+        md_no = int(match.group(1))
+        used_md[md_no] = line
+
+    return used_md
+
+
+  @staticmethod
+  def _GetDevInfo(minor):
+    """Get info about a MD device.
+
+    Currently only uuid is returned.
+
+    """
+    result = utils.RunCmd(["mdadm", "-D", "/dev/md%d" % minor])
+    if result.failed:
+      logger.Error("Can't display md: %s" % result.fail_reason)
+      return None
+    retval = {}
+    for line in result.stdout.splitlines():
+      line = line.strip()
+      kv = line.split(" : ", 1)
+      if kv:
+        if kv[0] == "UUID":
+          retval["uuid"] = kv[1]
+        elif kv[0] == "State":
+          retval["state"] = kv[1].split(", ")
+    return retval
+
+
+  @staticmethod
+  def _FindUnusedMinor():
+    """Compute an unused MD minor.
+
+    This code assumes that there are 256 minors only.
+
+    """
+    used_md = MDRaid1._GetUsedDevs()
+    i = 0
+    while i < 256:
+      if i not in used_md:
+        break
+      i += 1
+    if i == 256:
+      logger.Error("Critical: Out of md minor numbers.")
+      return None
+    return i
+
+
+  @classmethod
+  def _FindMDByUUID(cls, uuid):
+    """Find the minor of an MD array with a given UUID.
+
+    """
+    md_list = cls._GetUsedDevs()
+    for minor in md_list:
+      info = cls._GetDevInfo(minor)
+      if info and info["uuid"] == uuid:
+        return minor
+    return None
+
+
+  @classmethod
+  def Create(cls, unique_id, children, size):
+    """Create a new MD raid1 array.
+
+    """
+    if not isinstance(children, (tuple, list)):
+      raise ValueError("Invalid setup data for MDRaid1 dev: %s" %
+                       str(children))
+    for i in children:
+      if not isinstance(i, BlockDev):
+        raise ValueError("Invalid member in MDRaid1 dev: %s" % type(i))
+    for i in children:
+      result = utils.RunCmd(["mdadm", "--zero-superblock", "--force",
+                             i.dev_path])
+      if result.failed:
+        logger.Error("Can't zero superblock: %s" % result.fail_reason)
+        return None
+    minor = cls._FindUnusedMinor()
+    result = utils.RunCmd(["mdadm", "--create", "/dev/md%d" % minor,
+                           "--auto=yes", "--force", "-l1",
+                           "-n%d" % len(children)] +
+                          [dev.dev_path for dev in children])
+
+    if result.failed:
+      logger.Error("Can't create md: %s" % result.fail_reason)
+      return None
+    info = cls._GetDevInfo(minor)
+    if not info or not "uuid" in info:
+      logger.Error("Wrong information returned from mdadm -D: %s" % str(info))
+      return None
+    return MDRaid1(info["uuid"], children)
+
+
+  def Remove(self):
+    """Stub remove function for MD RAID 1 arrays.
+
+    We don't remove the superblock right now. Mark a to do.
+
+    """
+    #TODO: maybe zero superblock on child devices?
+    return self.Shutdown()
+
+
+  def AddChild(self, device):
+    """Add a new member to the md raid1.
+
+    """
+    if self.minor is None and not self.Attach():
+      raise errors.BlockDeviceError, "Can't attach to device"
+    if device.dev_path is None:
+      raise errors.BlockDeviceError, "New child is not initialised"
+    result = utils.RunCmd(["mdadm", "-a", self.dev_path, device.dev_path])
+    if result.failed:
+      raise errors.BlockDeviceError, ("Failed to add new device to array: %s" %
+                                      result.output)
+    new_len = len(self._children) + 1
+    result = utils.RunCmd(["mdadm", "--grow", self.dev_path, "-n", new_len])
+    if result.failed:
+      raise errors.BlockDeviceError, ("Can't grow md array: %s" %
+                                      result.output)
+    self._children.append(device)
+
+
+  def RemoveChild(self, dev_path):
+    """Remove member from the md raid1.
+
+    """
+    if self.minor is None and not self.Attach():
+      raise errors.BlockDeviceError, "Can't attach to device"
+    if len(self._children) == 1:
+      raise errors.BlockDeviceError, ("Can't reduce member when only one"
+                                      " child left")
+    for device in self._children:
+      if device.dev_path == dev_path:
+        break
+    else:
+      raise errors.BlockDeviceError, "Can't find child with this path"
+    new_len = len(self._children) - 1
+    result = utils.RunCmd(["mdadm", "-f", self.dev_path, dev_path])
+    if result.failed:
+      raise errors.BlockDeviceError, ("Failed to mark device as failed: %s" %
+                                      result.output)
+
+    # it seems here we need a short delay for MD to update its
+    # superblocks
+    time.sleep(0.5)
+    result = utils.RunCmd(["mdadm", "-r", self.dev_path, dev_path])
+    if result.failed:
+      raise errors.BlockDeviceError, ("Failed to remove device from array:"
+                                      " %s" % result.output)
+    result = utils.RunCmd(["mdadm", "--grow", "--force", self.dev_path,
+                           "-n", new_len])
+    if result.failed:
+      raise errors.BlockDeviceError, ("Can't shrink md array: %s" %
+                                      result.output)
+    self._children.remove(device)
+
+
+  def GetStatus(self):
+    """Return the status of the device.
+
+    """
+    self.Attach()
+    if self.minor is None:
+      retval = self.STATUS_UNKNOWN
+    else:
+      retval = self.STATUS_ONLINE
+    return retval
+
+
+  def _SetFromMinor(self, minor):
+    """Set our parameters based on the given minor.
+
+    This sets our minor variable and our dev_path.
+
+    """
+    self.minor = minor
+    self.dev_path = "/dev/md%d" % minor
+
+
+  def Assemble(self):
+    """Assemble the MD device.
+
+    At this point we should have:
+      - list of children devices
+      - uuid
+
+    """
+    result = super(MDRaid1, self).Assemble()
+    if not result:
+      return result
+    md_list = self._GetUsedDevs()
+    for minor in md_list:
+      info = self._GetDevInfo(minor)
+      if info and info["uuid"] == self.unique_id:
+        self._SetFromMinor(minor)
+        logger.Info("MD array %s already started" % str(self))
+        return True
+    free_minor = self._FindUnusedMinor()
+    result = utils.RunCmd(["mdadm", "-A", "--auto=yes", "--uuid",
+                           self.unique_id, "/dev/md%d" % free_minor] +
+                          [bdev.dev_path for bdev in self._children])
+    if result.failed:
+      logger.Error("Can't assemble MD array: %s" % result.fail_reason)
+      self.minor = None
+    else:
+      self.minor = free_minor
+    return not result.failed
+
+
+  def Shutdown(self):
+    """Tear down the MD array.
+
+    This does a 'mdadm --stop' so after this command, the array is no
+    longer available.
+
+    """
+    if self.minor is None and not self.Attach():
+      logger.Info("MD object not attached to a device")
+      return True
+
+    result = utils.RunCmd(["mdadm", "--stop", "/dev/md%d" % self.minor])
+    if result.failed:
+      logger.Error("Can't stop MD array: %s" % result.fail_reason)
+      return False
+    self.minor = None
+    self.dev_path = None
+    return True
+
+
+  def SetSyncSpeed(self, kbytes):
+    """Set the maximum sync speed for the MD array.
+
+    """
+    result = super(MDRaid1, self).SetSyncSpeed(kbytes)
+    if self.minor is None:
+      logger.Error("MD array not attached to a device")
+      return False
+    f = open("/sys/block/md%d/md/sync_speed_max" % self.minor, "w")
+    try:
+      f.write("%d" % kbytes)
+    finally:
+      f.close()
+    f = open("/sys/block/md%d/md/sync_speed_min" % self.minor, "w")
+    try:
+      f.write("%d" % (kbytes/2))
+    finally:
+      f.close()
+    return result
+
+
+  def GetSyncStatus(self):
+    """Returns the sync status of the device.
+
+    Returns:
+     (sync_percent, estimated_time)
+
+    If sync_percent is None, it means all is ok
+    If estimated_time is None, it means we can't esimate
+    the time needed, otherwise it's the time left in seconds
+
+    """
+    if self.minor is None and not self.Attach():
+      raise errors.BlockDeviceError("Can't attach to device in GetSyncStatus")
+    dev_info = self._GetDevInfo(self.minor)
+    is_clean = ("state" in dev_info and
+                len(dev_info["state"]) == 1 and
+                dev_info["state"][0] in ("clean", "active"))
+    sys_path = "/sys/block/md%s/md/" % self.minor
+    f = file(sys_path + "sync_action")
+    sync_status = f.readline().strip()
+    f.close()
+    if sync_status == "idle":
+      return None, None, not is_clean
+    f = file(sys_path + "sync_completed")
+    sync_completed = f.readline().strip().split(" / ")
+    f.close()
+    if len(sync_completed) != 2:
+      return 0, None, not is_clean
+    sync_done, sync_total = [float(i) for i in sync_completed]
+    sync_percent = 100.0*sync_done/sync_total
+    f = file(sys_path + "sync_speed")
+    sync_speed_k = int(f.readline().strip())
+    if sync_speed_k == 0:
+      time_est = None
+    else:
+      time_est = (sync_total - sync_done) / 2 / sync_speed_k
+    return sync_percent, time_est, not is_clean
+
+
+  def Open(self, force=False):
+    """Make the device ready for I/O.
+
+    This is a no-op for the MDRaid1 device type, although we could use
+    the 2.6.18's new array_state thing.
+
+    """
+    return True
+
+
+  def Close(self):
+    """Notifies that the device will no longer be used for I/O.
+
+    This is a no-op for the MDRaid1 device type, but see comment for
+    `Open()`.
+
+    """
+    return True
+
+
+
+class DRBDev(BlockDev):
+  """DRBD block device.
+
+  This implements the local host part of the DRBD device, i.e. it
+  doesn't do anything to the supposed peer. If you need a fully
+  connected DRBD pair, you need to use this class on both hosts.
+
+  The unique_id for the drbd device is the (local_ip, local_port,
+  remote_ip, remote_port) tuple, and it must have two children: the
+  data device and the meta_device. The meta device is checked for
+  valid size and is zeroed on create.
+
+  """
+  _DRBD_MAJOR = 147
+  _ST_UNCONFIGURED = "Unconfigured"
+  _ST_WFCONNECTION = "WFConnection"
+  _ST_CONNECTED = "Connected"
+
+  def __init__(self, unique_id, children):
+    super(DRBDev, self).__init__(unique_id, children)
+    self.major = self._DRBD_MAJOR
+    if len(children) != 2:
+      raise ValueError("Invalid configuration data %s" % str(children))
+    if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 4:
+      raise ValueError("Invalid configuration data %s" % str(unique_id))
+    self._lhost, self._lport, self._rhost, self._rport = unique_id
+    self.Attach()
+
+  @staticmethod
+  def _DevPath(minor):
+    """Return the path to a drbd device for a given minor.
+
+    """
+    return "/dev/drbd%d" % minor
+
+  @staticmethod
+  def _GetProcData():
+    """Return data from /proc/drbd.
+
+    """
+    stat = open("/proc/drbd", "r")
+    data = stat.read().splitlines()
+    stat.close()
+    return data
+
+
+  @classmethod
+  def _GetUsedDevs(cls):
+    """Compute the list of used DRBD devices.
+
+    """
+    data = cls._GetProcData()
+
+    used_devs = {}
+    valid_line = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
+    for line in data:
+      match = valid_line.match(line)
+      if not match:
+        continue
+      minor = int(match.group(1))
+      state = match.group(2)
+      if state == cls._ST_UNCONFIGURED:
+        continue
+      used_devs[minor] = state, line
+
+    return used_devs
+
+
+  @classmethod
+  def _FindUnusedMinor(cls):
+    """Find an unused DRBD device.
+
+    """
+    data = cls._GetProcData()
+
+    valid_line = re.compile("^ *([0-9]+): cs:Unconfigured$")
+    for line in data:
+      match = valid_line.match(line)
+      if match:
+        return int(match.group(1))
+    logger.Error("Error: no free drbd minors!")
+    return None
+
+
+  @classmethod
+  def _GetDevInfo(cls, minor):
+    """Get details about a given DRBD minor.
+
+    This return, if available, the local backing device in (major,
+    minor) formant and the local and remote (ip, port) information.
+
+    """
+    data = {}
+    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
+    if result.failed:
+      logger.Error("Can't display the drbd config: %s" % result.fail_reason)
+      return data
+    out = result.stdout
+    if out == "Not configured\n":
+      return data
+    for line in out.splitlines():
+      if "local_dev" not in data:
+        match = re.match("^Lower device: ([0-9]+):([0-9]+) .*$", line)
+        if match:
+          data["local_dev"] = (int(match.group(1)), int(match.group(2)))
+          continue
+      if "meta_dev" not in data:
+        match = re.match("^Meta device: (([0-9]+):([0-9]+)|internal).*$", line)
+        if match:
+          if match.group(2) is not None and match.group(3) is not None:
+            # matched on the major/minor
+            data["meta_dev"] = (int(match.group(2)), int(match.group(3)))
+          else:
+            # matched on the "internal" string
+            data["meta_dev"] = match.group(1)
+            # in this case, no meta_index is in the output
+            data["meta_index"] = -1
+          continue
+      if "meta_index" not in data:
+        match = re.match("^Meta index: ([0-9]+).*$", line)
+        if match:
+          data["meta_index"] = int(match.group(1))
+          continue
+      if "local_addr" not in data:
+        match = re.match("^Local address: ([0-9.]+):([0-9]+)$", line)
+        if match:
+          data["local_addr"] = (match.group(1), int(match.group(2)))
+          continue
+      if "remote_addr" not in data:
+        match = re.match("^Remote address: ([0-9.]+):([0-9]+)$", line)
+        if match:
+          data["remote_addr"] = (match.group(1), int(match.group(2)))
+          continue
+    return data
+
+
+  def _MatchesLocal(self, info):
+    """Test if our local config matches with an existing device.
+
+    The parameter should be as returned from `_GetDevInfo()`. This
+    method tests if our local backing device is the same as the one in
+    the info parameter, in effect testing if we look like the given
+    device.
+
+    """
+    if not ("local_dev" in info and "meta_dev" in info and
+            "meta_index" in info):
+      return False
+
+    backend = self._children[0]
+    if backend is not None:
+      retval = (info["local_dev"] == (backend.major, backend.minor))
+    else:
+      retval = (info["local_dev"] == (0, 0))
+    meta = self._children[1]
+    if meta is not None:
+      retval = retval and (info["meta_dev"] == (meta.major, meta.minor))
+      retval = retval and (info["meta_index"] == 0)
+    else:
+      retval = retval and (info["meta_dev"] == "internal" and
+                           info["meta_index"] == -1)
+    return retval
+
+
+  def _MatchesNet(self, info):
+    """Test if our network config matches with an existing device.
+
+    The parameter should be as returned from `_GetDevInfo()`. This
+    method tests if our network configuration is the same as the one
+    in the info parameter, in effect testing if we look like the given
+    device.
+
+    """
+    if (((self._lhost is None and not ("local_addr" in info)) and
+         (self._rhost is None and not ("remote_addr" in info)))):
+      return True
+
+    if self._lhost is None:
+      return False
+
+    if not ("local_addr" in info and
+            "remote_addr" in info):
+      return False
+
+    retval = (info["local_addr"] == (self._lhost, self._lport))
+    retval = (retval and
+              info["remote_addr"] == (self._rhost, self._rport))
+    return retval
+
+
+  @staticmethod
+  def _IsValidMeta(meta_device):
+    """Check if the given meta device looks like a valid one.
+
+    This currently only check the size, which must be around
+    128MiB.
+
+    """
+    result = utils.RunCmd(["blockdev", "--getsize", meta_device])
+    if result.failed:
+      logger.Error("Failed to get device size: %s" % result.fail_reason)
+      return False
+    try:
+      sectors = int(result.stdout)
+    except ValueError:
+      logger.Error("Invalid output from blockdev: '%s'" % result.stdout)
+      return False
+    bytes = sectors * 512
+    if bytes < 128*1024*1024: # less than 128MiB
+      logger.Error("Meta device too small (%.2fMib)" % (bytes/1024/1024))
+      return False
+    if bytes > (128+32)*1024*1024: # account for an extra (big) PE on LVM
+      logger.Error("Meta device too big (%.2fMiB)" % (bytes/1024/1024))
+      return False
+    return True
+
+
+  @classmethod
+  def _AssembleLocal(cls, minor, backend, meta):
+    """Configure the local part of a DRBD device.
+
+    This is the first thing that must be done on an unconfigured DRBD
+    device. And it must be done only once.
+
+    """
+    if not cls._IsValidMeta(meta):
+      return False
+    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disk",
+                           backend, meta, "0", "-e", "detach"])
+    if result.failed:
+      logger.Error("Can't attach local disk: %s" % result.output)
+    return not result.failed
+
+
+  @classmethod
+  def _ShutdownLocal(cls, minor):
+    """Detach from the local device.
+
+    I/Os will continue to be served from the remote device. If we
+    don't have a remote device, this operation will fail.
+
+    """
+    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
+    if result.failed:
+      logger.Error("Can't detach local device: %s" % result.output)
+    return not result.failed
+
+
+  @staticmethod
+  def _ShutdownAll(minor):
+    """Deactivate the device.
+
+    This will, of course, fail if the device is in use.
+
+    """
+    result = utils.RunCmd(["drbdsetup", DRBDev._DevPath(minor), "down"])
+    if result.failed:
+      logger.Error("Can't shutdown drbd device: %s" % result.output)
+    return not result.failed
+
+
+  @classmethod
+  def _AssembleNet(cls, minor, net_info, protocol):
+    """Configure the network part of the device.
+
+    This operation can be, in theory, done multiple times, but there
+    have been cases (in lab testing) in which the network part of the
+    device had become stuck and couldn't be shut down because activity
+    from the new peer (also stuck) triggered a timer re-init and
+    needed remote peer interface shutdown in order to clear. So please
+    don't change online the net config.
+
+    """
+    lhost, lport, rhost, rport = net_info
+    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "net",
+                           "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport),
+                           protocol])
+    if result.failed:
+      logger.Error("Can't setup network for dbrd device: %s" %
+                   result.fail_reason)
+      return False
+
+    timeout = time.time() + 10
+    ok = False
+    while time.time() < timeout:
+      info = cls._GetDevInfo(minor)
+      if not "local_addr" in info or not "remote_addr" in info:
+        time.sleep(1)
+        continue
+      if (info["local_addr"] != (lhost, lport) or
+          info["remote_addr"] != (rhost, rport)):
+        time.sleep(1)
+        continue
+      ok = True
+      break
+    if not ok:
+      logger.Error("Timeout while configuring network")
+      return False
+    return True
+
+
+  @classmethod
+  def _ShutdownNet(cls, minor):
+    """Disconnect from the remote peer.
+
+    This fails if we don't have a local device.
+
+    """
+    result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
+    logger.Error("Can't shutdown network: %s" % result.output)
+    return not result.failed
+
+
+  def _SetFromMinor(self, minor):
+    """Set our parameters based on the given minor.
+
+    This sets our minor variable and our dev_path.
+
+    """
+    if minor is None:
+      self.minor = self.dev_path = None
+    else:
+      self.minor = minor
+      self.dev_path = self._DevPath(minor)
+
+
+  def Assemble(self):
+    """Assemble the drbd.
+
+    Method:
+      - if we have a local backing device, we bind to it by:
+        - checking the list of used drbd devices
+        - check if the local minor use of any of them is our own device
+        - if yes, abort?
+        - if not, bind
+      - if we have a local/remote net info:
+        - redo the local backing device step for the remote device
+        - check if any drbd device is using the local port,
+          if yes abort
+        - check if any remote drbd device is using the remote
+          port, if yes abort (for now)
+        - bind our net port
+        - bind the remote net port
+
+    """
+    self.Attach()
+    if self.minor is not None:
+      logger.Info("Already assembled")
+      return True
+
+    result = super(DRBDev, self).Assemble()
+    if not result:
+      return result
+
+    minor = self._FindUnusedMinor()
+    if minor is None:
+      raise errors.BlockDeviceError, "Not enough free minors for DRBD!"
+    need_localdev_teardown = False
+    if self._children[0]:
+      result = self._AssembleLocal(minor, self._children[0].dev_path,
+                                   self._children[1].dev_path)
+      if not result:
+        return False
+      need_localdev_teardown = True
+    if self._lhost and self._lport and self._rhost and self._rport:
+      result = self._AssembleNet(minor,
+                                 (self._lhost, self._lport,
+                                  self._rhost, self._rport),
+                                 "C")
+      if not result:
+        if need_localdev_teardown:
+          # we will ignore failures from this
+          logger.Error("net setup failed, tearing down local device")
+          self._ShutdownAll(minor)
+        return False
+    self._SetFromMinor(minor)
+    return True
+
+
+  def Shutdown(self):
+    """Shutdown the DRBD device.
+
+    """
+    if self.minor is None and not self.Attach():
+      logger.Info("DRBD device not attached to a device during Shutdown")
+      return True
+    if not self._ShutdownAll(self.minor):
+      return False
+    self.minor = None
+    self.dev_path = None
+    return True
+
+
+  def Attach(self):
+    """Find a DRBD device which matches our config and attach to it.
+
+    In case of partially attached (local device matches but no network
+    setup), we perform the network attach. If successful, we re-test
+    the attach if can return success.
+
+    """
+    for minor in self._GetUsedDevs():
+      info = self._GetDevInfo(minor)
+      match_l = self._MatchesLocal(info)
+      match_r = self._MatchesNet(info)
+      if match_l and match_r:
+        break
+      if match_l and not match_r and "local_addr" not in info:
+        res_r = self._AssembleNet(minor,
+                                  (self._lhost, self._lport,
+                                   self._rhost, self._rport),
+                                  "C")
+        if res_r and self._MatchesNet(self._GetDevInfo(minor)):
+          break
+    else:
+      minor = None
+
+    self._SetFromMinor(minor)
+    return minor is not None
+
+
+  def Open(self, force=False):
+    """Make the local state primary.
+
+    If the 'force' parameter is given, the '--do-what-I-say' parameter
+    is given. Since this is a pottentialy dangerous operation, the
+    force flag should be only given after creation, when it actually
+    has to be given.
+
+    """
+    if self.minor is None and not self.Attach():
+      logger.Error("DRBD cannot attach to a device during open")
+      return False
+    cmd = ["drbdsetup", self.dev_path, "primary"]
+    if force:
+      cmd.append("--do-what-I-say")
+    result = utils.RunCmd(cmd)
+    if result.failed:
+      logger.Error("Can't make drbd device primary: %s" % result.output)
+      return False
+    return True
+
+
+  def Close(self):
+    """Make the local state secondary.
+
+    This will, of course, fail if the device is in use.
+
+    """
+    if self.minor is None and not self.Attach():
+      logger.Info("Instance not attached to a device")
+      raise errors.BlockDeviceError("Can't find device")
+    result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
+    if result.failed:
+      logger.Error("Can't switch drbd device to secondary: %s" % result.output)
+      raise errors.BlockDeviceError("Can't switch drbd device to secondary")
+
+
+  def SetSyncSpeed(self, kbytes):
+    """Set the speed of the DRBD syncer.
+
+    """
+    children_result = super(DRBDev, self).SetSyncSpeed(kbytes)
+    if self.minor is None:
+      logger.Info("Instance not attached to a device")
+      return False
+    result = utils.RunCmd(["drbdsetup", self.dev_path, "syncer", "-r", "%d" %
+                           kbytes])
+    if result.failed:
+      logger.Error("Can't change syncer rate: %s " % result.fail_reason)
+    return not result.failed and children_result
+
+
+  def GetSyncStatus(self):
+    """Returns the sync status of the device.
+
+    Returns:
+     (sync_percent, estimated_time)
+
+    If sync_percent is None, it means all is ok
+    If estimated_time is None, it means we can't esimate
+    the time needed, otherwise it's the time left in seconds
+
+    """
+    if self.minor is None and not self.Attach():
+      raise errors.BlockDeviceError("Can't attach to device in GetSyncStatus")
+    proc_info = self._MassageProcData(self._GetProcData())
+    if self.minor not in proc_info:
+      raise errors.BlockDeviceError("Can't find myself in /proc (minor %d)" %
+                                    self.minor)
+    line = proc_info[self.minor]
+    match = re.match("^.*sync'ed: *([0-9.]+)%.*"
+                     " finish: ([0-9]+):([0-9]+):([0-9]+) .*$", line)
+    if match:
+      sync_percent = float(match.group(1))
+      hours = int(match.group(2))
+      minutes = int(match.group(3))
+      seconds = int(match.group(4))
+      est_time = hours * 3600 + minutes * 60 + seconds
+    else:
+      sync_percent = None
+      est_time = None
+    match = re.match("^ *[0-9]+: cs:([^ ]+).*$", line)
+    if not match:
+      raise errors.BlockDeviceError("Can't find my data in /proc (minor %d)" %
+                                    self.minor)
+    client_state = match.group(1)
+    is_degraded = client_state != "Connected"
+    return sync_percent, est_time, is_degraded
+
+
+  @staticmethod
+  def _MassageProcData(data):
+    """Transform the output of _GetProdData into a nicer form.
+
+    Returns:
+      a dictionary of minor: joined lines from /proc/drbd for that minor
+
+    """
+    lmatch = re.compile("^ *([0-9]+):.*$")
+    results = {}
+    old_minor = old_line = None
+    for line in data:
+      lresult = lmatch.match(line)
+      if lresult is not None:
+        if old_minor is not None:
+          results[old_minor] = old_line
+        old_minor = int(lresult.group(1))
+        old_line = line
+      else:
+        if old_minor is not None:
+          old_line += " " + line.strip()
+    # add last line
+    if old_minor is not None:
+      results[old_minor] = old_line
+    return results
+
+
+  def GetStatus(self):
+    """Compute the status of the DRBD device
+
+    Note that DRBD devices don't have the STATUS_EXISTING state.
+
+    """
+    if self.minor is None and not self.Attach():
+      return self.STATUS_UNKNOWN
+
+    data = self._GetProcData()
+    match = re.compile("^ *%d: cs:[^ ]+ st:(Primary|Secondary)/.*$" %
+                       self.minor)
+    for line in data:
+      mresult = match.match(line)
+      if mresult:
+        break
+    else:
+      logger.Error("Can't find myself!")
+      return self.STATUS_UNKNOWN
+
+    state = mresult.group(2)
+    if state == "Primary":
+      result = self.STATUS_ONLINE
+    else:
+      result = self.STATUS_STANDBY
+
+    return result
+
+
+  @staticmethod
+  def _ZeroDevice(device):
+    """Zero a device.
+
+    This writes until we get ENOSPC.
+
+    """
+    f = open(device, "w")
+    buf = "\0" * 1048576
+    try:
+      while True:
+        f.write(buf)
+    except IOError, err:
+      if err.errno != errno.ENOSPC:
+        raise
+
+
+  @classmethod
+  def Create(cls, unique_id, children, size):
+    """Create a new DRBD device.
+
+    Since DRBD devices are not created per se, just assembled, this
+    function just zeroes the meta device.
+
+    """
+    if len(children) != 2:
+      raise errors.ProgrammerError("Invalid setup for the drbd device")
+    meta = children[1]
+    meta.Assemble()
+    if not meta.Attach():
+      raise errors.BlockDeviceError("Can't attach to meta device")
+    if not cls._IsValidMeta(meta.dev_path):
+      raise errors.BlockDeviceError("Invalid meta device")
+    logger.Info("Started zeroing device %s" % meta.dev_path)
+    cls._ZeroDevice(meta.dev_path)
+    logger.Info("Done zeroing device %s" % meta.dev_path)
+    return cls(unique_id, children)
+
+
+  def Remove(self):
+    """Stub remove for DRBD devices.
+
+    """
+    return self.Shutdown()
+
+
+DEV_MAP = {
+  "lvm": LogicalVolume,
+  "md_raid1": MDRaid1,
+  "drbd": DRBDev,
+  }
+
+
+def FindDevice(dev_type, unique_id, children):
+  """Search for an existing, assembled device.
+
+  This will succeed only if the device exists and is assembled, but it
+  does not do any actions in order to activate the device.
+
+  """
+  if dev_type not in DEV_MAP:
+    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
+  device = DEV_MAP[dev_type](unique_id, children)
+  if not device.Attach():
+    return None
+  return  device
+
+
+def AttachOrAssemble(dev_type, unique_id, children):
+  """Try to attach or assemble an existing device.
+
+  This will attach to an existing assembled device or will assemble
+  the device, as needed, to bring it fully up.
+
+  """
+  if dev_type not in DEV_MAP:
+    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
+  device = DEV_MAP[dev_type](unique_id, children)
+  if not device.Attach():
+    device.Assemble()
+  if not device.Attach():
+    raise errors.BlockDeviceError("Can't find a valid block device for"
+                                  " %s/%s/%s" %
+                                  (dev_type, unique_id, children))
+  return device
+
+
+def Create(dev_type, unique_id, children, size):
+  """Create a device.
+
+  """
+  if dev_type not in DEV_MAP:
+    raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
+  device = DEV_MAP[dev_type].Create(unique_id, children, size)
+  return device
diff --git a/lib/cli.py b/lib/cli.py
new file mode 100644 (file)
index 0000000..56ec477
--- /dev/null
@@ -0,0 +1,272 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2006, 2007 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 dealing with command line parsing"""
+
+
+import sys
+import textwrap
+import os.path
+import copy
+
+from ganeti import utils
+from ganeti import logger
+from ganeti import errors
+from ganeti import mcpu
+from ganeti import constants
+
+from optparse import (OptionParser, make_option, TitledHelpFormatter,
+                      Option, OptionValueError, SUPPRESS_HELP)
+
+__all__ = ["DEBUG_OPT", "NOHDR_OPT", "SEP_OPT", "GenericMain", "SubmitOpCode",
+           "cli_option",
+           "ARGS_NONE", "ARGS_FIXED", "ARGS_ATLEAST", "ARGS_ANY", "ARGS_ONE",
+           "USEUNITS_OPT"]
+
+DEBUG_OPT = make_option("-d", "--debug", default=False,
+                        action="store_true",
+                        help="Turn debugging on")
+
+NOHDR_OPT = make_option("--no-headers", default=False,
+                        action="store_true", dest="no_headers",
+                        help="Don't display column headers")
+
+SEP_OPT = make_option("--separator", default=" ",
+                      action="store", dest="separator",
+                      help="Separator between output fields"
+                      " (defaults to one space)")
+
+USEUNITS_OPT = make_option("--human-readable", default=False,
+                           action="store_true", dest="human_readable",
+                           help="Print sizes in human readable format")
+
+_LOCK_OPT = make_option("--lock-retries", default=None,
+                        type="int", help=SUPPRESS_HELP)
+
+
+def ARGS_FIXED(val):
+  """Macro-like function denoting a fixed number of arguments"""
+  return -val
+
+
+def ARGS_ATLEAST(val):
+  """Macro-like function denoting a minimum number of arguments"""
+  return val
+
+
+ARGS_NONE = None
+ARGS_ONE = ARGS_FIXED(1)
+ARGS_ANY = ARGS_ATLEAST(0)
+
+
+def check_unit(option, opt, value):
+  try:
+    return utils.ParseUnit(value)
+  except errors.UnitParseError, err:
+    raise OptionValueError, ("option %s: %s" % (opt, err))
+
+
+class CliOption(Option):
+  TYPES = Option.TYPES + ("unit",)
+  TYPE_CHECKER = copy.copy(Option.TYPE_CHECKER)
+  TYPE_CHECKER["unit"] = check_unit
+
+
+# optparse.py sets make_option, so we do it for our own option class, too
+cli_option = CliOption
+
+
+def _ParseArgs(argv, commands):
+  """Parses the command line and return the function which must be
+  executed together with its arguments
+
+  Arguments:
+    argv: the command line
+
+    commands: dictionary with special contents, see the design doc for
+    cmdline handling
+  """
+  if len(argv) == 0:
+    binary = "<command>"
+  else:
+    binary = argv[0].split("/")[-1]
+
+  if len(argv) > 1 and argv[1] == "--version":
+    print "%s (ganeti) %s" % (binary, constants.RELEASE_VERSION)
+    # Quit right away. That way we don't have to care about this special
+    # argument. optparse.py does it the same.
+    sys.exit(0)
+
+  if len(argv) < 2 or argv[1] not in commands.keys():
+    # let's do a nice thing
+    sortedcmds = commands.keys()
+    sortedcmds.sort()
+    print ("Usage: %(bin)s {command} [options...] [argument...]"
+           "\n%(bin)s <command> --help to see details, or"
+           " man %(bin)s\n" % {"bin": binary})
+    # compute the max line length for cmd + usage
+    mlen = max([len(" %s %s" % (cmd, commands[cmd][3])) for cmd in commands])
+    mlen = min(60, mlen) # should not get here...
+    # and format a nice command list
+    print "Commands:"
+    for cmd in sortedcmds:
+      cmdstr = " %s %s" % (cmd, commands[cmd][3])
+      help_text = commands[cmd][4]
+      help_lines = textwrap.wrap(help_text, 79-3-mlen)
+      print "%-*s - %s" % (mlen, cmdstr,
+                                          help_lines.pop(0))
+      for line in help_lines:
+        print "%-*s   %s" % (mlen, "", line)
+    print
+    return None, None, None
+  cmd = argv.pop(1)
+  func, nargs, parser_opts, usage, description = commands[cmd]
+  parser_opts.append(_LOCK_OPT)
+  parser = OptionParser(option_list=parser_opts,
+                        description=description,
+                        formatter=TitledHelpFormatter(),
+                        usage="%%prog %s %s" % (cmd, usage))
+  parser.disable_interspersed_args()
+  options, args = parser.parse_args()
+  if nargs is None:
+    if len(args) != 0:
+      print >> sys.stderr, ("Error: Command %s expects no arguments" % cmd)
+      return None, None, None
+  elif nargs < 0 and len(args) != -nargs:
+    print >> sys.stderr, ("Error: Command %s expects %d argument(s)" %
+                         (cmd, -nargs))
+    return None, None, None
+  elif nargs >= 0 and len(args) < nargs:
+    print >> sys.stderr, ("Error: Command %s expects at least %d argument(s)" %
+                         (cmd, nargs))
+    return None, None, None
+
+  return func, options, args
+
+
+def _AskUser(text):
+  """Ask the user a yes/no question.
+
+  Args:
+    questionstring - the question to ask.
+
+  Returns:
+    True or False depending on answer (No for False is default).
+
+  """
+  try:
+    f = file("/dev/tty", "r+")
+  except IOError:
+    return False
+  answer = False
+  try:
+    f.write(textwrap.fill(text))
+    f.write('\n')
+    f.write("y/[n]: ")
+    line = f.readline(16).strip().lower()
+    answer = line in ('y', 'yes')
+  finally:
+    f.close()
+  return answer
+
+
+def SubmitOpCode(op):
+  """Function to submit an opcode.
+
+  This is just a simple wrapper over the construction of the processor
+  instance. It should be extended to better handle feedback and
+  interaction functions.
+
+  """
+  proc = mcpu.Processor()
+  return proc.ExecOpCode(op, logger.ToStdout)
+
+
+def GenericMain(commands):
+  """Generic main function for all the gnt-* commands.
+
+  Argument: a dictionary with a special structure, see the design doc
+  for command line handling.
+
+  """
+  # save the program name and the entire command line for later logging
+  if sys.argv:
+    binary = os.path.basename(sys.argv[0]) or sys.argv[0]
+    if len(sys.argv) >= 2:
+      binary += " " + sys.argv[1]
+      old_cmdline = " ".join(sys.argv[2:])
+    else:
+      old_cmdline = ""
+  else:
+    binary = "<unknown program>"
+    old_cmdline = ""
+
+  func, options, args = _ParseArgs(sys.argv, commands)
+  if func is None: # parse error
+    return 1
+
+  options._ask_user = _AskUser
+
+  logger.SetupLogging(debug=options.debug, program=binary)
+
+  try:
+    utils.Lock('cmd', max_retries=options.lock_retries, debug=options.debug)
+  except errors.LockError, err:
+    logger.ToStderr(str(err))
+    return 1
+
+  if old_cmdline:
+    logger.Info("run with arguments '%s'" % old_cmdline)
+  else:
+    logger.Info("run with no arguments")
+
+  try:
+    try:
+      result = func(options, args)
+    except errors.ConfigurationError, err:
+      logger.Error("Corrupt configuration file: %s" % err)
+      logger.ToStderr("Aborting.")
+      result = 2
+    except errors.HooksAbort, err:
+      logger.ToStderr("Failure: hooks execution failed:")
+      for node, script, out in err.args[0]:
+        if out:
+          logger.ToStderr("  node: %s, script: %s, output: %s" %
+                          (node, script, out))
+        else:
+          logger.ToStderr("  node: %s, script: %s (no output)" %
+                          (node, script))
+      result = 1
+    except errors.HooksFailure, err:
+      logger.ToStderr("Failure: hooks general failure: %s" % str(err))
+      result = 1
+    except errors.OpPrereqError, err:
+      logger.ToStderr("Failure: prerequisites not met for this"
+                      " operation:\n%s" % str(err))
+      result = 1
+    except errors.OpExecError, err:
+      logger.ToStderr("Failure: command execution error:\n%s" % str(err))
+      result = 1
+  finally:
+    utils.Unlock('cmd')
+    utils.LockCleanup()
+
+  return result
diff --git a/lib/cmdlib.py b/lib/cmdlib.py
new file mode 100644 (file)
index 0000000..f6d2b17
--- /dev/null
@@ -0,0 +1,3347 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2006, 2007 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 commands used by gnt-* programs."""
+
+# pylint: disable-msg=W0613,W0201
+
+import os
+import os.path
+import sha
+import socket
+import time
+import tempfile
+import re
+import platform
+
+from ganeti import rpc
+from ganeti import ssh
+from ganeti import logger
+from ganeti import utils
+from ganeti import errors
+from ganeti import hypervisor
+from ganeti import config
+from ganeti import constants
+from ganeti import objects
+from ganeti import opcodes
+from ganeti import ssconf
+
+class LogicalUnit(object):
+  """Logical Unit base class..
+
+  Subclasses must follow these rules:
+    - implement CheckPrereq which also fills in the opcode instance
+      with all the fields (even if as None)
+    - implement Exec
+    - implement BuildHooksEnv
+    - redefine HPATH and HTYPE
+    - optionally redefine their run requirements (REQ_CLUSTER,
+      REQ_MASTER); note that all commands require root permissions
+
+  """
+  HPATH = None
+  HTYPE = None
+  _OP_REQP = []
+  REQ_CLUSTER = True
+  REQ_MASTER = True
+
+  def __init__(self, processor, op, cfg, sstore):
+    """Constructor for LogicalUnit.
+
+    This needs to be overriden in derived classes in order to check op
+    validity.
+
+    """
+    self.processor = processor
+    self.op = op
+    self.cfg = cfg
+    self.sstore = sstore
+    for attr_name in self._OP_REQP:
+      attr_val = getattr(op, attr_name, None)
+      if attr_val is None:
+        raise errors.OpPrereqError, ("Required parameter '%s' missing" %
+                                     attr_name)
+    if self.REQ_CLUSTER:
+      if not cfg.IsCluster():
+        raise errors.OpPrereqError, ("Cluster not initialized yet,"
+                                     " use 'gnt-cluster init' first.")
+      if self.REQ_MASTER:
+        master = cfg.GetMaster()
+        if master != socket.gethostname():
+          raise errors.OpPrereqError, ("Commands must be run on the master"
+                                       " node %s" % master)
+
+  def CheckPrereq(self):
+    """Check prerequisites for this LU.
+
+    This method should check that the prerequisites for the execution
+    of this LU are fulfilled. It can do internode communication, but
+    it should be idempotent - no cluster or system changes are
+    allowed.
+
+    The method should raise errors.OpPrereqError in case something is
+    not fulfilled. Its return value is ignored.
+
+    This method should also update all the parameters of the opcode to
+    their canonical form; e.g. a short node name must be fully
+    expanded after this method has successfully completed (so that
+    hooks, logging, etc. work correctly).
+
+    """
+    raise NotImplementedError
+
+  def Exec(self, feedback_fn):
+    """Execute the LU.
+
+    This method should implement the actual work. It should raise
+    errors.OpExecError for failures that are somewhat dealt with in
+    code, or expected.
+
+    """
+    raise NotImplementedError
+
+  def BuildHooksEnv(self):
+    """Build hooks environment for this LU.
+
+    This method should return a three-node tuple consisting of: a dict
+    containing the environment that will be used for running the
+    specific hook for this LU, a list of node names on which the hook
+    should run before the execution, and a list of node names on which
+    the hook should run after the execution.
+
+    The keys of the dict must not have 'GANETI_' prefixed as this will
+    be handled in the hooks runner. Also note additional keys will be
+    added by the hooks runner. If the LU doesn't define any
+    environment, an empty dict (and not None) should be returned.
+
+    As for the node lists, the master should not be included in the
+    them, as it will be added by the hooks runner in case this LU
+    requires a cluster to run on (otherwise we don't have a node
+    list). No nodes should be returned as an empty list (and not
+    None).
+
+    Note that if the HPATH for a LU class is None, this function will
+    not be called.
+
+    """
+    raise NotImplementedError
+
+
+class NoHooksLU(LogicalUnit):
+  """Simple LU which runs no hooks.
+
+  This LU is intended as a parent for other LogicalUnits which will
+  run no hooks, in order to reduce duplicate code.
+
+  """
+  HPATH = None
+  HTYPE = None
+
+  def BuildHooksEnv(self):
+    """Build hooks env.
+
+    This is a no-op, since we don't run hooks.
+
+    """
+    return
+
+
+def _UpdateEtcHosts(fullnode, ip):
+  """Ensure a node has a correct entry in /etc/hosts.
+
+  Args:
+    fullnode - Fully qualified domain name of host. (str)
+    ip       - IPv4 address of host (str)
+
+  """
+  node = fullnode.split(".", 1)[0]
+
+  f = open('/etc/hosts', 'r+')
+
+  inthere = False
+
+  save_lines = []
+  add_lines = []
+  removed = False
+
+  while True:
+    rawline = f.readline()
+
+    if not rawline:
+      # End of file
+      break
+
+    line = rawline.split('\n')[0]
+
+    # Strip off comments
+    line = line.split('#')[0]
+
+    if not line:
+      # Entire line was comment, skip
+      save_lines.append(rawline)
+      continue
+
+    fields = line.split()
+
+    haveall = True
+    havesome = False
+    for spec in [ ip, fullnode, node ]:
+      if spec not in fields:
+        haveall = False
+      if spec in fields:
+        havesome = True
+
+    if haveall:
+      inthere = True
+      save_lines.append(rawline)
+      continue
+
+    if havesome and not haveall:
+      # Line (old, or manual?) which is missing some.  Remove.
+      removed = True
+      continue
+
+    save_lines.append(rawline)
+
+  if not inthere:
+    add_lines.append('%s\t%s %s\n' % (ip, fullnode, node))
+
+  if removed:
+    if add_lines:
+      save_lines = save_lines + add_lines
+
+    # We removed a line, write a new file and replace old.
+    fd, tmpname = tempfile.mkstemp('tmp', 'hosts_', '/etc')
+    newfile = os.fdopen(fd, 'w')
+    newfile.write(''.join(save_lines))
+    newfile.close()
+    os.rename(tmpname, '/etc/hosts')
+
+  elif add_lines:
+    # Simply appending a new line will do the trick.
+    f.seek(0, 2)
+    for add in add_lines:
+      f.write(add)
+
+  f.close()
+
+
+def _UpdateKnownHosts(fullnode, ip, pubkey):
+  """Ensure a node has a correct known_hosts entry.
+
+  Args:
+    fullnode - Fully qualified domain name of host. (str)
+    ip       - IPv4 address of host (str)
+    pubkey   - the public key of the cluster
+
+  """
+  if os.path.exists('/etc/ssh/ssh_known_hosts'):
+    f = open('/etc/ssh/ssh_known_hosts', 'r+')
+  else:
+    f = open('/etc/ssh/ssh_known_hosts', 'w+')
+
+  inthere = False
+
+  save_lines = []
+  add_lines = []
+  removed = False
+
+  while True:
+    rawline = f.readline()
+    logger.Debug('read %s' % (repr(rawline),))
+
+    if not rawline:
+      # End of file
+      break
+
+    line = rawline.split('\n')[0]
+
+    parts = line.split(' ')
+    fields = parts[0].split(',')
+    key = parts[2]
+
+    haveall = True
+    havesome = False
+    for spec in [ ip, fullnode ]:
+      if spec not in fields:
+        haveall = False
+      if spec in fields:
+        havesome = True
+
+    logger.Debug("key, pubkey = %s." % (repr((key, pubkey)),))
+    if haveall and key == pubkey:
+      inthere = True
+      save_lines.append(rawline)
+      logger.Debug("Keeping known_hosts '%s'." % (repr(rawline),))
+      continue
+
+    if havesome and (not haveall or key != pubkey):
+      removed = True
+      logger.Debug("Discarding known_hosts '%s'." % (repr(rawline),))
+      continue
+
+    save_lines.append(rawline)
+
+  if not inthere:
+    add_lines.append('%s,%s ssh-rsa %s\n' % (fullnode, ip, pubkey))
+    logger.Debug("Adding known_hosts '%s'." % (repr(add_lines[-1]),))
+
+  if removed:
+    save_lines = save_lines + add_lines
+
+    # Write a new file and replace old.
+    fd, tmpname = tempfile.mkstemp('tmp', 'ssh_known_hosts_', '/etc/ssh')
+    newfile = os.fdopen(fd, 'w')
+    newfile.write(''.join(save_lines))
+    newfile.close()
+    logger.Debug("Wrote new known_hosts.")
+    os.rename(tmpname, '/etc/ssh/ssh_known_hosts')
+
+  elif add_lines:
+    # Simply appending a new line will do the trick.
+    f.seek(0, 2)
+    for add in add_lines:
+      f.write(add)
+
+  f.close()
+
+
+def _HasValidVG(vglist, vgname):
+  """Checks if the volume group list is valid.
+
+  A non-None return value means there's an error, and the return value
+  is the error message.
+
+  """
+  vgsize = vglist.get(vgname, None)
+  if vgsize is None:
+    return "volume group '%s' missing" % vgname
+  elif vgsize < 20480:
+    return ("volume group '%s' too small (20480MiB required, %dMib found" %
+            vgname, vgsize)
+  return None
+
+
+def _InitSSHSetup(node):
+  """Setup the SSH configuration for the cluster.
+
+
+  This generates a dsa keypair for root, adds the pub key to the
+  permitted hosts and adds the hostkey to its own known hosts.
+
+  Args:
+    node: the name of this host as a fqdn
+
+  """
+  utils.RemoveFile('/root/.ssh/known_hosts')
+
+  if os.path.exists('/root/.ssh/id_dsa'):
+    utils.CreateBackup('/root/.ssh/id_dsa')
+  if os.path.exists('/root/.ssh/id_dsa.pub'):
+    utils.CreateBackup('/root/.ssh/id_dsa.pub')
+
+  utils.RemoveFile('/root/.ssh/id_dsa')
+  utils.RemoveFile('/root/.ssh/id_dsa.pub')
+
+  result = utils.RunCmd(["ssh-keygen", "-t", "dsa",
+                         "-f", "/root/.ssh/id_dsa",
+                         "-q", "-N", ""])
+  if result.failed:
+    raise errors.OpExecError, ("could not generate ssh keypair, error %s" %
+                               result.output)
+
+  f = open('/root/.ssh/id_dsa.pub', 'r')
+  try:
+    utils.AddAuthorizedKey('/root/.ssh/authorized_keys', f.read(8192))
+  finally:
+    f.close()
+
+
+def _InitGanetiServerSetup(ss):
+  """Setup the necessary configuration for the initial node daemon.
+
+  This creates the nodepass file containing the shared password for
+  the cluster and also generates the SSL certificate.
+
+  """
+  # Create pseudo random password
+  randpass = sha.new(os.urandom(64)).hexdigest()
+  # and write it into sstore
+  ss.SetKey(ss.SS_NODED_PASS, randpass)
+
+  result = utils.RunCmd(["openssl", "req", "-new", "-newkey", "rsa:1024",
+                         "-days", str(365*5), "-nodes", "-x509",
+                         "-keyout", constants.SSL_CERT_FILE,
+                         "-out", constants.SSL_CERT_FILE, "-batch"])
+  if result.failed:
+    raise errors.OpExecError, ("could not generate server ssl cert, command"
+                               " %s had exitcode %s and error message %s" %
+                               (result.cmd, result.exit_code, result.output))
+
+  os.chmod(constants.SSL_CERT_FILE, 0400)
+
+  result = utils.RunCmd([constants.NODE_INITD_SCRIPT, "restart"])
+
+  if result.failed:
+    raise errors.OpExecError, ("could not start the node daemon, command %s"
+                               " had exitcode %s and error %s" %
+                               (result.cmd, result.exit_code, result.output))
+
+
+def _InitClusterInterface(fullname, name, ip):
+  """Initialize the master startup script.
+
+  """
+  f = file(constants.CLUSTER_NAME_FILE, 'w')
+  f.write("%s\n" % fullname)
+  f.close()
+
+  f = file(constants.MASTER_INITD_SCRIPT, 'w')
+  f.write ("#!/bin/sh\n")
+  f.write ("\n")
+  f.write ("# Start Ganeti Master Virtual Address\n")
+  f.write ("\n")
+  f.write ("DESC=\"Ganeti Master IP\"\n")
+  f.write ("MASTERNAME=\"%s\"\n" % name)
+  f.write ("MASTERIP=\"%s\"\n" % ip)
+  f.write ("case \"$1\" in\n")
+  f.write ("  start)\n")
+  f.write ("    if fping -q -c 3 ${MASTERIP} &>/dev/null; then\n")
+  f.write ("        echo \"$MASTERNAME no-go - there is already a master.\"\n")
+  f.write ("        rm -f %s\n" % constants.MASTER_CRON_LINK)
+  f.write ("        scp ${MASTERNAME}:%s %s\n" %
+           (constants.CLUSTER_CONF_FILE, constants.CLUSTER_CONF_FILE))
+  f.write ("    else\n")
+  f.write ("        echo -n \"Starting $DESC: \"\n")
+  f.write ("        ip address add ${MASTERIP}/32 dev xen-br0"
+           " label xen-br0:0\n")
+  f.write ("        arping -q -U -c 3 -I xen-br0 -s ${MASTERIP} ${MASTERIP}\n")
+  f.write ("        echo \"$MASTERNAME.\"\n")
+  f.write ("    fi\n")
+  f.write ("    ;;\n")
+  f.write ("  stop)\n")
+  f.write ("    echo -n \"Stopping $DESC: \"\n")
+  f.write ("    ip address del ${MASTERIP}/32 dev xen-br0\n")
+  f.write ("    echo \"$MASTERNAME.\"\n")
+  f.write ("    ;;\n")
+  f.write ("  *)\n")
+  f.write ("    echo \"Usage: $0 {start|stop}\" >&2\n")
+  f.write ("    exit 1\n")
+  f.write ("    ;;\n")
+  f.write ("esac\n")
+  f.write ("\n")
+  f.write ("exit 0\n")
+  f.flush()
+  os.fsync(f.fileno())
+  f.close()
+  os.chmod(constants.MASTER_INITD_SCRIPT, 0755)
+
+
+class LUInitCluster(LogicalUnit):
+  """Initialise the cluster.
+
+  """
+  HPATH = "cluster-init"
+  HTYPE = constants.HTYPE_CLUSTER
+  _OP_REQP = ["cluster_name", "hypervisor_type", "vg_name", "mac_prefix",
+              "def_bridge"]
+  REQ_CLUSTER = False
+
+  def BuildHooksEnv(self):
+    """Build hooks env.
+
+    Notes: Since we don't require a cluster, we must manually add
+    ourselves in the post-run node list.
+
+    """
+
+    env = {"CLUSTER": self.op.cluster_name,
+           "MASTER": self.hostname}
+    return env, [], [self.hostname['hostname_full']]
+
+  def CheckPrereq(self):
+    """Verify that the passed name is a valid one.
+
+    """
+    if config.ConfigWriter.IsCluster():
+      raise errors.OpPrereqError, ("Cluster is already initialised")
+
+    hostname_local = socket.gethostname()
+    self.hostname = hostname = utils.LookupHostname(hostname_local)
+    if not hostname:
+      raise errors.OpPrereqError, ("Cannot resolve my own hostname ('%s')" %
+                                   hostname_local)
+
+    self.clustername = clustername = utils.LookupHostname(self.op.cluster_name)
+    if not clustername:
+      raise errors.OpPrereqError, ("Cannot resolve given cluster name ('%s')"
+                                   % self.op.cluster_name)
+
+    result = utils.RunCmd(["fping", "-S127.0.0.1", "-q", hostname['ip']])
+    if result.failed:
+      raise errors.OpPrereqError, ("Inconsistency: this host's name resolves"
+                                   " to %s,\nbut this ip address does not"
+                                   " belong to this host."
+                                   " Aborting." % hostname['ip'])
+
+    secondary_ip = getattr(self.op, "secondary_ip", None)
+    if secondary_ip and not utils.IsValidIP(secondary_ip):
+      raise errors.OpPrereqError, ("Invalid secondary ip given")
+    if secondary_ip and secondary_ip != hostname['ip']:
+      result = utils.RunCmd(["fping", "-S127.0.0.1", "-q", secondary_ip])
+      if result.failed:
+        raise errors.OpPrereqError, ("You gave %s as secondary IP,\n"
+                                     "but it does not belong to this host." %
+                                     secondary_ip)
+    self.secondary_ip = secondary_ip
+
+    # checks presence of the volume group given
+    vgstatus = _HasValidVG(utils.ListVolumeGroups(), self.op.vg_name)
+
+    if vgstatus:
+      raise errors.OpPrereqError, ("Error: %s" % vgstatus)
+
+    if not re.match("^[0-9a-z]{2}:[0-9a-z]{2}:[0-9a-z]{2}$",
+                    self.op.mac_prefix):
+      raise errors.OpPrereqError, ("Invalid mac prefix given '%s'" %
+                                   self.op.mac_prefix)
+
+    if self.op.hypervisor_type not in hypervisor.VALID_HTYPES:
+      raise errors.OpPrereqError, ("Invalid hypervisor type given '%s'" %
+                                   self.op.hypervisor_type)
+
+  def Exec(self, feedback_fn):
+    """Initialize the cluster.
+
+    """
+    clustername = self.clustername
+    hostname = self.hostname
+
+    # adds the cluste name file and master startup script
+    _InitClusterInterface(clustername['hostname_full'],
+                          clustername['hostname'],
+                          clustername['ip'])
+
+    # set up the simple store
+    ss = ssconf.SimpleStore()
+    ss.SetKey(ss.SS_HYPERVISOR, self.op.hypervisor_type)
+
+    # set up the inter-node password and certificate
+    _InitGanetiServerSetup(ss)
+
+    # start the master ip
+    rpc.call_node_start_master(hostname['hostname_full'])
+
+    # set up ssh config and /etc/hosts
+    f = open('/etc/ssh/ssh_host_rsa_key.pub', 'r')
+    try:
+      sshline = f.read()
+    finally:
+      f.close()
+    sshkey = sshline.split(" ")[1]
+
+    _UpdateEtcHosts(hostname['hostname_full'],
+                    hostname['ip'],
+                    )
+
+    _UpdateKnownHosts(hostname['hostname_full'],
+                      hostname['ip'],
+                      sshkey,
+                      )
+
+    _InitSSHSetup(hostname['hostname'])
+
+    # init of cluster config file
+    cfgw = config.ConfigWriter()
+    cfgw.InitConfig(hostname['hostname'], hostname['ip'], self.secondary_ip,
+                    clustername['hostname'], sshkey, self.op.mac_prefix,
+                    self.op.vg_name, self.op.def_bridge)
+
+
+class LUDestroyCluster(NoHooksLU):
+  """Logical unit for destroying the cluster.
+
+  """
+  _OP_REQP = []
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    This checks whether the cluster is empty.
+
+    Any errors are signalled by raising errors.OpPrereqError.
+
+    """
+    master = self.cfg.GetMaster()
+
+    nodelist = self.cfg.GetNodeList()
+    if len(nodelist) > 0 and nodelist != [master]:
+        raise errors.OpPrereqError, ("There are still %d node(s) in "
+                                     "this cluster." % (len(nodelist) - 1))
+
+  def Exec(self, feedback_fn):
+    """Destroys the cluster.
+
+    """
+    utils.CreateBackup('/root/.ssh/id_dsa')
+    utils.CreateBackup('/root/.ssh/id_dsa.pub')
+    rpc.call_node_leave_cluster(self.cfg.GetMaster())
+
+
+class LUVerifyCluster(NoHooksLU):
+  """Verifies the cluster status.
+
+  """
+  _OP_REQP = []
+
+  def _VerifyNode(self, node, file_list, local_cksum, vglist, node_result,
+                  remote_version, feedback_fn):
+    """Run multiple tests against a node.
+
+    Test list:
+      - compares ganeti version
+      - checks vg existance and size > 20G
+      - checks config file checksum
+      - checks ssh to other nodes
+
+    Args:
+      node: name of the node to check
+      file_list: required list of files
+      local_cksum: dictionary of local files and their checksums
+    """
+    # compares ganeti version
+    local_version = constants.PROTOCOL_VERSION
+    if not remote_version:
+      feedback_fn(" - ERROR: connection to %s failed" % (node))
+      return True
+
+    if local_version != remote_version:
+      feedback_fn("  - ERROR: sw version mismatch: master %s, node(%s) %s" %
+                      (local_version, node, remote_version))
+      return True
+
+    # checks vg existance and size > 20G
+
+    bad = False
+    if not vglist:
+      feedback_fn("  - ERROR: unable to check volume groups on node %s." %
+                      (node,))
+      bad = True
+    else:
+      vgstatus = _HasValidVG(vglist, self.cfg.GetVGName())
+      if vgstatus:
+        feedback_fn("  - ERROR: %s on node %s" % (vgstatus, node))
+        bad = True
+
+    # checks config file checksum
+    # checks ssh to any
+
+    if 'filelist' not in node_result:
+      bad = True
+      feedback_fn("  - ERROR: node hasn't returned file checksum data")
+    else:
+      remote_cksum = node_result['filelist']
+      for file_name in file_list:
+        if file_name not in remote_cksum:
+          bad = True
+          feedback_fn("  - ERROR: file '%s' missing" % file_name)
+        elif remote_cksum[file_name] != local_cksum[file_name]:
+          bad = True
+          feedback_fn("  - ERROR: file '%s' has wrong checksum" % file_name)
+
+    if 'nodelist' not in node_result:
+      bad = True
+      feedback_fn("  - ERROR: node hasn't returned node connectivity data")
+    else:
+      if node_result['nodelist']:
+        bad = True
+        for node in node_result['nodelist']:
+          feedback_fn("  - ERROR: communication with node '%s': %s" %
+                          (node, node_result['nodelist'][node]))
+    hyp_result = node_result.get('hypervisor', None)
+    if hyp_result is not None:
+      feedback_fn("  - ERROR: hypervisor verify failure: '%s'" % hyp_result)
+    return bad
+
+  def _VerifyInstance(self, instance, node_vol_is, node_instance, feedback_fn):
+    """Verify an instance.
+
+    This function checks to see if the required block devices are
+    available on the instance's node.
+
+    """
+    bad = False
+
+    instancelist = self.cfg.GetInstanceList()
+    if not instance in instancelist:
+      feedback_fn("  - ERROR: instance %s not in instance list %s" %
+                      (instance, instancelist))
+      bad = True
+
+    instanceconfig = self.cfg.GetInstanceInfo(instance)
+    node_current = instanceconfig.primary_node
+
+    node_vol_should = {}
+    instanceconfig.MapLVsByNode(node_vol_should)
+
+    for node in node_vol_should:
+      for volume in node_vol_should[node]:
+        if node not in node_vol_is or volume not in node_vol_is[node]:
+          feedback_fn("  - ERROR: volume %s missing on node %s" %
+                          (volume, node))
+          bad = True
+
+    if not instanceconfig.status == 'down':
+      if not instance in node_instance[node_current]:
+        feedback_fn("  - ERROR: instance %s not running on node %s" %
+                        (instance, node_current))
+        bad = True
+
+    for node in node_instance:
+      if (not node == node_current):
+        if instance in node_instance[node]:
+          feedback_fn("  - ERROR: instance %s should not run on node %s" %
+                          (instance, node))
+          bad = True
+
+    return not bad
+
+  def _VerifyOrphanVolumes(self, node_vol_should, node_vol_is, feedback_fn):
+    """Verify if there are any unknown volumes in the cluster.
+
+    The .os, .swap and backup volumes are ignored. All other volumes are
+    reported as unknown.
+
+    """
+    bad = False
+
+    for node in node_vol_is:
+      for volume in node_vol_is[node]:
+        if node not in node_vol_should or volume not in node_vol_should[node]:
+          feedback_fn("  - ERROR: volume %s on node %s should not exist" %
+                      (volume, node))
+          bad = True
+    return bad
+
+
+  def _VerifyOrphanInstances(self, instancelist, node_instance, feedback_fn):
+    """Verify the list of running instances.
+
+    This checks what instances are running but unknown to the cluster.
+
+    """
+    bad = False
+    for node in node_instance:
+      for runninginstance in node_instance[node]:
+        if runninginstance not in instancelist:
+          feedback_fn("  - ERROR: instance %s on node %s should not exist" %
+                          (runninginstance, node))
+          bad = True
+    return bad
+
+  def _VerifyNodeConfigFiles(self, ismaster, node, file_list, feedback_fn):
+    """Verify the list of node config files"""
+
+    bad = False
+    for file_name in constants.MASTER_CONFIGFILES:
+      if ismaster and file_name not in file_list:
+        feedback_fn("  - ERROR: master config file %s missing from master"
+                    " node %s" % (file_name, node))
+        bad = True
+      elif not ismaster and file_name in file_list:
+        feedback_fn("  - ERROR: master config file %s should not exist"
+                    " on non-master node %s" % (file_name, node))
+        bad = True
+
+    for file_name in constants.NODE_CONFIGFILES:
+      if file_name not in file_list:
+        feedback_fn("  - ERROR: config file %s missing from node %s" %
+                    (file_name, node))
+        bad = True
+
+    return bad
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    This has no prerequisites.
+
+    """
+    pass
+
+  def Exec(self, feedback_fn):
+    """Verify integrity of cluster, performing various test on nodes.
+
+    """
+    bad = False
+    feedback_fn("* Verifying global settings")
+    self.cfg.VerifyConfig()
+
+    master = self.cfg.GetMaster()
+    vg_name = self.cfg.GetVGName()
+    nodelist = utils.NiceSort(self.cfg.GetNodeList())
+    instancelist = utils.NiceSort(self.cfg.GetInstanceList())
+    node_volume = {}
+    node_instance = {}
+
+    # FIXME: verify OS list
+    # do local checksums
+    file_names = constants.CLUSTER_CONF_FILES
+    local_checksums = utils.FingerprintFiles(file_names)
+
+    feedback_fn("* Gathering data (%d nodes)" % len(nodelist))
+    all_configfile = rpc.call_configfile_list(nodelist)
+    all_volumeinfo = rpc.call_volume_list(nodelist, vg_name)
+    all_instanceinfo = rpc.call_instance_list(nodelist)
+    all_vglist = rpc.call_vg_list(nodelist)
+    node_verify_param = {
+      'filelist': file_names,
+      'nodelist': nodelist,
+      'hypervisor': None,
+      }
+    all_nvinfo = rpc.call_node_verify(nodelist, node_verify_param)
+    all_rversion = rpc.call_version(nodelist)
+
+    for node in nodelist:
+      feedback_fn("* Verifying node %s" % node)
+      result = self._VerifyNode(node, file_names, local_checksums,
+                                all_vglist[node], all_nvinfo[node],
+                                all_rversion[node], feedback_fn)
+      bad = bad or result
+      # node_configfile
+      nodeconfigfile = all_configfile[node]
+
+      if not nodeconfigfile:
+        feedback_fn("  - ERROR: connection to %s failed" % (node))
+        bad = True
+        continue
+
+      bad = bad or self._VerifyNodeConfigFiles(node==master, node,
+                                               nodeconfigfile, feedback_fn)
+
+      # node_volume
+      volumeinfo = all_volumeinfo[node]
+
+      if type(volumeinfo) != dict:
+        feedback_fn("  - ERROR: connection to %s failed" % (node,))
+        bad = True
+        continue
+
+      node_volume[node] = volumeinfo
+
+      # node_instance
+      nodeinstance = all_instanceinfo[node]
+      if type(nodeinstance) != list:
+        feedback_fn("  - ERROR: connection to %s failed" % (node,))
+        bad = True
+        continue
+
+      node_instance[node] = nodeinstance
+
+    node_vol_should = {}
+
+    for instance in instancelist:
+      feedback_fn("* Verifying instance %s" % instance)
+      result =  self._VerifyInstance(instance, node_volume, node_instance,
+                                     feedback_fn)
+      bad = bad or result
+
+      inst_config = self.cfg.GetInstanceInfo(instance)
+
+      inst_config.MapLVsByNode(node_vol_should)
+
+    feedback_fn("* Verifying orphan volumes")
+    result = self._VerifyOrphanVolumes(node_vol_should, node_volume,
+                                       feedback_fn)
+    bad = bad or result
+
+    feedback_fn("* Verifying remaining instances")
+    result = self._VerifyOrphanInstances(instancelist, node_instance,
+                                         feedback_fn)
+    bad = bad or result
+
+    return int(bad)
+
+
+def _WaitForSync(cfgw, instance, oneshot=False, unlock=False):
+  """Sleep and poll for an instance's disk to sync.
+
+  """
+  if not instance.disks:
+    return True
+
+  if not oneshot:
+    logger.ToStdout("Waiting for instance %s to sync disks." % instance.name)
+
+  node = instance.primary_node
+
+  for dev in instance.disks:
+    cfgw.SetDiskID(dev, node)
+
+  retries = 0
+  while True:
+    max_time = 0
+    done = True
+    cumul_degraded = False
+    rstats = rpc.call_blockdev_getmirrorstatus(node, instance.disks)
+    if not rstats:
+      logger.ToStderr("Can't get any data from node %s" % node)
+      retries += 1
+      if retries >= 10:
+        raise errors.RemoteError, ("Can't contact node %s for mirror data,"
+                                   " aborting." % node)
+      time.sleep(6)
+      continue
+    retries = 0
+    for i in range(len(rstats)):
+      mstat = rstats[i]
+      if mstat is None:
+        logger.ToStderr("Can't compute data for node %s/%s" %
+                        (node, instance.disks[i].iv_name))
+        continue
+      perc_done, est_time, is_degraded = mstat
+      cumul_degraded = cumul_degraded or (is_degraded and perc_done is None)
+      if perc_done is not None:
+        done = False
+        if est_time is not None:
+          rem_time = "%d estimated seconds remaining" % est_time
+          max_time = est_time
+        else:
+          rem_time = "no time estimate"
+        logger.ToStdout("- device %s: %5.2f%% done, %s" %
+                        (instance.disks[i].iv_name, perc_done, rem_time))
+    if done or oneshot:
+      break
+
+    if unlock:
+      utils.Unlock('cmd')
+    try:
+      time.sleep(min(60, max_time))
+    finally:
+      if unlock:
+        utils.Lock('cmd')
+
+  if done:
+    logger.ToStdout("Instance %s's disks are in sync." % instance.name)
+  return not cumul_degraded
+
+
+def _CheckDiskConsistency(cfgw, dev, node, on_primary):
+  """Check that mirrors are not degraded.
+
+  """
+
+  cfgw.SetDiskID(dev, node)
+
+  result = True
+  if on_primary or dev.AssembleOnSecondary():
+    rstats = rpc.call_blockdev_find(node, dev)
+    if not rstats:
+      logger.ToStderr("Can't get any data from node %s" % node)
+      result = False
+    else:
+      result = result and (not rstats[5])
+  if dev.children:
+    for child in dev.children:
+      result = result and _CheckDiskConsistency(cfgw, child, node, on_primary)
+
+  return result
+
+
+class LUDiagnoseOS(NoHooksLU):
+  """Logical unit for OS diagnose/query.
+
+  """
+  _OP_REQP = []
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    This always succeeds, since this is a pure query LU.
+
+    """
+    return
+
+  def Exec(self, feedback_fn):
+    """Compute the list of OSes.
+
+    """
+    node_list = self.cfg.GetNodeList()
+    node_data = rpc.call_os_diagnose(node_list)
+    if node_data == False:
+      raise errors.OpExecError, "Can't gather the list of OSes"
+    return node_data
+
+
+class LURemoveNode(LogicalUnit):
+  """Logical unit for removing a node.
+
+  """
+  HPATH = "node-remove"
+  HTYPE = constants.HTYPE_NODE
+  _OP_REQP = ["node_name"]
+
+  def BuildHooksEnv(self):
+    """Build hooks env.
+
+    This doesn't run on the target node in the pre phase as a failed
+    node would not allows itself to run.
+
+    """
+    all_nodes = self.cfg.GetNodeList()
+    all_nodes.remove(self.op.node_name)
+    return {"NODE_NAME": self.op.node_name}, all_nodes, all_nodes
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    This checks:
+     - the node exists in the configuration
+     - it does not have primary or secondary instances
+     - it's not the master
+
+    Any errors are signalled by raising errors.OpPrereqError.
+
+    """
+
+    node = self.cfg.GetNodeInfo(self.cfg.ExpandNodeName(self.op.node_name))
+    if node is None:
+      logger.Error("Error: Node '%s' is unknown." % self.op.node_name)
+      return 1
+
+    instance_list = self.cfg.GetInstanceList()
+
+    masternode = self.cfg.GetMaster()
+    if node.name == masternode:
+      raise errors.OpPrereqError, ("Node is the master node,"
+                                   " you need to failover first.")
+
+    for instance_name in instance_list:
+      instance = self.cfg.GetInstanceInfo(instance_name)
+      if node.name == instance.primary_node:
+        raise errors.OpPrereqError, ("Instance %s still running on the node,"
+                                     " please remove first." % instance_name)
+      if node.name in instance.secondary_nodes:
+        raise errors.OpPrereqError, ("Instance %s has node as a secondary,"
+                                     " please remove first." % instance_name)
+    self.op.node_name = node.name
+    self.node = node
+
+  def Exec(self, feedback_fn):
+    """Removes the node from the cluster.
+
+    """
+    node = self.node
+    logger.Info("stopping the node daemon and removing configs from node %s" %
+                node.name)
+
+    rpc.call_node_leave_cluster(node.name)
+
+    ssh.SSHCall(node.name, 'root', "%s stop" % constants.NODE_INITD_SCRIPT)
+
+    logger.Info("Removing node %s from config" % node.name)
+
+    self.cfg.RemoveNode(node.name)
+
+
+class LUQueryNodes(NoHooksLU):
+  """Logical unit for querying nodes.
+
+  """
+  _OP_REQP = ["output_fields"]
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    This checks that the fields required are valid output fields.
+
+    """
+    self.static_fields = frozenset(["name", "pinst", "sinst", "pip", "sip"])
+    self.dynamic_fields = frozenset(["dtotal", "dfree",
+                                     "mtotal", "mnode", "mfree"])
+    self.all_fields = self.static_fields | self.dynamic_fields
+
+    if not self.all_fields.issuperset(self.op.output_fields):
+      raise errors.OpPrereqError, ("Unknown output fields selected: %s"
+                                   % ",".join(frozenset(self.op.output_fields).
+                                              difference(self.all_fields)))
+
+
+  def Exec(self, feedback_fn):
+    """Computes the list of nodes and their attributes.
+
+    """
+    nodenames = utils.NiceSort(self.cfg.GetNodeList())
+    nodelist = [self.cfg.GetNodeInfo(name) for name in nodenames]
+
+
+    # begin data gathering
+
+    if self.dynamic_fields.intersection(self.op.output_fields):
+      live_data = {}
+      node_data = rpc.call_node_info(nodenames, self.cfg.GetVGName())
+      for name in nodenames:
+        nodeinfo = node_data.get(name, None)
+        if nodeinfo:
+          live_data[name] = {
+            "mtotal": utils.TryConvert(int, nodeinfo['memory_total']),
+            "mnode": utils.TryConvert(int, nodeinfo['memory_dom0']),
+            "mfree": utils.TryConvert(int, nodeinfo['memory_free']),
+            "dtotal": utils.TryConvert(int, nodeinfo['vg_size']),
+            "dfree": utils.TryConvert(int, nodeinfo['vg_free']),
+            }
+        else:
+          live_data[name] = {}
+    else:
+      live_data = dict.fromkeys(nodenames, {})
+
+    node_to_primary = dict.fromkeys(nodenames, 0)
+    node_to_secondary = dict.fromkeys(nodenames, 0)
+
+    if "pinst" in self.op.output_fields or "sinst" in self.op.output_fields:
+      instancelist = self.cfg.GetInstanceList()
+
+      for instance in instancelist:
+        instanceinfo = self.cfg.GetInstanceInfo(instance)
+        node_to_primary[instanceinfo.primary_node] += 1
+        for secnode in instanceinfo.secondary_nodes:
+          node_to_secondary[secnode] += 1
+
+    # end data gathering
+
+    output = []
+    for node in nodelist:
+      node_output = []
+      for field in self.op.output_fields:
+        if field == "name":
+          val = node.name
+        elif field == "pinst":
+          val = node_to_primary[node.name]
+        elif field == "sinst":
+          val = node_to_secondary[node.name]
+        elif field == "pip":
+          val = node.primary_ip
+        elif field == "sip":
+          val = node.secondary_ip
+        elif field in self.dynamic_fields:
+          val = live_data[node.name].get(field, "?")
+        else:
+          raise errors.ParameterError, field
+        val = str(val)
+        node_output.append(val)
+      output.append(node_output)
+
+    return output
+
+
+def _CheckNodesDirs(node_list, paths):
+  """Verify if the given nodes have the same files.
+
+  Args:
+    node_list: the list of node names to check
+    paths: the list of directories to checksum and compare
+
+  Returns:
+    list of (node, different_file, message); if empty, the files are in sync
+
+  """
+  file_names = []
+  for dir_name in paths:
+    flist = [os.path.join(dir_name, name) for name in os.listdir(dir_name)]
+    flist = [name for name in flist if os.path.isfile(name)]
+    file_names.extend(flist)
+
+  local_checksums = utils.FingerprintFiles(file_names)
+
+  results = []
+  verify_params = {'filelist': file_names}
+  all_node_results = rpc.call_node_verify(node_list, verify_params)
+  for node_name in node_list:
+    node_result = all_node_results.get(node_name, False)
+    if not node_result or 'filelist' not in node_result:
+      results.append((node_name, "'all files'", "node communication error"))
+      continue
+    remote_checksums = node_result['filelist']
+    for fname in local_checksums:
+      if fname not in remote_checksums:
+        results.append((node_name, fname, "missing file"))
+      elif remote_checksums[fname] != local_checksums[fname]:
+        results.append((node_name, fname, "wrong checksum"))
+  return results
+
+
+class LUAddNode(LogicalUnit):
+  """Logical unit for adding node to the cluster.
+
+  """
+  HPATH = "node-add"
+  HTYPE = constants.HTYPE_NODE
+  _OP_REQP = ["node_name"]
+
+  def BuildHooksEnv(self):
+    """Build hooks env.
+
+    This will run on all nodes before, and on all nodes + the new node after.
+
+    """
+    env = {
+      "NODE_NAME": self.op.node_name,
+      "NODE_PIP": self.op.primary_ip,
+      "NODE_SIP": self.op.secondary_ip,
+      }
+    nodes_0 = self.cfg.GetNodeList()
+    nodes_1 = nodes_0 + [self.op.node_name, ]
+    return env, nodes_0, nodes_1
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    This checks:
+     - the new node is not already in the config
+     - it is resolvable
+     - its parameters (single/dual homed) matches the cluster
+
+    Any errors are signalled by raising errors.OpPrereqError.
+
+    """
+    node_name = self.op.node_name
+    cfg = self.cfg
+
+    dns_data = utils.LookupHostname(node_name)
+    if not dns_data:
+      raise errors.OpPrereqError, ("Node %s is not resolvable" % node_name)
+
+    node = dns_data['hostname']
+    primary_ip = self.op.primary_ip = dns_data['ip']
+    secondary_ip = getattr(self.op, "secondary_ip", None)
+    if secondary_ip is None:
+      secondary_ip = primary_ip
+    if not utils.IsValidIP(secondary_ip):
+      raise errors.OpPrereqError, ("Invalid secondary IP given")
+    self.op.secondary_ip = secondary_ip
+    node_list = cfg.GetNodeList()
+    if node in node_list:
+      raise errors.OpPrereqError, ("Node %s is already in the configuration"
+                                   % node)
+
+    for existing_node_name in node_list:
+      existing_node = cfg.GetNodeInfo(existing_node_name)
+      if (existing_node.primary_ip == primary_ip or
+          existing_node.secondary_ip == primary_ip or
+          existing_node.primary_ip == secondary_ip or
+          existing_node.secondary_ip == secondary_ip):
+        raise errors.OpPrereqError, ("New node ip address(es) conflict with"
+                                     " existing node %s" % existing_node.name)
+
+    # check that the type of the node (single versus dual homed) is the
+    # same as for the master
+    myself = cfg.GetNodeInfo(cfg.GetMaster())
+    master_singlehomed = myself.secondary_ip == myself.primary_ip
+    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"
+                                     " new node has one")
+      else:
+        raise errors.OpPrereqError ("The master has a private ip but the"
+                                    " new node doesn't have one")
+
+    # checks reachablity
+    command = ["fping", "-q", primary_ip]
+    result = utils.RunCmd(command)
+    if result.failed:
+      raise errors.OpPrereqError, ("Node not reachable by ping")
+
+    if not newbie_singlehomed:
+      # check reachability from my secondary ip to newbie's secondary ip
+      command = ["fping", "-S%s" % myself.secondary_ip, "-q", secondary_ip]
+      result = utils.RunCmd(command)
+      if result.failed:
+        raise errors.OpPrereqError, ("Node secondary ip not reachable by ping")
+
+    self.new_node = objects.Node(name=node,
+                                 primary_ip=primary_ip,
+                                 secondary_ip=secondary_ip)
+
+  def Exec(self, feedback_fn):
+    """Adds the new node to the cluster.
+
+    """
+    new_node = self.new_node
+    node = new_node.name
+
+    # set up inter-node password and certificate and restarts the node daemon
+    gntpass = self.sstore.GetNodeDaemonPassword()
+    if not re.match('^[a-zA-Z0-9.]{1,64}$', gntpass):
+      raise errors.OpExecError, ("ganeti password corruption detected")
+    f = open(constants.SSL_CERT_FILE)
+    try:
+      gntpem = f.read(8192)
+    finally:
+      f.close()
+    # in the base64 pem encoding, neither '!' nor '.' are valid chars,
+    # so we use this to detect an invalid certificate; as long as the
+    # cert doesn't contain this, the here-document will be correctly
+    # parsed by the shell sequence below
+    if re.search('^!EOF\.', gntpem, re.MULTILINE):
+      raise errors.OpExecError, ("invalid PEM encoding in the SSL certificate")
+    if not gntpem.endswith("\n"):
+      raise errors.OpExecError, ("PEM must end with newline")
+    logger.Info("copy cluster pass to %s and starting the node daemon" % node)
+
+    # remove first the root's known_hosts file
+    utils.RemoveFile("/root/.ssh/known_hosts")
+    # 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
+    ss = self.sstore
+    mycommand = ("umask 077 && "
+                 "echo '%s' > '%s' && "
+                 "cat > '%s' << '!EOF.' && \n"
+                 "%s!EOF.\n%s restart" %
+                 (gntpass, ss.KeyToFilename(ss.SS_NODED_PASS),
+                  constants.SSL_CERT_FILE, gntpem,
+                  constants.NODE_INITD_SCRIPT))
+
+    result = ssh.SSHCall(node, 'root', mycommand, batch=False, ask_key=True)
+    if result.failed:
+      raise errors.OpExecError, ("Remote command on node %s, error: %s,"
+                                 " output: %s" %
+                                 (node, result.fail_reason, result.output))
+
+    # check connectivity
+    time.sleep(4)
+
+    result = rpc.call_version([node])[node]
+    if result:
+      if constants.PROTOCOL_VERSION == result:
+        logger.Info("communication to node %s fine, sw version %s match" %
+                    (node, result))
+      else:
+        raise errors.OpExecError, ("Version mismatch master version %s,"
+                                   " node version %s" %
+                                   (constants.PROTOCOL_VERSION, result))
+    else:
+      raise errors.OpExecError, ("Cannot get version from the new node")
+
+    # setup ssh on node
+    logger.Info("copy ssh key to node %s" % node)
+    keyarray = []
+    keyfiles = ["/etc/ssh/ssh_host_dsa_key", "/etc/ssh/ssh_host_dsa_key.pub",
+                "/etc/ssh/ssh_host_rsa_key", "/etc/ssh/ssh_host_rsa_key.pub",
+                "/root/.ssh/id_dsa", "/root/.ssh/id_dsa.pub"]
+
+    for i in keyfiles:
+      f = open(i, 'r')
+      try:
+        keyarray.append(f.read())
+      finally:
+        f.close()
+
+    result = rpc.call_node_add(node, keyarray[0], keyarray[1], keyarray[2],
+                               keyarray[3], keyarray[4], keyarray[5])
+
+    if not result:
+      raise errors.OpExecError, ("Cannot transfer ssh keys to the new node")
+
+    # Add node to our /etc/hosts, and add key to known_hosts
+    _UpdateEtcHosts(new_node.name, new_node.primary_ip)
+    _UpdateKnownHosts(new_node.name, new_node.primary_ip,
+                      self.cfg.GetHostKey())
+
+    if new_node.secondary_ip != new_node.primary_ip:
+      result = ssh.SSHCall(node, "root",
+                           "fping -S 127.0.0.1 -q %s" % new_node.secondary_ip)
+      if result.failed:
+        raise errors.OpExecError, ("Node claims it doesn't have the"
+                                   " secondary ip you gave (%s).\n"
+                                   "Please fix and re-run this command." %
+                                   new_node.secondary_ip)
+
+    # Distribute updated /etc/hosts and known_hosts to all nodes,
+    # including the node just added
+    myself = self.cfg.GetNodeInfo(self.cfg.GetMaster())
+    dist_nodes = self.cfg.GetNodeList() + [node]
+    if myself.name in dist_nodes:
+      dist_nodes.remove(myself.name)
+
+    logger.Debug("Copying hosts and known_hosts to all nodes")
+    for fname in ("/etc/hosts", "/etc/ssh/ssh_known_hosts"):
+      result = rpc.call_upload_file(dist_nodes, fname)
+      for to_node in dist_nodes:
+        if not result[to_node]:
+          logger.Error("copy of file %s to node %s failed" %
+                       (fname, to_node))
+
+    to_copy = [constants.MASTER_CRON_FILE,
+               constants.MASTER_INITD_SCRIPT,
+               constants.CLUSTER_NAME_FILE]
+    to_copy.extend(ss.GetFileList())
+    for fname in to_copy:
+      if not ssh.CopyFileToNode(node, fname):
+        logger.Error("could not copy file %s to node %s" % (fname, node))
+
+    logger.Info("adding node %s to cluster.conf" % node)
+    self.cfg.AddNode(new_node)
+
+
+class LUMasterFailover(LogicalUnit):
+  """Failover the master node to the current node.
+
+  This is a special LU in that it must run on a non-master node.
+
+  """
+  HPATH = "master-failover"
+  HTYPE = constants.HTYPE_CLUSTER
+  REQ_MASTER = False
+  _OP_REQP = []
+
+  def BuildHooksEnv(self):
+    """Build hooks env.
+
+    This will run on the new master only in the pre phase, and on all
+    the nodes in the post phase.
+
+    """
+    env = {
+      "NEW_MASTER": self.new_master,
+      "OLD_MASTER": self.old_master,
+      }
+    return env, [self.new_master], self.cfg.GetNodeList()
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    This checks that we are not already the master.
+
+    """
+    self.new_master = socket.gethostname()
+
+    self.old_master = self.cfg.GetMaster()
+
+    if self.old_master == self.new_master:
+      raise errors.OpPrereqError, ("This commands must be run on the node"
+                                   " where you want the new master to be.\n"
+                                   "%s is already the master" %
+                                   self.old_master)
+
+  def Exec(self, feedback_fn):
+    """Failover the master node.
+
+    This command, when run on a non-master node, will cause the current
+    master to cease being master, and the non-master to become new
+    master.
+
+    """
+
+    #TODO: do not rely on gethostname returning the FQDN
+    logger.Info("setting master to %s, old master: %s" %
+                (self.new_master, self.old_master))
+
+    if not rpc.call_node_stop_master(self.old_master):
+      logger.Error("could disable the master role on the old master"
+                   " %s, please disable manually" % self.old_master)
+
+    if not rpc.call_node_start_master(self.new_master):
+      logger.Error("could not start the master role on the new master"
+                   " %s, please check" % self.new_master)
+
+    self.cfg.SetMaster(self.new_master)
+
+
+class LUQueryClusterInfo(NoHooksLU):
+  """Query cluster configuration.
+
+  """
+  _OP_REQP = []
+
+  def CheckPrereq(self):
+    """No prerequsites needed for this LU.
+
+    """
+    pass
+
+  def Exec(self, feedback_fn):
+    """Return cluster config.
+
+    """
+    instances = [self.cfg.GetInstanceInfo(name)
+                 for name in self.cfg.GetInstanceList()]
+    result = {
+      "name": self.cfg.GetClusterName(),
+      "software_version": constants.RELEASE_VERSION,
+      "protocol_version": constants.PROTOCOL_VERSION,
+      "config_version": constants.CONFIG_VERSION,
+      "os_api_version": constants.OS_API_VERSION,
+      "export_version": constants.EXPORT_VERSION,
+      "master": self.cfg.GetMaster(),
+      "architecture": (platform.architecture()[0], platform.machine()),
+      "instances": [(instance.name, instance.primary_node)
+                    for instance in instances],
+      "nodes": self.cfg.GetNodeList(),
+      }
+
+    return result
+
+
+class LUClusterCopyFile(NoHooksLU):
+  """Copy file to cluster.
+
+  """
+  _OP_REQP = ["nodes", "filename"]
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    It should check that the named file exists and that the given list
+    of nodes is valid.
+
+    """
+    if not os.path.exists(self.op.filename):
+      raise errors.OpPrereqError("No such filename '%s'" % self.op.filename)
+    if self.op.nodes:
+      nodes = self.op.nodes
+    else:
+      nodes = self.cfg.GetNodeList()
+    self.nodes = []
+    for node in nodes:
+      nname = self.cfg.ExpandNodeName(node)
+      if nname is None:
+        raise errors.OpPrereqError, ("Node '%s' is unknown." % node)
+      self.nodes.append(nname)
+
+  def Exec(self, feedback_fn):
+    """Copy a file from master to some nodes.
+
+    Args:
+      opts - class with options as members
+      args - list containing a single element, the file name
+    Opts used:
+      nodes - list containing the name of target nodes; if empty, all nodes
+
+    """
+    filename = self.op.filename
+
+    myname = socket.gethostname()
+
+    for node in self.nodes:
+      if node == myname:
+        continue
+      if not ssh.CopyFileToNode(node, filename):
+        logger.Error("Copy of file %s to node %s failed" % (filename, node))
+
+
+class LUDumpClusterConfig(NoHooksLU):
+  """Return a text-representation of the cluster-config.
+
+  """
+  _OP_REQP = []
+
+  def CheckPrereq(self):
+    """No prerequisites.
+
+    """
+    pass
+
+  def Exec(self, feedback_fn):
+    """Dump a representation of the cluster config to the standard output.
+
+    """
+    return self.cfg.DumpConfig()
+
+
+class LURunClusterCommand(NoHooksLU):
+  """Run a command on some nodes.
+
+  """
+  _OP_REQP = ["command", "nodes"]
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    It checks that the given list of nodes is valid.
+
+    """
+    if self.op.nodes:
+      nodes = self.op.nodes
+    else:
+      nodes = self.cfg.GetNodeList()
+    self.nodes = []
+    for node in nodes:
+      nname = self.cfg.ExpandNodeName(node)
+      if nname is None:
+        raise errors.OpPrereqError, ("Node '%s' is unknown." % node)
+      self.nodes.append(nname)
+
+  def Exec(self, feedback_fn):
+    """Run a command on some nodes.
+
+    """
+    data = []
+    for node in self.nodes:
+      result = utils.RunCmd(["ssh", node, self.op.command])
+      data.append((node, result.cmd, result.output, result.exit_code))
+
+    return data
+
+
+class LUActivateInstanceDisks(NoHooksLU):
+  """Bring up an instance's disks.
+
+  """
+  _OP_REQP = ["instance_name"]
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    This checks that the instance is in the cluster.
+
+    """
+    instance = self.cfg.GetInstanceInfo(
+      self.cfg.ExpandInstanceName(self.op.instance_name))
+    if instance is None:
+      raise errors.OpPrereqError, ("Instance '%s' not known" %
+                                   self.op.instance_name)
+    self.instance = instance
+
+
+  def Exec(self, feedback_fn):
+    """Activate the disks.
+
+    """
+    disks_ok, disks_info = _AssembleInstanceDisks(self.instance, self.cfg)
+    if not disks_ok:
+      raise errors.OpExecError, ("Cannot activate block devices")
+
+    return disks_info
+
+
+def _AssembleInstanceDisks(instance, cfg, ignore_secondaries=False):
+  """Prepare the block devices for an instance.
+
+  This sets up the block devices on all nodes.
+
+  Args:
+    instance: a ganeti.objects.Instance object
+    ignore_secondaries: if true, errors on secondary nodes won't result
+                        in an error return from the function
+
+  Returns:
+    false if the operation failed
+    list of (host, instance_visible_name, node_visible_name) if the operation
+         suceeded with the mapping from node devices to instance devices
+  """
+  device_info = []
+  disks_ok = True
+  for inst_disk in instance.disks:
+    master_result = None
+    for node, node_disk in inst_disk.ComputeNodeTree(instance.primary_node):
+      cfg.SetDiskID(node_disk, node)
+      is_primary = node == instance.primary_node
+      result = rpc.call_blockdev_assemble(node, node_disk, is_primary)
+      if not result:
+        logger.Error("could not prepare block device %s on node %s (is_pri"
+                     "mary=%s)" % (inst_disk.iv_name, node, is_primary))
+        if is_primary or not ignore_secondaries:
+          disks_ok = False
+      if is_primary:
+        master_result = result
+    device_info.append((instance.primary_node, inst_disk.iv_name,
+                        master_result))
+
+  return disks_ok, device_info
+
+
+class LUDeactivateInstanceDisks(NoHooksLU):
+  """Shutdown an instance's disks.
+
+  """
+  _OP_REQP = ["instance_name"]
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    This checks that the instance is in the cluster.
+
+    """
+    instance = self.cfg.GetInstanceInfo(
+      self.cfg.ExpandInstanceName(self.op.instance_name))
+    if instance is None:
+      raise errors.OpPrereqError, ("Instance '%s' not known" %
+                                   self.op.instance_name)
+    self.instance = instance
+
+  def Exec(self, feedback_fn):
+    """Deactivate the disks
+
+    """
+    instance = self.instance
+    ins_l = rpc.call_instance_list([instance.primary_node])
+    ins_l = ins_l[instance.primary_node]
+    if not type(ins_l) is list:
+      raise errors.OpExecError, ("Can't contact node '%s'" %
+                                 instance.primary_node)
+
+    if self.instance.name in ins_l:
+      raise errors.OpExecError, ("Instance is running, can't shutdown"
+                                 " block devices.")
+
+    _ShutdownInstanceDisks(instance, self.cfg)
+
+
+def _ShutdownInstanceDisks(instance, cfg, ignore_primary=False):
+  """Shutdown block devices of an instance.
+
+  This does the shutdown on all nodes of the instance.
+
+  If the ignore_primary is false, errors on the primary node are
+  ignored.
+
+  """
+  result = True
+  for disk in instance.disks:
+    for node, top_disk in disk.ComputeNodeTree(instance.primary_node):
+      cfg.SetDiskID(top_disk, node)
+      if not rpc.call_blockdev_shutdown(node, top_disk):
+        logger.Error("could not shutdown block device %s on node %s" %
+                     (disk.iv_name, node))
+        if not ignore_primary or node != instance.primary_node:
+          result = False
+  return result
+
+
+class LUStartupInstance(LogicalUnit):
+  """Starts an instance.
+
+  """
+  HPATH = "instance-start"
+  HTYPE = constants.HTYPE_INSTANCE
+  _OP_REQP = ["instance_name", "force"]
+
+  def BuildHooksEnv(self):
+    """Build hooks env.
+
+    This runs on master, primary and secondary nodes of the instance.
+
+    """
+    env = {
+      "INSTANCE_NAME": self.op.instance_name,
+      "INSTANCE_PRIMARY": self.instance.primary_node,
+      "INSTANCE_SECONDARIES": " ".join(self.instance.secondary_nodes),
+      "FORCE": self.op.force,
+      }
+    nl = ([self.cfg.GetMaster(), self.instance.primary_node] +
+          list(self.instance.secondary_nodes))
+    return env, nl, nl
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    This checks that the instance is in the cluster.
+
+    """
+    instance = self.cfg.GetInstanceInfo(
+      self.cfg.ExpandInstanceName(self.op.instance_name))
+    if instance is None:
+      raise errors.OpPrereqError, ("Instance '%s' not known" %
+                                   self.op.instance_name)
+
+    # check bridges existance
+    brlist = [nic.bridge for nic in instance.nics]
+    if not rpc.call_bridges_exist(instance.primary_node, brlist):
+      raise errors.OpPrereqError, ("one or more target bridges %s does not"
+                                   " exist on destination node '%s'" %
+                                   (brlist, instance.primary_node))
+
+    self.instance = instance
+    self.op.instance_name = instance.name
+
+  def Exec(self, feedback_fn):
+    """Start the instance.
+
+    """
+    instance = self.instance
+    force = self.op.force
+    extra_args = getattr(self.op, "extra_args", "")
+
+    node_current = instance.primary_node
+
+    nodeinfo = rpc.call_node_info([node_current], self.cfg.GetVGName())
+    if not nodeinfo:
+      raise errors.OpExecError, ("Could not contact node %s for infos" %
+                                 (node_current))
+
+    freememory = nodeinfo[node_current]['memory_free']
+    memory = instance.memory
+    if memory > freememory:
+      raise errors.OpExecError, ("Not enough memory to start instance"
+                                 " %s on node %s"
+                                 " needed %s MiB, available %s MiB" %
+                                 (instance.name, node_current, memory,
+                                  freememory))
+
+    disks_ok, dummy = _AssembleInstanceDisks(instance, self.cfg,
+                                             ignore_secondaries=force)
+    if not disks_ok:
+      _ShutdownInstanceDisks(instance, self.cfg)
+      if not force:
+        logger.Error("If the message above refers to a secondary node,"
+                     " you can retry the operation using '--force'.")
+      raise errors.OpExecError, ("Disk consistency error")
+
+    if not rpc.call_instance_start(node_current, instance, extra_args):
+      _ShutdownInstanceDisks(instance, self.cfg)
+      raise errors.OpExecError, ("Could not start instance")
+
+    self.cfg.MarkInstanceUp(instance.name)
+
+
+class LUShutdownInstance(LogicalUnit):
+  """Shutdown an instance.
+
+  """
+  HPATH = "instance-stop"
+  HTYPE = constants.HTYPE_INSTANCE
+  _OP_REQP = ["instance_name"]
+
+  def BuildHooksEnv(self):
+    """Build hooks env.
+
+    This runs on master, primary and secondary nodes of the instance.
+
+    """
+    env = {
+      "INSTANCE_NAME": self.op.instance_name,
+      "INSTANCE_PRIMARY": self.instance.primary_node,
+      "INSTANCE_SECONDARIES": " ".join(self.instance.secondary_nodes),
+      }
+    nl = ([self.cfg.GetMaster(), self.instance.primary_node] +
+          list(self.instance.secondary_nodes))
+    return env, nl, nl
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    This checks that the instance is in the cluster.
+
+    """
+    instance = self.cfg.GetInstanceInfo(
+      self.cfg.ExpandInstanceName(self.op.instance_name))
+    if instance is None:
+      raise errors.OpPrereqError, ("Instance '%s' not known" %
+                                   self.op.instance_name)
+    self.instance = instance
+
+  def Exec(self, feedback_fn):
+    """Shutdown the instance.
+
+    """
+    instance = self.instance
+    node_current = instance.primary_node
+    if not rpc.call_instance_shutdown(node_current, instance):
+      logger.Error("could not shutdown instance")
+
+    self.cfg.MarkInstanceDown(instance.name)
+    _ShutdownInstanceDisks(instance, self.cfg)
+
+
+class LURemoveInstance(LogicalUnit):
+  """Remove an instance.
+
+  """
+  HPATH = "instance-remove"
+  HTYPE = constants.HTYPE_INSTANCE
+  _OP_REQP = ["instance_name"]
+
+  def BuildHooksEnv(self):
+    """Build hooks env.
+
+    This runs on master, primary and secondary nodes of the instance.
+
+    """
+    env = {
+      "INSTANCE_NAME": self.op.instance_name,
+      "INSTANCE_PRIMARY": self.instance.primary_node,
+      "INSTANCE_SECONDARIES": " ".join(self.instance.secondary_nodes),
+      }
+    nl = ([self.cfg.GetMaster(), self.instance.primary_node] +
+          list(self.instance.secondary_nodes))
+    return env, nl, nl
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    This checks that the instance is in the cluster.
+
+    """
+    instance = self.cfg.GetInstanceInfo(
+      self.cfg.ExpandInstanceName(self.op.instance_name))
+    if instance is None:
+      raise errors.OpPrereqError, ("Instance '%s' not known" %
+                                   self.op.instance_name)
+    self.instance = instance
+
+  def Exec(self, feedback_fn):
+    """Remove the instance.
+
+    """
+    instance = self.instance
+    logger.Info("shutting down instance %s on node %s" %
+                (instance.name, instance.primary_node))
+
+    if not rpc.call_instance_shutdown(instance.primary_node, instance):
+      raise errors.OpExecError, ("Could not shutdown instance %s on node %s" %
+                                 (instance.name, instance.primary_node))
+
+    logger.Info("removing block devices for instance %s" % instance.name)
+
+    _RemoveDisks(instance, self.cfg)
+
+    logger.Info("removing instance %s out of cluster config" % instance.name)
+
+    self.cfg.RemoveInstance(instance.name)
+
+
+class LUQueryInstances(NoHooksLU):
+  """Logical unit for querying instances.
+
+  """
+  OP_REQP = ["output_fields"]
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    This checks that the fields required are valid output fields.
+
+    """
+
+    self.static_fields = frozenset(["name", "os", "pnode", "snodes",
+                                    "admin_state", "admin_ram",
+                                    "disk_template", "ip", "mac", "bridge"])
+    self.dynamic_fields = frozenset(["oper_state", "oper_ram"])
+    self.all_fields = self.static_fields | self.dynamic_fields
+
+    if not self.all_fields.issuperset(self.op.output_fields):
+      raise errors.OpPrereqError, ("Unknown output fields selected: %s"
+                                   % ",".join(frozenset(self.op.output_fields).
+                                              difference(self.all_fields)))
+
+  def Exec(self, feedback_fn):
+    """Computes the list of nodes and their attributes.
+
+    """
+
+    instance_names = utils.NiceSort(self.cfg.GetInstanceList())
+    instance_list = [self.cfg.GetInstanceInfo(iname) for iname
+                     in instance_names]
+
+    # begin data gathering
+
+    nodes = frozenset([inst.primary_node for inst in instance_list])
+
+    bad_nodes = []
+    if self.dynamic_fields.intersection(self.op.output_fields):
+      live_data = {}
+      node_data = rpc.call_all_instances_info(nodes)
+      for name in nodes:
+        result = node_data[name]
+        if result:
+          live_data.update(result)
+        elif result == False:
+          bad_nodes.append(name)
+        # else no instance is alive
+    else:
+      live_data = dict([(name, {}) for name in instance_names])
+
+    # end data gathering
+
+    output = []
+    for instance in instance_list:
+      iout = []
+      for field in self.op.output_fields:
+        if field == "name":
+          val = instance.name
+        elif field == "os":
+          val = instance.os
+        elif field == "pnode":
+          val = instance.primary_node
+        elif field == "snodes":
+          val = ",".join(instance.secondary_nodes) or "-"
+        elif field == "admin_state":
+          if instance.status == "down":
+            val = "no"
+          else:
+            val = "yes"
+        elif field == "oper_state":
+          if instance.primary_node in bad_nodes:
+            val = "(node down)"
+          else:
+            if live_data.get(instance.name):
+              val = "running"
+            else:
+              val = "stopped"
+        elif field == "admin_ram":
+          val = instance.memory
+        elif field == "oper_ram":
+          if instance.primary_node in bad_nodes:
+            val = "(node down)"
+          elif instance.name in live_data:
+            val = live_data[instance.name].get("memory", "?")
+          else:
+            val = "-"
+        elif field == "disk_template":
+          val = instance.disk_template
+        elif field == "ip":
+          val = instance.nics[0].ip
+        elif field == "bridge":
+          val = instance.nics[0].bridge
+        elif field == "mac":
+          val = instance.nics[0].mac
+        else:
+          raise errors.ParameterError, field
+        val = str(val)
+        iout.append(val)
+      output.append(iout)
+
+    return output
+
+
+class LUFailoverInstance(LogicalUnit):
+  """Failover an instance.
+
+  """
+  HPATH = "instance-failover"
+  HTYPE = constants.HTYPE_INSTANCE
+  _OP_REQP = ["instance_name", "ignore_consistency"]
+
+  def BuildHooksEnv(self):
+    """Build hooks env.
+
+    This runs on master, primary and secondary nodes of the instance.
+
+    """
+    env = {
+      "INSTANCE_NAME": self.op.instance_name,
+      "INSTANCE_PRIMARY": self.instance.primary_node,
+      "INSTANCE_SECONDARIES": " ".join(self.instance.secondary_nodes),
+      "IGNORE_CONSISTENCY": self.op.ignore_consistency,
+      }
+    nl = [self.cfg.GetMaster()] + list(self.instance.secondary_nodes)
+    return env, nl, nl
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    This checks that the instance is in the cluster.
+
+    """
+    instance = self.cfg.GetInstanceInfo(
+      self.cfg.ExpandInstanceName(self.op.instance_name))
+    if instance is None:
+      raise errors.OpPrereqError, ("Instance '%s' not known" %
+                                   self.op.instance_name)
+
+    # check bridge existance
+    brlist = [nic.bridge for nic in instance.nics]
+    if not rpc.call_bridges_exist(instance.primary_node, brlist):
+      raise errors.OpPrereqError, ("one or more target bridges %s does not"
+                                   " exist on destination node '%s'" %
+                                   (brlist, instance.primary_node))
+
+    self.instance = instance
+
+  def Exec(self, feedback_fn):
+    """Failover an instance.
+
+    The failover is done by shutting it down on its present node and
+    starting it on the secondary.
+
+    """
+    instance = self.instance
+
+    source_node = instance.primary_node
+    target_node = instance.secondary_nodes[0]
+
+    feedback_fn("* checking disk consistency between source and target")
+    for dev in instance.disks:
+      # for remote_raid1, these are md over drbd
+      if not _CheckDiskConsistency(self.cfg, dev, target_node, False):
+        if not self.op.ignore_consistency:
+          raise errors.OpExecError, ("Disk %s is degraded on target node,"
+                                     " aborting failover." % dev.iv_name)
+
+    feedback_fn("* checking target node resource availability")
+    nodeinfo = rpc.call_node_info([target_node], self.cfg.GetVGName())
+
+    if not nodeinfo:
+      raise errors.OpExecError, ("Could not contact target node %s." %
+                                 target_node)
+
+    free_memory = int(nodeinfo[target_node]['memory_free'])
+    memory = instance.memory
+    if memory > free_memory:
+      raise errors.OpExecError, ("Not enough memory to create instance %s on"
+                                 " node %s. needed %s MiB, available %s MiB" %
+                                 (instance.name, target_node, memory,
+                                  free_memory))
+
+    feedback_fn("* shutting down instance on source node")
+    logger.Info("Shutting down instance %s on node %s" %
+                (instance.name, source_node))
+
+    if not rpc.call_instance_shutdown(source_node, instance):
+      logger.Error("Could not shutdown instance %s on node %s. Proceeding"
+                   " anyway. Please make sure node %s is down"  %
+                   (instance.name, source_node, source_node))
+
+    feedback_fn("* deactivating the instance's disks on source node")
+    if not _ShutdownInstanceDisks(instance, self.cfg, ignore_primary=True):
+      raise errors.OpExecError, ("Can't shut down the instance's disks.")
+
+    instance.primary_node = target_node
+    # distribute new instance config to the other nodes
+    self.cfg.AddInstance(instance)
+
+    feedback_fn("* activating the instance's disks on target node")
+    logger.Info("Starting instance %s on node %s" %
+                (instance.name, target_node))
+
+    disks_ok, dummy = _AssembleInstanceDisks(instance, self.cfg,
+                                             ignore_secondaries=True)
+    if not disks_ok:
+      _ShutdownInstanceDisks(instance, self.cfg)
+      raise errors.OpExecError, ("Can't activate the instance's disks")
+
+    feedback_fn("* starting the instance on the target node")
+    if not rpc.call_instance_start(target_node, instance, None):
+      _ShutdownInstanceDisks(instance, self.cfg)
+      raise errors.OpExecError("Could not start instance %s on node %s." %
+                               (instance, target_node))
+
+
+def _CreateBlockDevOnPrimary(cfg, node, device):
+  """Create a tree of block devices on the primary node.
+
+  This always creates all devices.
+
+  """
+
+  if device.children:
+    for child in device.children:
+      if not _CreateBlockDevOnPrimary(cfg, node, child):
+        return False
+
+  cfg.SetDiskID(device, node)
+  new_id = rpc.call_blockdev_create(node, device, device.size, True)
+  if not new_id:
+    return False
+  if device.physical_id is None:
+    device.physical_id = new_id
+  return True
+
+
+def _CreateBlockDevOnSecondary(cfg, node, device, force):
+  """Create a tree of block devices on a secondary node.
+
+  If this device type has to be created on secondaries, create it and
+  all its children.
+
+  If not, just recurse to children keeping the same 'force' value.
+
+  """
+  if device.CreateOnSecondary():
+    force = True
+  if device.children:
+    for child in device.children:
+      if not _CreateBlockDevOnSecondary(cfg, node, child, force):
+        return False
+
+  if not force:
+    return True
+  cfg.SetDiskID(device, node)
+  new_id = rpc.call_blockdev_create(node, device, device.size, False)
+  if not new_id:
+    return False
+  if device.physical_id is None:
+    device.physical_id = new_id
+  return True
+
+
+def _GenerateMDDRBDBranch(cfg, vgname, primary, secondary, size, base):
+  """Generate a drbd device complete with its children.
+
+  """
+  port = cfg.AllocatePort()
+  base = "%s_%s" % (base, port)
+  dev_data = objects.Disk(dev_type="lvm", size=size,
+                          logical_id=(vgname, "%s.data" % base))
+  dev_meta = objects.Disk(dev_type="lvm", size=128,
+                          logical_id=(vgname, "%s.meta" % base))
+  drbd_dev = objects.Disk(dev_type="drbd", size=size,
+                          logical_id = (primary, secondary, port),
+                          children = [dev_data, dev_meta])
+  return drbd_dev
+
+
+def _GenerateDiskTemplate(cfg, vgname, template_name,
+                          instance_name, primary_node,
+                          secondary_nodes, disk_sz, swap_sz):
+  """Generate the entire disk layout for a given template type.
+
+  """
+  #TODO: compute space requirements
+
+  if template_name == "diskless":
+    disks = []
+  elif template_name == "plain":
+    if len(secondary_nodes) != 0:
+      raise errors.ProgrammerError("Wrong template configuration")
+    sda_dev = objects.Disk(dev_type="lvm", size=disk_sz,
+                           logical_id=(vgname, "%s.os" % instance_name),
+                           iv_name = "sda")
+    sdb_dev = objects.Disk(dev_type="lvm", size=swap_sz,
+                           logical_id=(vgname, "%s.swap" % instance_name),
+                           iv_name = "sdb")
+    disks = [sda_dev, sdb_dev]
+  elif template_name == "local_raid1":
+    if len(secondary_nodes) != 0:
+      raise errors.ProgrammerError("Wrong template configuration")
+    sda_dev_m1 = objects.Disk(dev_type="lvm", size=disk_sz,
+                              logical_id=(vgname, "%s.os_m1" % instance_name))
+    sda_dev_m2 = objects.Disk(dev_type="lvm", size=disk_sz,
+                              logical_id=(vgname, "%s.os_m2" % instance_name))
+    md_sda_dev = objects.Disk(dev_type="md_raid1", iv_name = "sda",
+                              size=disk_sz,
+                              children = [sda_dev_m1, sda_dev_m2])
+    sdb_dev_m1 = objects.Disk(dev_type="lvm", size=swap_sz,
+                              logical_id=(vgname, "%s.swap_m1" %
+                                          instance_name))
+    sdb_dev_m2 = objects.Disk(dev_type="lvm", size=swap_sz,
+                              logical_id=(vgname, "%s.swap_m2" %
+                                          instance_name))
+    md_sdb_dev = objects.Disk(dev_type="md_raid1", iv_name = "sdb",
+                              size=swap_sz,
+                              children = [sdb_dev_m1, sdb_dev_m2])
+    disks = [md_sda_dev, md_sdb_dev]
+  elif template_name == "remote_raid1":
+    if len(secondary_nodes) != 1:
+      raise errors.ProgrammerError("Wrong template configuration")
+    remote_node = secondary_nodes[0]
+    drbd_sda_dev = _GenerateMDDRBDBranch(cfg, vgname,
+                                         primary_node, remote_node, disk_sz,
+                                         "%s-sda" % instance_name)
+    md_sda_dev = objects.Disk(dev_type="md_raid1", iv_name="sda",
+                              children = [drbd_sda_dev], size=disk_sz)
+    drbd_sdb_dev = _GenerateMDDRBDBranch(cfg, vgname,
+                                         primary_node, remote_node, swap_sz,
+                                         "%s-sdb" % instance_name)
+    md_sdb_dev = objects.Disk(dev_type="md_raid1", iv_name="sdb",
+                              children = [drbd_sdb_dev], size=swap_sz)
+    disks = [md_sda_dev, md_sdb_dev]
+  else:
+    raise errors.ProgrammerError("Invalid disk template '%s'" % template_name)
+  return disks
+
+
+def _CreateDisks(cfg, instance):
+  """Create all disks for an instance.
+
+  This abstracts away some work from AddInstance.
+
+  Args:
+    instance: the instance object
+
+  Returns:
+    True or False showing the success of the creation process
+
+  """
+  for device in instance.disks:
+    logger.Info("creating volume %s for instance %s" %
+              (device.iv_name, instance.name))
+    #HARDCODE
+    for secondary_node in instance.secondary_nodes:
+      if not _CreateBlockDevOnSecondary(cfg, secondary_node, device, False):
+        logger.Error("failed to create volume %s (%s) on secondary node %s!" %
+                     (device.iv_name, device, secondary_node))
+        return False
+    #HARDCODE
+    if not _CreateBlockDevOnPrimary(cfg, instance.primary_node, device):
+      logger.Error("failed to create volume %s on primary!" %
+                   device.iv_name)
+      return False
+  return True
+
+
+def _RemoveDisks(instance, cfg):
+  """Remove all disks for an instance.
+
+  This abstracts away some work from `AddInstance()` and
+  `RemoveInstance()`. Note that in case some of the devices couldn't
+  be remove, the removal will continue with the other ones (compare
+  with `_CreateDisks()`).
+
+  Args:
+    instance: the instance object
+
+  Returns:
+    True or False showing the success of the removal proces
+
+  """
+  logger.Info("removing block devices for instance %s" % instance.name)
+
+  result = True
+  for device in instance.disks:
+    for node, disk in device.ComputeNodeTree(instance.primary_node):
+      cfg.SetDiskID(disk, node)
+      if not rpc.call_blockdev_remove(node, disk):
+        logger.Error("could not remove block device %s on node %s,"
+                     " continuing anyway" %
+                     (device.iv_name, node))
+        result = False
+  return result
+
+
+class LUCreateInstance(LogicalUnit):
+  """Create an instance.
+
+  """
+  HPATH = "instance-add"
+  HTYPE = constants.HTYPE_INSTANCE
+  _OP_REQP = ["instance_name", "mem_size", "disk_size", "pnode",
+              "disk_template", "swap_size", "mode", "start", "vcpus",
+              "wait_for_sync"]
+
+  def BuildHooksEnv(self):
+    """Build hooks env.
+
+    This runs on master, primary and secondary nodes of the instance.
+
+    """
+    env = {
+      "INSTANCE_NAME": self.op.instance_name,
+      "INSTANCE_PRIMARY": self.op.pnode,
+      "INSTANCE_SECONDARIES": " ".join(self.secondaries),
+      "DISK_TEMPLATE": self.op.disk_template,
+      "MEM_SIZE": self.op.mem_size,
+      "DISK_SIZE": self.op.disk_size,
+      "SWAP_SIZE": self.op.swap_size,
+      "VCPUS": self.op.vcpus,
+      "BRIDGE": self.op.bridge,
+      "INSTANCE_ADD_MODE": self.op.mode,
+      }
+    if self.op.mode == constants.INSTANCE_IMPORT:
+      env["SRC_NODE"] = self.op.src_node
+      env["SRC_PATH"] = self.op.src_path
+      env["SRC_IMAGE"] = self.src_image
+    if self.inst_ip:
+      env["INSTANCE_IP"] = self.inst_ip
+
+    nl = ([self.cfg.GetMaster(), self.op.pnode] +
+          self.secondaries)
+    return env, nl, nl
+
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    """
+    if self.op.mode not in (constants.INSTANCE_CREATE,
+                            constants.INSTANCE_IMPORT):
+      raise errors.OpPrereqError, ("Invalid instance creation mode '%s'" %
+                                   self.op.mode)
+
+    if self.op.mode == constants.INSTANCE_IMPORT:
+      src_node = getattr(self.op, "src_node", None)
+      src_path = getattr(self.op, "src_path", None)
+      if src_node is None or src_path is None:
+        raise errors.OpPrereqError, ("Importing an instance requires source"
+                                     " node and path options")
+      src_node_full = self.cfg.ExpandNodeName(src_node)
+      if src_node_full is None:
+        raise errors.OpPrereqError, ("Unknown source node '%s'" % src_node)
+      self.op.src_node = src_node = src_node_full
+
+      if not os.path.isabs(src_path):
+        raise errors.OpPrereqError, ("The source path must be absolute")
+
+      export_info = rpc.call_export_info(src_node, src_path)
+
+      if not export_info:
+        raise errors.OpPrereqError, ("No export found in dir %s" % src_path)
+
+      if not export_info.has_section(constants.INISECT_EXP):
+        raise errors.ProgrammerError, ("Corrupted export config")
+
+      ei_version = export_info.get(constants.INISECT_EXP, 'version')
+      if (int(ei_version) != constants.EXPORT_VERSION):
+        raise errors.OpPrereqError, ("Wrong export version %s (wanted %d)" %
+                                     (ei_version, constants.EXPORT_VERSION))
+
+      if int(export_info.get(constants.INISECT_INS, 'disk_count')) > 1:
+        raise errors.OpPrereqError, ("Can't import instance with more than"
+                                     " one data disk")
+
+      # FIXME: are the old os-es, disk sizes, etc. useful?
+      self.op.os_type = export_info.get(constants.INISECT_EXP, 'os')
+      diskimage = os.path.join(src_path, export_info.get(constants.INISECT_INS,
+                                                         'disk0_dump'))
+      self.src_image = diskimage
+    else: # INSTANCE_CREATE
+      if getattr(self.op, "os_type", None) is None:
+        raise errors.OpPrereqError, ("No guest OS specified")
+
+    # check primary node
+    pnode = self.cfg.GetNodeInfo(self.cfg.ExpandNodeName(self.op.pnode))
+    if pnode is None:
+      raise errors.OpPrereqError, ("Primary node '%s' is uknown" %
+                                   self.op.pnode)
+    self.op.pnode = pnode.name
+    self.pnode = pnode
+    self.secondaries = []
+    # disk template and mirror node verification
+    if self.op.disk_template not in constants.DISK_TEMPLATES:
+      raise errors.OpPrereqError, ("Invalid disk template name")
+
+    if self.op.disk_template == constants.DT_REMOTE_RAID1:
+      if getattr(self.op, "snode", None) is None:
+        raise errors.OpPrereqError, ("The 'remote_raid1' disk template needs"
+                                     " a mirror node")
+
+      snode_name = self.cfg.ExpandNodeName(self.op.snode)
+      if snode_name is None:
+        raise errors.OpPrereqError, ("Unknown secondary node '%s'" %
+                                     self.op.snode)
+      elif snode_name == pnode.name:
+        raise errors.OpPrereqError, ("The secondary node cannot be"
+                                     " the primary node.")
+      self.secondaries.append(snode_name)
+
+    # os verification
+    os_obj = rpc.call_os_get([pnode.name], self.op.os_type)[pnode.name]
+    if not isinstance(os_obj, objects.OS):
+      raise errors.OpPrereqError, ("OS '%s' not in supported os list for"
+                                   " primary node"  % self.op.os_type)
+
+    # instance verification
+    hostname1 = utils.LookupHostname(self.op.instance_name)
+    if not hostname1:
+      raise errors.OpPrereqError, ("Instance name '%s' not found in dns" %
+                                   self.op.instance_name)
+
+    self.op.instance_name = instance_name = hostname1['hostname']
+    instance_list = self.cfg.GetInstanceList()
+    if instance_name in instance_list:
+      raise errors.OpPrereqError, ("Instance '%s' is already in the cluster" %
+                                   instance_name)
+
+    ip = getattr(self.op, "ip", None)
+    if ip is None or ip.lower() == "none":
+      inst_ip = None
+    elif ip.lower() == "auto":
+      inst_ip = hostname1['ip']
+    else:
+      if not utils.IsValidIP(ip):
+        raise errors.OpPrereqError, ("given IP address '%s' doesn't look"
+                                     " like a valid IP" % ip)
+      inst_ip = ip
+    self.inst_ip = inst_ip
+
+    command = ["fping", "-q", hostname1['ip']]
+    result = utils.RunCmd(command)
+    if not result.failed:
+      raise errors.OpPrereqError, ("IP %s of instance %s already in use" %
+                                   (hostname1['ip'], instance_name))
+
+    # bridge verification
+    bridge = getattr(self.op, "bridge", None)
+    if bridge is None:
+      self.op.bridge = self.cfg.GetDefBridge()
+    else:
+      self.op.bridge = bridge
+
+    if not rpc.call_bridges_exist(self.pnode.name, [self.op.bridge]):
+      raise errors.OpPrereqError, ("target bridge '%s' does not exist on"
+                                   " destination node '%s'" %
+                                   (self.op.bridge, pnode.name))
+
+    if self.op.start:
+      self.instance_status = 'up'
+    else:
+      self.instance_status = 'down'
+
+  def Exec(self, feedback_fn):
+    """Create and add the instance to the cluster.
+
+    """
+    instance = self.op.instance_name
+    pnode_name = self.pnode.name
+
+    nic = objects.NIC(bridge=self.op.bridge, mac=self.cfg.GenerateMAC())
+    if self.inst_ip is not None:
+      nic.ip = self.inst_ip
+
+    disks = _GenerateDiskTemplate(self.cfg, self.cfg.GetVGName(),
+                                  self.op.disk_template,
+                                  instance, pnode_name,
+                                  self.secondaries, self.op.disk_size,
+                                  self.op.swap_size)
+
+    iobj = objects.Instance(name=instance, os=self.op.os_type,
+                            primary_node=pnode_name,
+                            memory=self.op.mem_size,
+                            vcpus=self.op.vcpus,
+                            nics=[nic], disks=disks,
+                            disk_template=self.op.disk_template,
+                            status=self.instance_status,
+                            )
+
+    feedback_fn("* creating instance disks...")
+    if not _CreateDisks(self.cfg, iobj):
+      _RemoveDisks(iobj, self.cfg)
+      raise errors.OpExecError, ("Device creation failed, reverting...")
+
+    feedback_fn("adding instance %s to cluster config" % instance)
+
+    self.cfg.AddInstance(iobj)
+
+    if self.op.wait_for_sync:
+      disk_abort = not _WaitForSync(self.cfg, iobj)
+    elif iobj.disk_template == "remote_raid1":
+      # make sure the disks are not degraded (still sync-ing is ok)
+      time.sleep(15)
+      feedback_fn("* checking mirrors status")
+      disk_abort = not _WaitForSync(self.cfg, iobj, oneshot=True)
+    else:
+      disk_abort = False
+
+    if disk_abort:
+      _RemoveDisks(iobj, self.cfg)
+      self.cfg.RemoveInstance(iobj.name)
+      raise errors.OpExecError, ("There are some degraded disks for"
+                                      " this instance")
+
+    feedback_fn("creating os for instance %s on node %s" %
+                (instance, pnode_name))
+
+    if iobj.disk_template != constants.DT_DISKLESS:
+      if self.op.mode == constants.INSTANCE_CREATE:
+        feedback_fn("* running the instance OS create scripts...")
+        if not rpc.call_instance_os_add(pnode_name, iobj, "sda", "sdb"):
+          raise errors.OpExecError, ("could not add os for instance %s"
+                                          " on node %s" %
+                                          (instance, pnode_name))
+
+      elif self.op.mode == constants.INSTANCE_IMPORT:
+        feedback_fn("* running the instance OS import scripts...")
+        src_node = self.op.src_node
+        src_image = self.src_image
+        if not rpc.call_instance_os_import(pnode_name, iobj, "sda", "sdb",
+                                                src_node, src_image):
+          raise errors.OpExecError, ("Could not import os for instance"
+                                          " %s on node %s" %
+                                          (instance, pnode_name))
+      else:
+        # also checked in the prereq part
+        raise errors.ProgrammerError, ("Unknown OS initialization mode '%s'"
+                                       % self.op.mode)
+
+    if self.op.start:
+      logger.Info("starting instance %s on node %s" % (instance, pnode_name))
+      feedback_fn("* starting instance...")
+      if not rpc.call_instance_start(pnode_name, iobj, None):
+        raise errors.OpExecError, ("Could not start instance")
+
+
+class LUConnectConsole(NoHooksLU):
+  """Connect to an instance's console.
+
+  This is somewhat special in that it returns the command line that
+  you need to run on the master node in order to connect to the
+  console.
+
+  """
+  _OP_REQP = ["instance_name"]
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    This checks that the instance is in the cluster.
+
+    """
+    instance = self.cfg.GetInstanceInfo(
+      self.cfg.ExpandInstanceName(self.op.instance_name))
+    if instance is None:
+      raise errors.OpPrereqError, ("Instance '%s' not known" %
+                                   self.op.instance_name)
+    self.instance = instance
+
+  def Exec(self, feedback_fn):
+    """Connect to the console of an instance
+
+    """
+    instance = self.instance
+    node = instance.primary_node
+
+    node_insts = rpc.call_instance_list([node])[node]
+    if node_insts is False:
+      raise errors.OpExecError, ("Can't connect to node %s." % node)
+
+    if instance.name not in node_insts:
+      raise errors.OpExecError, ("Instance %s is not running." % instance.name)
+
+    logger.Debug("connecting to console of %s on %s" % (instance.name, node))
+
+    hyper = hypervisor.GetHypervisor()
+    console_cmd = hyper.GetShellCommandForConsole(instance.name)
+    return node, console_cmd
+
+
+class LUAddMDDRBDComponent(LogicalUnit):
+  """Adda new mirror member to an instance's disk.
+
+  """
+  HPATH = "mirror-add"
+  HTYPE = constants.HTYPE_INSTANCE
+  _OP_REQP = ["instance_name", "remote_node", "disk_name"]
+
+  def BuildHooksEnv(self):
+    """Build hooks env.
+
+    This runs on the master, the primary and all the secondaries.
+
+    """
+    env = {
+      "INSTANCE_NAME": self.op.instance_name,
+      "NEW_SECONDARY": self.op.remote_node,
+      "DISK_NAME": self.op.disk_name,
+      }
+    nl = [self.cfg.GetMaster(), self.instance.primary_node,
+          self.op.remote_node,] + list(self.instance.secondary_nodes)
+    return env, nl, nl
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    This checks that the instance is in the cluster.
+
+    """
+    instance = self.cfg.GetInstanceInfo(
+      self.cfg.ExpandInstanceName(self.op.instance_name))
+    if instance is None:
+      raise errors.OpPrereqError, ("Instance '%s' not known" %
+                                   self.op.instance_name)
+    self.instance = instance
+
+    remote_node = self.cfg.ExpandNodeName(self.op.remote_node)
+    if remote_node is None:
+      raise errors.OpPrereqError, ("Node '%s' not known" % self.op.remote_node)
+    self.remote_node = remote_node
+
+    if remote_node == instance.primary_node:
+      raise errors.OpPrereqError, ("The specified node is the primary node of"
+                                   " the instance.")
+
+    if instance.disk_template != constants.DT_REMOTE_RAID1:
+      raise errors.OpPrereqError, ("Instance's disk layout is not"
+                                   " remote_raid1.")
+    for disk in instance.disks:
+      if disk.iv_name == self.op.disk_name:
+        break
+    else:
+      raise errors.OpPrereqError, ("Can't find this device ('%s') in the"
+                                   " instance." % self.op.disk_name)
+    if len(disk.children) > 1:
+      raise errors.OpPrereqError, ("The device already has two slave"
+                                   " devices.\n"
+                                   "This would create a 3-disk raid1"
+                                   " which we don't allow.")
+    self.disk = disk
+
+  def Exec(self, feedback_fn):
+    """Add the mirror component
+
+    """
+    disk = self.disk
+    instance = self.instance
+
+    remote_node = self.remote_node
+    new_drbd = _GenerateMDDRBDBranch(self.cfg, instance.primary_node,
+                                     remote_node, disk.size, "%s-%s" %
+                                     (instance.name, self.op.disk_name))
+
+    logger.Info("adding new mirror component on secondary")
+    #HARDCODE
+    if not _CreateBlockDevOnSecondary(self.cfg, remote_node, new_drbd, False):
+      raise errors.OpExecError, ("Failed to create new component on secondary"
+                                 " node %s" % remote_node)
+
+    logger.Info("adding new mirror component on primary")
+    #HARDCODE
+    if not _CreateBlockDevOnPrimary(self.cfg, instance.primary_node, new_drbd):
+      # remove secondary dev
+      self.cfg.SetDiskID(new_drbd, remote_node)
+      rpc.call_blockdev_remove(remote_node, new_drbd)
+      raise errors.OpExecError, ("Failed to create volume on primary")
+
+    # the device exists now
+    # call the primary node to add the mirror to md
+    logger.Info("adding new mirror component to md")
+    if not rpc.call_blockdev_addchild(instance.primary_node,
+                                           disk, new_drbd):
+      logger.Error("Can't add mirror compoment to md!")
+      self.cfg.SetDiskID(new_drbd, remote_node)
+      if not rpc.call_blockdev_remove(remote_node, new_drbd):
+        logger.Error("Can't rollback on secondary")
+      self.cfg.SetDiskID(new_drbd, instance.primary_node)
+      if not rpc.call_blockdev_remove(instance.primary_node, new_drbd):
+        logger.Error("Can't rollback on primary")
+      raise errors.OpExecError, "Can't add mirror component to md array"
+
+    disk.children.append(new_drbd)
+
+    self.cfg.AddInstance(instance)
+
+    _WaitForSync(self.cfg, instance)
+
+    return 0
+
+
+class LURemoveMDDRBDComponent(LogicalUnit):
+  """Remove a component from a remote_raid1 disk.
+
+  """
+  HPATH = "mirror-remove"
+  HTYPE = constants.HTYPE_INSTANCE
+  _OP_REQP = ["instance_name", "disk_name", "disk_id"]
+
+  def BuildHooksEnv(self):
+    """Build hooks env.
+
+    This runs on the master, the primary and all the secondaries.
+
+    """
+    env = {
+      "INSTANCE_NAME": self.op.instance_name,
+      "DISK_NAME": self.op.disk_name,
+      "DISK_ID": self.op.disk_id,
+      "OLD_SECONDARY": self.old_secondary,
+      }
+    nl = [self.cfg.GetMaster(),
+          self.instance.primary_node] + list(self.instance.secondary_nodes)
+    return env, nl, nl
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    This checks that the instance is in the cluster.
+
+    """
+    instance = self.cfg.GetInstanceInfo(
+      self.cfg.ExpandInstanceName(self.op.instance_name))
+    if instance is None:
+      raise errors.OpPrereqError, ("Instance '%s' not known" %
+                                   self.op.instance_name)
+    self.instance = instance
+
+    if instance.disk_template != constants.DT_REMOTE_RAID1:
+      raise errors.OpPrereqError, ("Instance's disk layout is not"
+                                   " remote_raid1.")
+    for disk in instance.disks:
+      if disk.iv_name == self.op.disk_name:
+        break
+    else:
+      raise errors.OpPrereqError, ("Can't find this device ('%s') in the"
+                                   " instance." % self.op.disk_name)
+    for child in disk.children:
+      if child.dev_type == "drbd" and child.logical_id[2] == self.op.disk_id:
+        break
+    else:
+      raise errors.OpPrereqError, ("Can't find the device with this port.")
+
+    if len(disk.children) < 2:
+      raise errors.OpPrereqError, ("Cannot remove the last component from"
+                                   " a mirror.")
+    self.disk = disk
+    self.child = child
+    if self.child.logical_id[0] == instance.primary_node:
+      oid = 1
+    else:
+      oid = 0
+    self.old_secondary = self.child.logical_id[oid]
+
+  def Exec(self, feedback_fn):
+    """Remove the mirror component
+
+    """
+    instance = self.instance
+    disk = self.disk
+    child = self.child
+    logger.Info("remove mirror component")
+    self.cfg.SetDiskID(disk, instance.primary_node)
+    if not rpc.call_blockdev_removechild(instance.primary_node,
+                                              disk, child):
+      raise errors.OpExecError, ("Can't remove child from mirror.")
+
+    for node in child.logical_id[:2]:
+      self.cfg.SetDiskID(child, node)
+      if not rpc.call_blockdev_remove(node, child):
+        logger.Error("Warning: failed to remove device from node %s,"
+                     " continuing operation." % node)
+
+    disk.children.remove(child)
+    self.cfg.AddInstance(instance)
+
+
+class LUReplaceDisks(LogicalUnit):
+  """Replace the disks of an instance.
+
+  """
+  HPATH = "mirrors-replace"
+  HTYPE = constants.HTYPE_INSTANCE
+  _OP_REQP = ["instance_name"]
+
+  def BuildHooksEnv(self):
+    """Build hooks env.
+
+    This runs on the master, the primary and all the secondaries.
+
+    """
+    env = {
+      "INSTANCE_NAME": self.op.instance_name,
+      "NEW_SECONDARY": self.op.remote_node,
+      "OLD_SECONDARY": self.instance.secondary_nodes[0],
+      }
+    nl = [self.cfg.GetMaster(),
+          self.instance.primary_node] + list(self.instance.secondary_nodes)
+    return env, nl, nl
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    This checks that the instance is in the cluster.
+
+    """
+    instance = self.cfg.GetInstanceInfo(
+      self.cfg.ExpandInstanceName(self.op.instance_name))
+    if instance is None:
+      raise errors.OpPrereqError, ("Instance '%s' not known" %
+                                   self.op.instance_name)
+    self.instance = instance
+
+    if instance.disk_template != constants.DT_REMOTE_RAID1:
+      raise errors.OpPrereqError, ("Instance's disk layout is not"
+                                   " remote_raid1.")
+
+    if len(instance.secondary_nodes) != 1:
+      raise errors.OpPrereqError, ("The instance has a strange layout,"
+                                   " expected one secondary but found %d" %
+                                   len(instance.secondary_nodes))
+
+    remote_node = getattr(self.op, "remote_node", None)
+    if remote_node is None:
+      remote_node = instance.secondary_nodes[0]
+    else:
+      remote_node = self.cfg.ExpandNodeName(remote_node)
+      if remote_node is None:
+        raise errors.OpPrereqError, ("Node '%s' not known" %
+                                     self.op.remote_node)
+    if remote_node == instance.primary_node:
+      raise errors.OpPrereqError, ("The specified node is the primary node of"
+                                   " the instance.")
+    self.op.remote_node = remote_node
+
+  def Exec(self, feedback_fn):
+    """Replace the disks of an instance.
+
+    """
+    instance = self.instance
+    iv_names = {}
+    # start of work
+    remote_node = self.op.remote_node
+    cfg = self.cfg
+    for dev in instance.disks:
+      size = dev.size
+      new_drbd = _GenerateMDDRBDBranch(cfg, instance.primary_node,
+                                       remote_node, size,
+                                       "%s-%s" % (instance.name, dev.iv_name))
+      iv_names[dev.iv_name] = (dev, dev.children[0], new_drbd)
+      logger.Info("adding new mirror component on secondary for %s" %
+                  dev.iv_name)
+      #HARDCODE
+      if not _CreateBlockDevOnSecondary(cfg, remote_node, new_drbd, False):
+        raise errors.OpExecError, ("Failed to create new component on"
+                                   " secondary node %s\n"
+                                   "Full abort, cleanup manually!" %
+                                   remote_node)
+
+      logger.Info("adding new mirror component on primary")
+      #HARDCODE
+      if not _CreateBlockDevOnPrimary(cfg, instance.primary_node, new_drbd):
+        # remove secondary dev
+        cfg.SetDiskID(new_drbd, remote_node)
+        rpc.call_blockdev_remove(remote_node, new_drbd)
+        raise errors.OpExecError("Failed to create volume on primary!\n"
+                                 "Full abort, cleanup manually!!")
+
+      # the device exists now
+      # call the primary node to add the mirror to md
+      logger.Info("adding new mirror component to md")
+      if not rpc.call_blockdev_addchild(instance.primary_node, dev,
+                                             new_drbd):
+        logger.Error("Can't add mirror compoment to md!")
+        cfg.SetDiskID(new_drbd, remote_node)
+        if not rpc.call_blockdev_remove(remote_node, new_drbd):
+          logger.Error("Can't rollback on secondary")
+        cfg.SetDiskID(new_drbd, instance.primary_node)
+        if not rpc.call_blockdev_remove(instance.primary_node, new_drbd):
+          logger.Error("Can't rollback on primary")
+        raise errors.OpExecError, ("Full abort, cleanup manually!!")
+
+      dev.children.append(new_drbd)
+      cfg.AddInstance(instance)
+
+    # this can fail as the old devices are degraded and _WaitForSync
+    # does a combined result over all disks, so we don't check its
+    # return value
+    _WaitForSync(cfg, instance, unlock=True)
+
+    # so check manually all the devices
+    for name in iv_names:
+      dev, child, new_drbd = iv_names[name]
+      cfg.SetDiskID(dev, instance.primary_node)
+      is_degr = rpc.call_blockdev_find(instance.primary_node, dev)[5]
+      if is_degr:
+        raise errors.OpExecError, ("MD device %s is degraded!" % name)
+      cfg.SetDiskID(new_drbd, instance.primary_node)
+      is_degr = rpc.call_blockdev_find(instance.primary_node, new_drbd)[5]
+      if is_degr:
+        raise errors.OpExecError, ("New drbd device %s is degraded!" % name)
+
+    for name in iv_names:
+      dev, child, new_drbd = iv_names[name]
+      logger.Info("remove mirror %s component" % name)
+      cfg.SetDiskID(dev, instance.primary_node)
+      if not rpc.call_blockdev_removechild(instance.primary_node,
+                                                dev, child):
+        logger.Error("Can't remove child from mirror, aborting"
+                     " *this device cleanup*.\nYou need to cleanup manually!!")
+        continue
+
+      for node in child.logical_id[:2]:
+        logger.Info("remove child device on %s" % node)
+        cfg.SetDiskID(child, node)
+        if not rpc.call_blockdev_remove(node, child):
+          logger.Error("Warning: failed to remove device from node %s,"
+                       " continuing operation." % node)
+
+      dev.children.remove(child)
+
+      cfg.AddInstance(instance)
+
+
+class LUQueryInstanceData(NoHooksLU):
+  """Query runtime instance data.
+
+  """
+  _OP_REQP = ["instances"]
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    This only checks the optional instance list against the existing names.
+
+    """
+    if not isinstance(self.op.instances, list):
+      raise errors.OpPrereqError, "Invalid argument type 'instances'"
+    if self.op.instances:
+      self.wanted_instances = []
+      names = self.op.instances
+      for name in names:
+        instance = self.cfg.GetInstanceInfo(self.cfg.ExpandInstanceName(name))
+        if instance is None:
+          raise errors.OpPrereqError, ("No such instance name '%s'" % name)
+      self.wanted_instances.append(instance)
+    else:
+      self.wanted_instances = [self.cfg.GetInstanceInfo(name) for name
+                               in self.cfg.GetInstanceList()]
+    return
+
+
+  def _ComputeDiskStatus(self, instance, snode, dev):
+    """Compute block device status.
+
+    """
+    self.cfg.SetDiskID(dev, instance.primary_node)
+    dev_pstatus = rpc.call_blockdev_find(instance.primary_node, dev)
+    if dev.dev_type == "drbd":
+      # we change the snode then (otherwise we use the one passed in)
+      if dev.logical_id[0] == instance.primary_node:
+        snode = dev.logical_id[1]
+      else:
+        snode = dev.logical_id[0]
+
+    if snode:
+      self.cfg.SetDiskID(dev, snode)
+      dev_sstatus = rpc.call_blockdev_find(snode, dev)
+    else:
+      dev_sstatus = None
+
+    if dev.children:
+      dev_children = [self._ComputeDiskStatus(instance, snode, child)
+                      for child in dev.children]
+    else:
+      dev_children = []
+
+    data = {
+      "iv_name": dev.iv_name,
+      "dev_type": dev.dev_type,
+      "logical_id": dev.logical_id,
+      "physical_id": dev.physical_id,
+      "pstatus": dev_pstatus,
+      "sstatus": dev_sstatus,
+      "children": dev_children,
+      }
+
+    return data
+
+  def Exec(self, feedback_fn):
+    """Gather and return data"""
+
+    result = {}
+    for instance in self.wanted_instances:
+      remote_info = rpc.call_instance_info(instance.primary_node,
+                                                instance.name)
+      if remote_info and "state" in remote_info:
+        remote_state = "up"
+      else:
+        remote_state = "down"
+      if instance.status == "down":
+        config_state = "down"
+      else:
+        config_state = "up"
+
+      disks = [self._ComputeDiskStatus(instance, None, device)
+               for device in instance.disks]
+
+      idict = {
+        "name": instance.name,
+        "config_state": config_state,
+        "run_state": remote_state,
+        "pnode": instance.primary_node,
+        "snodes": instance.secondary_nodes,
+        "os": instance.os,
+        "memory": instance.memory,
+        "nics": [(nic.mac, nic.ip, nic.bridge) for nic in instance.nics],
+        "disks": disks,
+        }
+
+      result[instance.name] = idict
+
+    return result
+
+
+class LUQueryNodeData(NoHooksLU):
+  """Logical unit for querying node data.
+
+  """
+  _OP_REQP = ["nodes"]
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    This only checks the optional node list against the existing names.
+
+    """
+    if not isinstance(self.op.nodes, list):
+      raise errors.OpPrereqError, "Invalid argument type 'nodes'"
+    if self.op.nodes:
+      self.wanted_nodes = []
+      names = self.op.nodes
+      for name in names:
+        node = self.cfg.GetNodeInfo(self.cfg.ExpandNodeName(name))
+        if node is None:
+          raise errors.OpPrereqError, ("No such node name '%s'" % name)
+      self.wanted_nodes.append(node)
+    else:
+      self.wanted_nodes = [self.cfg.GetNodeInfo(name) for name
+                           in self.cfg.GetNodeList()]
+    return
+
+  def Exec(self, feedback_fn):
+    """Compute and return the list of nodes.
+
+    """
+
+    ilist = [self.cfg.GetInstanceInfo(iname) for iname
+             in self.cfg.GetInstanceList()]
+    result = []
+    for node in self.wanted_nodes:
+      result.append((node.name, node.primary_ip, node.secondary_ip,
+                     [inst.name for inst in ilist
+                      if inst.primary_node == node.name],
+                     [inst.name for inst in ilist
+                      if node.name in inst.secondary_nodes],
+                     ))
+    return result
+
+
+class LUSetInstanceParms(LogicalUnit):
+  """Modifies an instances's parameters.
+
+  """
+  HPATH = "instance-modify"
+  HTYPE = constants.HTYPE_INSTANCE
+  _OP_REQP = ["instance_name"]
+
+  def BuildHooksEnv(self):
+    """Build hooks env.
+
+    This runs on the master, primary and secondaries.
+
+    """
+    env = {
+      "INSTANCE_NAME": self.op.instance_name,
+      }
+    if self.mem:
+      env["MEM_SIZE"] = self.mem
+    if self.vcpus:
+      env["VCPUS"] = self.vcpus
+    if self.do_ip:
+      env["INSTANCE_IP"] = self.ip
+    if self.bridge:
+      env["BRIDGE"] = self.bridge
+
+    nl = [self.cfg.GetMaster(),
+          self.instance.primary_node] + list(self.instance.secondary_nodes)
+
+    return env, nl, nl
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    This only checks the instance list against the existing names.
+
+    """
+    self.mem = getattr(self.op, "mem", None)
+    self.vcpus = getattr(self.op, "vcpus", None)
+    self.ip = getattr(self.op, "ip", None)
+    self.bridge = getattr(self.op, "bridge", None)
+    if [self.mem, self.vcpus, self.ip, self.bridge].count(None) == 4:
+      raise errors.OpPrereqError, ("No changes submitted")
+    if self.mem is not None:
+      try:
+        self.mem = int(self.mem)
+      except ValueError, err:
+        raise errors.OpPrereqError, ("Invalid memory size: %s" % str(err))
+    if self.vcpus is not None:
+      try:
+        self.vcpus = int(self.vcpus)
+      except ValueError, err:
+        raise errors.OpPrereqError, ("Invalid vcpus number: %s" % str(err))
+    if self.ip is not None:
+      self.do_ip = True
+      if self.ip.lower() == "none":
+        self.ip = None
+      else:
+        if not utils.IsValidIP(self.ip):
+          raise errors.OpPrereqError, ("Invalid IP address '%s'." % self.ip)
+    else:
+      self.do_ip = False
+
+    instance = self.cfg.GetInstanceInfo(
+      self.cfg.ExpandInstanceName(self.op.instance_name))
+    if instance is None:
+      raise errors.OpPrereqError, ("No such instance name '%s'" %
+                                   self.op.instance_name)
+    self.op.instance_name = instance.name
+    self.instance = instance
+    return
+
+  def Exec(self, feedback_fn):
+    """Modifies an instance.
+
+    All parameters take effect only at the next restart of the instance.
+    """
+    result = []
+    instance = self.instance
+    if self.mem:
+      instance.memory = self.mem
+      result.append(("mem", self.mem))
+    if self.vcpus:
+      instance.vcpus = self.vcpus
+      result.append(("vcpus",  self.vcpus))
+    if self.do_ip:
+      instance.nics[0].ip = self.ip
+      result.append(("ip", self.ip))
+    if self.bridge:
+      instance.nics[0].bridge = self.bridge
+      result.append(("bridge", self.bridge))
+
+    self.cfg.AddInstance(instance)
+
+    return result
+
+
+class LUQueryExports(NoHooksLU):
+  """Query the exports list
+
+  """
+  _OP_REQP = []
+
+  def CheckPrereq(self):
+    """Check that the nodelist contains only existing nodes.
+
+    """
+    nodes = getattr(self.op, "nodes", None)
+    if not nodes:
+      self.op.nodes = self.cfg.GetNodeList()
+    else:
+      expnodes = [self.cfg.ExpandNodeName(node) for node in nodes]
+      if expnodes.count(None) > 0:
+        raise errors.OpPrereqError, ("At least one of the given nodes %s"
+                                     " is unknown" % self.op.nodes)
+      self.op.nodes = expnodes
+
+  def Exec(self, feedback_fn):
+
+    """Compute the list of all the exported system images.
+
+    Returns:
+      a dictionary with the structure node->(export-list)
+      where export-list is a list of the instances exported on
+      that node.
+
+    """
+    return rpc.call_export_list(self.op.nodes)
+
+
+class LUExportInstance(LogicalUnit):
+  """Export an instance to an image in the cluster.
+
+  """
+  HPATH = "instance-export"
+  HTYPE = constants.HTYPE_INSTANCE
+  _OP_REQP = ["instance_name", "target_node", "shutdown"]
+
+  def BuildHooksEnv(self):
+    """Build hooks env.
+
+    This will run on the master, primary node and target node.
+
+    """
+    env = {
+      "INSTANCE_NAME": self.op.instance_name,
+      "EXPORT_NODE": self.op.target_node,
+      "EXPORT_DO_SHUTDOWN": self.op.shutdown,
+      }
+    nl = [self.cfg.GetMaster(), self.instance.primary_node,
+          self.op.target_node]
+    return env, nl, nl
+
+  def CheckPrereq(self):
+    """Check prerequisites.
+
+    This checks that the instance name is a valid one.
+
+    """
+    instance_name = self.cfg.ExpandInstanceName(self.op.instance_name)
+    self.instance = self.cfg.GetInstanceInfo(instance_name)
+    if self.instance is None:
+      raise errors.OpPrereqError, ("Instance '%s' not found" %
+                                   self.op.instance_name)
+
+    # node verification
+    dst_node_short = self.cfg.ExpandNodeName(self.op.target_node)
+    self.dst_node = self.cfg.GetNodeInfo(dst_node_short)
+
+    if self.dst_node is None:
+      raise errors.OpPrereqError, ("Destination node '%s' is uknown." %
+                                   self.op.target_node)
+    self.op.target_node = self.dst_node.name
+
+  def Exec(self, feedback_fn):
+    """Export an instance to an image in the cluster.
+
+    """
+    instance = self.instance
+    dst_node = self.dst_node
+    src_node = instance.primary_node
+    # shutdown the instance, unless requested not to do so
+    if self.op.shutdown:
+      op = opcodes.OpShutdownInstance(instance_name=instance.name)
+      self.processor.ChainOpCode(op, feedback_fn)
+
+    vgname = self.cfg.GetVGName()
+
+    snap_disks = []
+
+    try:
+      for disk in instance.disks:
+        if disk.iv_name == "sda":
+          # new_dev_name will be a snapshot of an lvm leaf of the one we passed
+          new_dev_name = rpc.call_blockdev_snapshot(src_node, disk)
+
+          if not new_dev_name:
+            logger.Error("could not snapshot block device %s on node %s" %
+                         (disk.logical_id[1], src_node))
+          else:
+            new_dev = objects.Disk(dev_type="lvm", size=disk.size,
+                                      logical_id=(vgname, new_dev_name),
+                                      physical_id=(vgname, new_dev_name),
+                                      iv_name=disk.iv_name)
+            snap_disks.append(new_dev)
+
+    finally:
+      if self.op.shutdown:
+        op = opcodes.OpStartupInstance(instance_name=instance.name,
+                                       force=False)
+        self.processor.ChainOpCode(op, feedback_fn)
+
+    # TODO: check for size
+
+    for dev in snap_disks:
+      if not rpc.call_snapshot_export(src_node, dev, dst_node.name,
+                                           instance):
+        logger.Error("could not export block device %s from node"
+                     " %s to node %s" %
+                     (dev.logical_id[1], src_node, dst_node.name))
+      if not rpc.call_blockdev_remove(src_node, dev):
+        logger.Error("could not remove snapshot block device %s from"
+                     " node %s" % (dev.logical_id[1], src_node))
+
+    if not rpc.call_finalize_export(dst_node.name, instance, snap_disks):
+      logger.Error("could not finalize export for instance %s on node %s" %
+                   (instance.name, dst_node.name))
+
+    nodelist = self.cfg.GetNodeList()
+    nodelist.remove(dst_node.name)
+
+    # on one-node clusters nodelist will be empty after the removal
+    # if we proceed the backup would be removed because OpQueryExports
+    # substitutes an empty list with the full cluster node list.
+    if nodelist:
+      op = opcodes.OpQueryExports(nodes=nodelist)
+      exportlist = self.processor.ChainOpCode(op, feedback_fn)
+      for node in exportlist:
+        if instance.name in exportlist[node]:
+          if not rpc.call_export_remove(node, instance.name):
+            logger.Error("could not remove older export for instance %s"
+                         " on node %s" % (instance.name, node))
diff --git a/lib/config.py b/lib/config.py
new file mode 100644 (file)
index 0000000..51af348
--- /dev/null
@@ -0,0 +1,540 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2006, 2007 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.
+
+
+"""Configuration management for Ganeti
+
+This module provides the interface to the ganeti cluster configuration.
+
+
+The configuration data is stored on every node but is updated on the
+master only. After each update, the master distributes the data to the
+other nodes.
+
+Currently the data storage format is pickle as yaml was initially not
+available, then we used it but it was a memory-eating slow beast, so
+we reverted to pickle using custom Unpicklers.
+
+"""
+
+import os
+import socket
+import tempfile
+import random
+
+from ganeti import errors
+from ganeti import logger
+from ganeti import utils
+from ganeti import constants
+from ganeti import rpc
+from ganeti import objects
+
+
+class ConfigWriter:
+  """The interface to the cluster configuration"""
+
+  def __init__(self, cfg_file=None, offline=False):
+    self._config_data = None
+    self._config_time = None
+    self._offline = offline
+    if cfg_file is None:
+      self._cfg_file = constants.CLUSTER_CONF_FILE
+    else:
+      self._cfg_file = cfg_file
+
+  # this method needs to be static, so that we can call it on the class
+  @staticmethod
+  def IsCluster():
+    """Check if the cluster is configured.
+
+    """
+    return os.path.exists(constants.CLUSTER_CONF_FILE)
+
+  def GenerateMAC(self):
+    """Generate a MAC for an instance.
+
+    This should check the current instances for duplicates.
+
+    """
+    self._OpenConfig()
+    self._ReleaseLock()
+    prefix = self._config_data.cluster.mac_prefix
+    all_macs = self._AllMACs()
+    retries = 64
+    while retries > 0:
+      byte1 = random.randrange(0, 256)
+      byte2 = random.randrange(0, 256)
+      byte3 = random.randrange(0, 256)
+      mac = "%s:%02x:%02x:%02x" % (prefix, byte1, byte2, byte3)
+      if mac not in all_macs:
+        break
+      retries -= 1
+    else:
+      raise errors.ConfigurationError, ("Can't generate unique MAC")
+    return mac
+
+  def _AllMACs(self):
+    """Return all MACs present in the config.
+
+    """
+    self._OpenConfig()
+    self._ReleaseLock()
+
+    result = []
+    for instance in self._config_data.instances.values():
+      for nic in instance.nics:
+        result.append(nic.mac)
+
+    return result
+
+  def VerifyConfig(self):
+    """Stub verify function.
+    """
+    self._OpenConfig()
+    self._ReleaseLock()
+
+    result = []
+    seen_macs = []
+    data = self._config_data
+    for instance_name in data.instances:
+      instance = data.instances[instance_name]
+      if instance.primary_node not in data.nodes:
+        result.append("Instance '%s' has invalid primary node '%s'" %
+                      (instance_name, instance.primary_node))
+      for snode in instance.secondary_nodes:
+        if snode not in data.nodes:
+          result.append("Instance '%s' has invalid secondary node '%s'" %
+                        (instance_name, snode))
+      for idx, nic in enumerate(instance.nics):
+        if nic.mac in seen_macs:
+          result.append("Instance '%s' has NIC %d mac %s duplicate" %
+                        (instance_name, idx, nic.mac))
+        else:
+          seen_macs.append(nic.mac)
+    return result
+
+
+  def SetDiskID(self, disk, node_name):
+    """Convert the unique ID to the ID needed on the target nodes.
+
+    This is used only for drbd, which needs ip/port configuration.
+
+    The routine descends down and updates its children also, because
+    this helps when the only the top device is passed to the remote
+    node.
+
+    """
+    if disk.children:
+      for child in disk.children:
+        self.SetDiskID(child, node_name)
+
+    if disk.logical_id is None and disk.physical_id is not None:
+      return
+    if disk.dev_type == "drbd":
+      pnode, snode, port = disk.logical_id
+      if node_name not in (pnode, snode):
+        raise errors.ConfigurationError, ("DRBD device not knowing node %s" %
+                                          node_name)
+      pnode_info = self.GetNodeInfo(pnode)
+      snode_info = self.GetNodeInfo(snode)
+      if pnode_info is None or snode_info is None:
+        raise errors.ConfigurationError("Can't find primary or secondary node"
+                                        " for %s" % str(disk))
+      if pnode == node_name:
+        disk.physical_id = (pnode_info.secondary_ip, port,
+                            snode_info.secondary_ip, port)
+      else: # it must be secondary, we tested above
+        disk.physical_id = (snode_info.secondary_ip, port,
+                            pnode_info.secondary_ip, port)
+    else:
+      disk.physical_id = disk.logical_id
+    return
+
+  def AllocatePort(self):
+    """Allocate a port.
+
+    The port will be recorded in the cluster config.
+
+    """
+    self._OpenConfig()
+
+    self._config_data.cluster.highest_used_port += 1
+    if self._config_data.cluster.highest_used_port >= constants.LAST_DRBD_PORT:
+      raise errors.ConfigurationError, ("The highest used port is greater"
+                                        " than %s. Aborting." %
+                                        constants.LAST_DRBD_PORT)
+    port = self._config_data.cluster.highest_used_port
+
+    self._WriteConfig()
+    return port
+
+  def GetHostKey(self):
+    """Return the rsa hostkey from the config.
+
+    Args: None
+
+    Returns: rsa hostkey
+    """
+    self._OpenConfig()
+    self._ReleaseLock()
+    return self._config_data.cluster.rsahostkeypub
+
+  def AddInstance(self, instance):
+    """Add an instance to the config.
+
+    This should be used after creating a new instance.
+
+    Args:
+      instance: the instance object
+    """
+    if not isinstance(instance, objects.Instance):
+      raise errors.ProgrammerError("Invalid type passed to AddInstance")
+
+    self._OpenConfig()
+    self._config_data.instances[instance.name] = instance
+    self._WriteConfig()
+
+  def MarkInstanceUp(self, instance_name):
+    """Mark the instance status to up in the config.
+
+    """
+    self._OpenConfig()
+
+    if instance_name not in self._config_data.instances:
+      raise errors.ConfigurationError, ("Unknown instance '%s'" %
+                                        instance_name)
+    instance = self._config_data.instances[instance_name]
+    instance.status = "up"
+    self._WriteConfig()
+
+  def RemoveInstance(self, instance_name):
+    """Remove the instance from the configuration.
+
+    """
+    self._OpenConfig()
+
+    if instance_name not in self._config_data.instances:
+      raise errors.ConfigurationError, ("Unknown instance '%s'" %
+                                        instance_name)
+    del self._config_data.instances[instance_name]
+    self._WriteConfig()
+
+  def MarkInstanceDown(self, instance_name):
+    """Mark the status of an instance to down in the configuration.
+
+    """
+
+    self._OpenConfig()
+
+    if instance_name not in self._config_data.instances:
+      raise errors.ConfigurationError, ("Unknown instance '%s'" %
+                                        instance_name)
+    instance = self._config_data.instances[instance_name]
+    instance.status = "down"
+    self._WriteConfig()
+
+  def GetInstanceList(self):
+    """Get the list of instances.
+
+    Returns:
+      array of instances, ex. ['instance2.example.com','instance1.example.com']
+      these contains all the instances, also the ones in Admin_down state
+
+    """
+    self._OpenConfig()
+    self._ReleaseLock()
+
+    return self._config_data.instances.keys()
+
+  def ExpandInstanceName(self, short_name):
+    """Attempt to expand an incomplete instance name.
+
+    """
+    self._OpenConfig()
+    self._ReleaseLock()
+
+    return utils.MatchNameComponent(short_name,
+                                    self._config_data.instances.keys())
+
+  def GetInstanceInfo(self, instance_name):
+    """Returns informations about an instance.
+
+    It takes the information from the configuration file. Other informations of
+    an instance are taken from the live systems.
+
+    Args:
+      instance: name of the instance, ex instance1.example.com
+
+    Returns:
+      the instance object
+
+    """
+    self._OpenConfig()
+    self._ReleaseLock()
+
+    if instance_name not in self._config_data.instances:
+      return None
+
+    return self._config_data.instances[instance_name]
+
+  def AddNode(self, node):
+    """Add a node to the configuration.
+
+    Args:
+      node: an object.Node instance
+
+    """
+    self._OpenConfig()
+    self._config_data.nodes[node.name] = node
+    self._WriteConfig()
+
+  def RemoveNode(self, node_name):
+    """Remove a node from the configuration.
+
+    """
+    self._OpenConfig()
+    if node_name not in self._config_data.nodes:
+      raise errors.ConfigurationError, ("Unknown node '%s'" % node_name)
+
+    del self._config_data.nodes[node_name]
+    self._WriteConfig()
+
+  def ExpandNodeName(self, short_name):
+    """Attempt to expand an incomplete instance name.
+
+    """
+    self._OpenConfig()
+    self._ReleaseLock()
+
+    return utils.MatchNameComponent(short_name,
+                                    self._config_data.nodes.keys())
+
+  def GetNodeInfo(self, node_name):
+    """Get the configuration of a node, as stored in the config.
+
+    Args: node: nodename (tuple) of the node
+
+    Returns: the node object
+
+    """
+    self._OpenConfig()
+    self._ReleaseLock()
+
+    if node_name not in self._config_data.nodes:
+      return None
+
+    return self._config_data.nodes[node_name]
+
+  def GetNodeList(self):
+    """Return the list of nodes which are in the configuration.
+
+    """
+    self._OpenConfig()
+    self._ReleaseLock()
+    return self._config_data.nodes.keys()
+
+  def DumpConfig(self):
+    """Return the entire configuration of the cluster.
+    """
+    self._OpenConfig()
+    self._ReleaseLock()
+    return self._config_data
+
+  def _BumpSerialNo(self):
+    """Bump up the serial number of the config.
+
+    """
+    self._config_data.cluster.serial_no += 1
+
+  def _OpenConfig(self):
+    """Read the config data from disk.
+
+    In case we already have configuration data and the config file has
+    the same mtime as when we read it, we skip the parsing of the
+    file, since de-serialisation could be slow.
+
+    """
+    try:
+      st = os.stat(self._cfg_file)
+    except OSError, err:
+      raise errors.ConfigurationError, "Can't stat config file: %s" % err
+    if (self._config_data is not None and
+        self._config_time is not None and
+        self._config_time == st.st_mtime):
+      # data is current, so skip loading of config file
+      return
+    f = open(self._cfg_file, 'r')
+    try:
+      try:
+        data = objects.ConfigObject.Load(f)
+      except Exception, err:
+        raise errors.ConfigurationError, err
+    finally:
+      f.close()
+    if (not hasattr(data, 'cluster') or
+        not hasattr(data.cluster, 'config_version')):
+      raise errors.ConfigurationError, ("Incomplete configuration"
+                                        " (missing cluster.config_version)")
+    if data.cluster.config_version != constants.CONFIG_VERSION:
+      raise errors.ConfigurationError, ("Cluster configuration version"
+                                        " mismatch, got %s instead of %s" %
+                                        (data.cluster.config_version,
+                                         constants.CONFIG_VERSION))
+    self._config_data = data
+    self._config_time = st.st_mtime
+
+  def _ReleaseLock(self):
+    """xxxx
+    """
+
+  def _DistributeConfig(self):
+    """Distribute the configuration to the other nodes.
+
+    Currently, this only copies the configuration file. In the future,
+    it could be used to encapsulate the 2/3-phase update mechanism.
+
+    """
+    if self._offline:
+      return True
+    bad = False
+    nodelist = self.GetNodeList()
+    myhostname = socket.gethostname()
+
+    tgt_list = []
+    for node in nodelist:
+      nodeinfo = self.GetNodeInfo(node)
+      if nodeinfo.name == myhostname:
+        continue
+      tgt_list.append(node)
+
+    result = rpc.call_upload_file(tgt_list, self._cfg_file)
+    for node in tgt_list:
+      if not result[node]:
+        logger.Error("copy of file %s to node %s failed" %
+                     (self._cfg_file, node))
+        bad = True
+    return not bad
+
+  def _WriteConfig(self, destination=None):
+    """Write the configuration data to persistent storage.
+
+    """
+    if destination is None:
+      destination = self._cfg_file
+    self._BumpSerialNo()
+    dir_name, file_name = os.path.split(destination)
+    fd, name = tempfile.mkstemp('.newconfig', file_name, dir_name)
+    f = os.fdopen(fd, 'w')
+    try:
+      self._config_data.Dump(f)
+      os.fsync(f.fileno())
+    finally:
+      f.close()
+    # we don't need to do os.close(fd) as f.close() did it
+    os.rename(name, destination)
+    self._DistributeConfig()
+
+  def InitConfig(self, node, primary_ip, secondary_ip,
+                 clustername, hostkeypub, mac_prefix, vg_name, def_bridge):
+    """Create the initial cluster configuration.
+
+    It will contain the current node, which will also be the master
+    node, and no instances or operating systmes.
+
+    Args:
+      node: the nodename of the initial node
+      primary_ip: the IP address of the current host
+      secondary_ip: the secondary IP of the current host or None
+      clustername: the name of the cluster
+      hostkeypub: the public hostkey of this host
+    """
+
+    hu_port = constants.FIRST_DRBD_PORT - 1
+    globalconfig = objects.Cluster(config_version=constants.CONFIG_VERSION,
+                                   serial_no=1, master_node=node,
+                                   name=clustername,
+                                   rsahostkeypub=hostkeypub,
+                                   highest_used_port=hu_port,
+                                   mac_prefix=mac_prefix,
+                                   volume_group_name=vg_name,
+                                   default_bridge=def_bridge)
+    if secondary_ip is None:
+      secondary_ip = primary_ip
+    nodeconfig = objects.Node(name=node, primary_ip=primary_ip,
+                              secondary_ip=secondary_ip)
+
+    self._config_data = objects.ConfigData(nodes={node: nodeconfig},
+                                           instances={},
+                                           cluster=globalconfig)
+    self._WriteConfig()
+
+  def GetClusterName(self):
+    """Return the cluster name.
+
+    """
+    self._OpenConfig()
+    self._ReleaseLock()
+    return self._config_data.cluster.name
+
+  def GetVGName(self):
+    """Return the volume group name.
+
+    """
+    self._OpenConfig()
+    self._ReleaseLock()
+    return self._config_data.cluster.volume_group_name
+
+  def GetDefBridge(self):
+    """Return the default bridge.
+
+    """
+    self._OpenConfig()
+    self._ReleaseLock()
+    return self._config_data.cluster.default_bridge
+
+  def GetMACPrefix(self):
+    """Return the mac prefix.
+
+    """
+    self._OpenConfig()
+    self._ReleaseLock()
+    return self._config_data.cluster.mac_prefix
+
+  def GetMaster(self):
+    """Get the name of the master.
+
+    """
+    self._OpenConfig()
+    self._ReleaseLock()
+    return self._config_data.cluster.master_node
+
+  def SetMaster(self, master_node):
+    """Change the master of the cluster.
+
+    As with all changes, the configuration data will be distributed to
+    all nodes.
+
+    This function is used for manual master failover.
+
+    """
+    self._OpenConfig()
+    self._config_data.cluster.master_node = master_node
+    self._WriteConfig()
+    self._ReleaseLock()
diff --git a/lib/constants.py b/lib/constants.py
new file mode 100644 (file)
index 0000000..5eff9a6
--- /dev/null
@@ -0,0 +1,113 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2006, 2007 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 holding different constants."""
+
+# various versions
+CONFIG_VERSION = 2
+PROTOCOL_VERSION = 2
+RELEASE_VERSION = "1.2a1"
+OS_API_VERSION = 4
+EXPORT_VERSION = 0
+
+
+# file paths
+DATA_DIR = "/var/lib/ganeti"
+CLUSTER_CONF_FILE = DATA_DIR + "/config.data"
+CLUSTER_NAME_FILE = DATA_DIR + "/cluster-name"
+SSL_CERT_FILE = DATA_DIR + "/server.pem"
+HYPERCONF_FILE = DATA_DIR + "/hypervisor"
+WATCHER_STATEFILE = DATA_DIR + "/restart_state"
+
+ETC_DIR = "/etc/ganeti"
+
+MASTER_CRON_FILE = ETC_DIR + "/master-cron"
+MASTER_CRON_LINK = "/etc/cron.d/ganeti-master-cron"
+NODE_INITD_SCRIPT = "/etc/init.d/ganeti"
+NODE_INITD_NAME = "ganeti"
+DEFAULT_NODED_PORT = 1811
+FIRST_DRBD_PORT = 11000
+LAST_DRBD_PORT = 14999
+MASTER_INITD_SCRIPT = "/etc/init.d/ganeti-master"
+MASTER_INITD_NAME = "ganeti-master"
+
+LOG_DIR = "/var/log/ganeti"
+LOG_OS_DIR = LOG_DIR + "/os"
+LOG_NODESERVER = LOG_DIR + "/node-daemon.log"
+
+OS_DIR = "/srv/ganeti/os"
+EXPORT_DIR = "/srv/ganeti/export"
+
+EXPORT_CONF_FILE = "config.ini"
+
+# hooks-related constants
+HOOKS_BASE_DIR = "/etc/ganeti/hooks"
+HOOKS_PHASE_PRE = "pre"
+HOOKS_PHASE_POST = "post"
+HOOKS_VERSION = 1
+
+# hooks subject type (what object type does the LU deal with)
+HTYPE_CLUSTER = "CLUSTER"
+HTYPE_NODE = "NODE"
+HTYPE_INSTANCE = "INSTANCE"
+
+HKR_SKIP = 0
+HKR_FAIL = 1
+HKR_SUCCESS = 2
+
+# disk template types
+DT_DISKLESS = "diskless"
+DT_PLAIN = "plain"
+DT_LOCAL_RAID1 = "local_raid1"
+DT_REMOTE_RAID1 = "remote_raid1"
+
+# instance creation modem
+INSTANCE_CREATE = "create"
+INSTANCE_IMPORT = "import"
+
+DISK_TEMPLATES = frozenset([DT_DISKLESS, DT_PLAIN,
+                            DT_LOCAL_RAID1, DT_REMOTE_RAID1])
+
+# file groups
+CLUSTER_CONF_FILES = ["/etc/hosts",
+                      "/etc/ssh/ssh_known_hosts",
+                      "/etc/ssh/ssh_host_dsa_key",
+                      "/etc/ssh/ssh_host_dsa_key.pub",
+                      "/etc/ssh/ssh_host_rsa_key",
+                      "/etc/ssh/ssh_host_rsa_key.pub",
+                      "/root/.ssh/authorized_keys",
+                      "/root/.ssh/id_dsa",
+                      "/root/.ssh/id_dsa.pub",
+                      CLUSTER_CONF_FILE,
+                      SSL_CERT_FILE,
+                      MASTER_CRON_FILE,
+                      ]
+
+MASTER_CONFIGFILES = [MASTER_CRON_LINK,
+                      "/etc/rc2.d/S21%s" % MASTER_INITD_NAME]
+
+NODE_CONFIGFILES = [NODE_INITD_SCRIPT,
+                    "/etc/rc2.d/S20%s" % NODE_INITD_NAME,
+                    "/etc/rc0.d/K80%s" % NODE_INITD_NAME]
+
+# import/export config options
+INISECT_EXP = "export"
+INISECT_INS = "instance"
diff --git a/lib/errors.py b/lib/errors.py
new file mode 100644 (file)
index 0000000..7bcd564
--- /dev/null
@@ -0,0 +1,170 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2006, 2007 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.
+
+
+"""Ganeti exception handling"""
+
+
+class GenericError(Exception):
+  """Base exception for Ganeti.
+
+  """
+  pass
+
+
+class LVMError(GenericError):
+  """LVM-related exception.
+
+  This exception codifies problems with LVM setup.
+
+  """
+  pass
+
+
+class LockError(GenericError):
+  """Lock error exception.
+
+  This signifies problems in the locking subsystem.
+
+  """
+  pass
+
+
+class HypervisorError(GenericError):
+  """Hypervisor-related exception.
+
+  This is raised in case we can't communicate with the hypervisor
+  properly.
+
+  """
+  pass
+
+
+class ProgrammerError(GenericError):
+  """Programming-related error.
+
+  This is raised in cases we determine that the calling conventions
+  have been violated, meaning we got some desynchronisation between
+  parts of our code. It signifies a real programming bug.
+
+  """
+  pass
+
+
+class BlockDeviceError(GenericError):
+  """Block-device related exception.
+
+  This is raised in case we can't setup the instance's block devices
+  properly.
+
+  """
+  pass
+
+
+class ConfigurationError(GenericError):
+  """Configuration related exception.
+
+  Things like having an instance with a primary node that doesn't
+  exist in the config or such raise this exception.
+
+  """
+  pass
+
+
+class RemoteError(GenericError):
+  """Programming-related error on remote call.
+
+  This is raised when an unhandled error occurs in a call to a
+  remote node.  It usually signifies a real programming bug.
+
+  """
+  pass
+
+
+class InvalidOS(GenericError):
+  """Missing OS on node.
+
+  This is raised when an OS exists on the master (or is otherwise
+  requested to the code) but not on the target node.
+
+  This exception has two arguments:
+    - the name of the os
+    - the reason why we consider this an invalid OS (text of error message)
+
+  """
+
+
+class ParameterError(GenericError):
+  """A passed parameter to a command is invalid.
+
+  This is raised when the parameter passed to a request function is
+  invalid. Correct code should have verified this before passing the
+  request structure.
+
+  The argument to this exception should be the parameter name.
+
+  """
+  pass
+
+
+class OpPrereqError(GenericError):
+  """Prerequisites for the OpCode are not fulfilled.
+
+  """
+
+class OpExecError(GenericError):
+  """Error during OpCode execution.
+
+  """
+
+class OpCodeUnknown(GenericError):
+  """Unknown opcode submitted.
+
+  This signifies a mismatch between the definitions on the client and
+  server side.
+
+  """
+
+class HooksFailure(GenericError):
+  """A generic hook failure.
+
+  This signifies usually a setup misconfiguration.
+
+  """
+
+class HooksAbort(HooksFailure):
+  """A required hook has failed.
+
+  This caused an abort of the operation in the initial phase. This
+  exception always has an attribute args which is a list of tuples of:
+    - node: the source node on which this hooks has failed
+    - script: the name of the script which aborted the run
+
+  """
+
+class UnitParseError(GenericError):
+  """Unable to parse size unit.
+
+  """
+
+
+class SshKeyError(GenericError):
+  """Invalid SSH key.
+  """
diff --git a/lib/hypervisor.py b/lib/hypervisor.py
new file mode 100644 (file)
index 0000000..39a628b
--- /dev/null
@@ -0,0 +1,496 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2006, 2007 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 that abstracts the virtualisation interface
+
+"""
+
+import time
+import os
+from cStringIO import StringIO
+
+from ganeti import utils
+from ganeti import logger
+from ganeti import ssconf
+from ganeti.errors import HypervisorError
+
+_HT_XEN30 = "xen-3.0"
+_HT_FAKE = "fake"
+
+VALID_HTYPES = (_HT_XEN30, _HT_FAKE)
+
+def GetHypervisor():
+  """Return a Hypervisor instance.
+
+  This function parses the cluster hypervisor configuration file and
+  instantiates a class based on the value of this file.
+
+  """
+  ht_kind = ssconf.SimpleStore().GetHypervisorType()
+  if ht_kind == _HT_XEN30:
+    cls = XenHypervisor
+  elif ht_kind == _HT_FAKE:
+    cls = FakeHypervisor
+  else:
+    raise HypervisorError, "Unknown hypervisor type '%s'" % ht_kind
+  return cls()
+
+
+class BaseHypervisor(object):
+  """Abstract virtualisation technology interface
+
+  The goal is that all aspects of the virtualisation technology must
+  be abstracted away from the rest of code.
+
+  """
+  def __init__(self):
+    pass
+
+  def StartInstance(self, instance, block_devices, extra_args):
+    """Start an instance."""
+    raise NotImplementedError
+
+  def StopInstance(self, instance, force=False):
+    """Stop an instance."""
+    raise NotImplementedError
+
+  def ListInstances(self):
+    """Get the list of running instances."""
+    raise NotImplementedError
+
+  def GetInstanceInfo(self, instance_name):
+    """Get instance properties.
+
+    Args:
+      instance_name: the instance name
+
+    Returns:
+      (name, id, memory, vcpus, state, times)
+
+    """
+    raise NotImplementedError
+
+  def GetAllInstancesInfo(self):
+    """Get properties of all instances.
+
+    Returns:
+      [(name, id, memory, vcpus, stat, times),...]
+    """
+    raise NotImplementedError
+
+  def GetNodeInfo(self):
+    """Return information about the node.
+
+    The return value is a dict, which has to have the following items:
+      (all values in MiB)
+      - memory_total: the total memory size on the node
+      - memory_free: the available memory on the node for instances
+      - memory_dom0: the memory used by the node itself, if available
+
+    """
+    raise NotImplementedError
+
+  @staticmethod
+  def GetShellCommandForConsole(instance_name):
+    """Return a command for connecting to the console of an instance.
+
+    """
+    raise NotImplementedError
+
+  def Verify(self):
+    """Verify the hypervisor.
+
+    """
+    raise NotImplementedError
+
+
+class XenHypervisor(BaseHypervisor):
+  """Xen hypervisor interface"""
+
+  @staticmethod
+  def _WriteConfigFile(instance, block_devices, extra_args):
+    """Create a Xen 3.0 config file.
+
+    """
+
+    config = StringIO()
+    config.write("# this is autogenerated by Ganeti, please do not edit\n#\n")
+    config.write("kernel = '/boot/vmlinuz-2.6-xenU'\n")
+    config.write("memory = %d\n" % instance.memory)
+    config.write("vcpus = %d\n" % instance.vcpus)
+    config.write("name = '%s'\n" % instance.name)
+
+    vif_data = []
+    for nic in instance.nics:
+      nic_str = "mac=%s, bridge=%s" % (nic.mac, nic.bridge)
+      ip = getattr(nic, "ip", None)
+      if ip is not None:
+        nic_str += ", ip=%s" % ip
+      vif_data.append("'%s'" % nic_str)
+
+    config.write("vif = [%s]\n" % ",".join(vif_data))
+
+    disk_data = ["'phy:%s,%s,w'" % (rldev.dev_path, cfdev.iv_name)
+                 for cfdev, rldev in block_devices]
+    config.write("disk = [%s]\n" % ",".join(disk_data))
+
+    config.write("root = '/dev/sda ro'\n")
+    config.write("on_poweroff = 'destroy'\n")
+    config.write("on_reboot = 'restart'\n")
+    config.write("on_crash = 'restart'\n")
+    if extra_args:
+      config.write("extra = '%s'\n" % extra_args)
+    # just in case it exists
+    utils.RemoveFile("/etc/xen/auto/%s" % instance.name)
+    f = open("/etc/xen/%s" % instance.name, "w")
+    f.write(config.getvalue())
+    f.close()
+    return True
+
+  @staticmethod
+  def _RemoveConfigFile(instance):
+    """Remove the xen configuration file.
+
+    """
+    utils.RemoveFile("/etc/xen/%s" % instance.name)
+
+  @staticmethod
+  def _GetXMList(include_node):
+    """Return the list of running instances.
+
+    If the `include_node` argument is True, then we return information
+    for dom0 also, otherwise we filter that from the return value.
+
+    The return value is a list of (name, id, memory, vcpus, state, time spent)
+
+    """
+    for dummy in range(5):
+      result = utils.RunCmd(["xm", "list"])
+      if not result.failed:
+        break
+      logger.Error("xm list failed (%s): %s" % (result.fail_reason,
+                                                result.output))
+      time.sleep(1)
+
+    if result.failed:
+      raise HypervisorError("xm list failed, retries exceeded (%s): %s" %
+                            (result.fail_reason, result.stderr))
+
+    # skip over the heading and the domain 0 line (optional)
+    if include_node:
+      to_skip = 1
+    else:
+      to_skip = 2
+    lines = result.stdout.splitlines()[to_skip:]
+    result = []
+    for line in lines:
+      # The format of lines is:
+      # Name      ID Mem(MiB) VCPUs State  Time(s)
+      # Domain-0   0  3418     4 r-----    266.2
+      data = line.split()
+      if len(data) != 6:
+        raise HypervisorError("Can't parse output of xm list, line: %s" % line)
+      try:
+        data[1] = int(data[1])
+        data[2] = int(data[2])
+        data[3] = int(data[3])
+        data[5] = float(data[5])
+      except ValueError, err:
+        raise HypervisorError("Can't parse output of xm list,"
+                              " line: %s, error: %s" % (line, err))
+      result.append(data)
+    return result
+
+  def ListInstances(self):
+    """Get the list of running instances.
+
+    """
+    xm_list = self._GetXMList(False)
+    names = [info[0] for info in xm_list]
+    return names
+
+  def GetInstanceInfo(self, instance_name):
+    """Get instance properties.
+
+    Args:
+      instance_name: the instance name
+
+    Returns:
+      (name, id, memory, vcpus, stat, times)
+    """
+    xm_list = self._GetXMList(instance_name=="Domain-0")
+    result = None
+    for data in xm_list:
+      if data[0] == instance_name:
+        result = data
+        break
+    return result
+
+  def GetAllInstancesInfo(self):
+    """Get properties of all instances.
+
+    Returns:
+      [(name, id, memory, vcpus, stat, times),...]
+    """
+    xm_list = self._GetXMList(False)
+    return xm_list
+
+  def StartInstance(self, instance, block_devices, extra_args):
+    """Start an instance."""
+    self._WriteConfigFile(instance, block_devices, extra_args)
+    result = utils.RunCmd(["xm", "create", instance.name])
+
+    if result.failed:
+      raise HypervisorError("Failed to start instance %s: %s" %
+                            (instance.name, result.fail_reason))
+
+  def StopInstance(self, instance, force=False):
+    """Stop an instance."""
+    self._RemoveConfigFile(instance)
+    if force:
+      command = ["xm", "destroy", instance.name]
+    else:
+      command = ["xm", "shutdown", instance.name]
+    result = utils.RunCmd(command)
+
+    if result.failed:
+      raise HypervisorError("Failed to stop instance %s: %s" %
+                            (instance.name, result.fail_reason))
+
+  def GetNodeInfo(self):
+    """Return information about the node.
+
+    The return value is a dict, which has to have the following items:
+      (all values in MiB)
+      - memory_total: the total memory size on the node
+      - memory_free: the available memory on the node for instances
+      - memory_dom0: the memory used by the node itself, if available
+
+    """
+    # note: in xen 3, memory has changed to total_memory
+    result = utils.RunCmd(["xm", "info"])
+    if result.failed:
+      logger.Error("Can't run 'xm info': %s" % result.fail_reason)
+      return None
+
+    xmoutput = result.stdout.splitlines()
+    result = {}
+    for line in xmoutput:
+      splitfields = line.split(":", 1)
+
+      if len(splitfields) > 1:
+        key = splitfields[0].strip()
+        val = splitfields[1].strip()
+        if key == 'memory' or key == 'total_memory':
+          result['memory_total'] = int(val)
+        elif key == 'free_memory':
+          result['memory_free'] = int(val)
+    dom0_info = self.GetInstanceInfo("Domain-0")
+    if dom0_info is not None:
+      result['memory_dom0'] = dom0_info[2]
+
+    return result
+
+  @staticmethod
+  def GetShellCommandForConsole(instance_name):
+    """Return a command for connecting to the console of an instance.
+
+    """
+    return "xm console %s" % instance_name
+
+
+  def Verify(self):
+    """Verify the hypervisor.
+
+    For Xen, this verifies that the xend process is running.
+
+    """
+    if not utils.CheckDaemonAlive('/var/run/xend.pid', 'xend'):
+      return "xend daemon is not running"
+
+
+class FakeHypervisor(BaseHypervisor):
+  """Fake hypervisor interface.
+
+  This can be used for testing the ganeti code without having to have
+  a real virtualisation software installed.
+
+  """
+
+  _ROOT_DIR = "/var/run/ganeti-fake-hypervisor"
+
+  def __init__(self):
+    BaseHypervisor.__init__(self)
+    if not os.path.exists(self._ROOT_DIR):
+      os.mkdir(self._ROOT_DIR)
+
+  def ListInstances(self):
+    """Get the list of running instances.
+
+    """
+    return os.listdir(self._ROOT_DIR)
+
+  def GetInstanceInfo(self, instance_name):
+    """Get instance properties.
+
+    Args:
+      instance_name: the instance name
+
+    Returns:
+      (name, id, memory, vcpus, stat, times)
+    """
+    file_name = "%s/%s" % (self._ROOT_DIR, instance_name)
+    if not os.path.exists(file_name):
+      return None
+    try:
+      fh = file(file_name, "r")
+      try:
+        inst_id = fh.readline().strip()
+        memory = fh.readline().strip()
+        vcpus = fh.readline().strip()
+        stat = "---b-"
+        times = "0"
+        return (instance_name, inst_id, memory, vcpus, stat, times)
+      finally:
+        fh.close()
+    except IOError, err:
+      raise HypervisorError("Failed to list instance %s: %s" %
+                            (instance_name, err))
+
+  def GetAllInstancesInfo(self):
+    """Get properties of all instances.
+
+    Returns:
+      [(name, id, memory, vcpus, stat, times),...]
+    """
+    data = []
+    for file_name in os.listdir(self._ROOT_DIR):
+      try:
+        fh = file(self._ROOT_DIR+"/"+file_name, "r")
+        inst_id = "-1"
+        memory = "0"
+        stat = "-----"
+        times = "-1"
+        try:
+          inst_id = fh.readline().strip()
+          memory = fh.readline().strip()
+          vcpus = fh.readline().strip()
+          stat = "---b-"
+          times = "0"
+        finally:
+          fh.close()
+        data.append((file_name, inst_id, memory, vcpus, stat, times))
+      except IOError, err:
+        raise HypervisorError("Failed to list instances: %s" % err)
+    return data
+
+  def StartInstance(self, instance, force, extra_args):
+    """Start an instance.
+
+    For the fake hypervisor, it just creates a file in the base dir,
+    creating an exception if it already exists. We don't actually
+    handle race conditions properly, since these are *FAKE* instances.
+
+    """
+    file_name = self._ROOT_DIR + "/%s" % instance.name
+    if os.path.exists(file_name):
+      raise HypervisorError("Failed to start instance %s: %s" %
+                            (instance.name, "already running"))
+    try:
+      fh = file(file_name, "w")
+      try:
+        fh.write("0\n%d\n%d\n" % (instance.memory, instance.vcpus))
+      finally:
+        fh.close()
+    except IOError, err:
+      raise HypervisorError("Failed to start instance %s: %s" %
+                            (instance.name, err))
+
+  def StopInstance(self, instance, force=False):
+    """Stop an instance.
+
+    For the fake hypervisor, this just removes the file in the base
+    dir, if it exist, otherwise we raise an exception.
+
+    """
+    file_name = self._ROOT_DIR + "/%s" % instance.name
+    if not os.path.exists(file_name):
+      raise HypervisorError("Failed to stop instance %s: %s" %
+                            (instance.name, "not running"))
+    utils.RemoveFile(file_name)
+
+  def GetNodeInfo(self):
+    """Return information about the node.
+
+    The return value is a dict, which has to have the following items:
+      (all values in MiB)
+      - memory_total: the total memory size on the node
+      - memory_free: the available memory on the node for instances
+      - memory_dom0: the memory used by the node itself, if available
+
+    """
+    # global ram usage from the xm info command
+    # memory                 : 3583
+    # free_memory            : 747
+    # note: in xen 3, memory has changed to total_memory
+    try:
+      fh = file("/proc/meminfo")
+      try:
+        data = fh.readlines()
+      finally:
+        fh.close()
+    except IOError, err:
+      raise HypervisorError("Failed to list node info: %s" % err)
+
+    result = {}
+    sum_free = 0
+    for line in data:
+      splitfields = line.split(":", 1)
+
+      if len(splitfields) > 1:
+        key = splitfields[0].strip()
+        val = splitfields[1].strip()
+        if key == 'MemTotal':
+          result['memory_total'] = int(val.split()[0])/1024
+        elif key in ('MemFree', 'Buffers', 'Cached'):
+          sum_free += int(val.split()[0])/1024
+        elif key == 'Active':
+          result['memory_dom0'] = int(val.split()[0])/1024
+
+    result['memory_free'] = sum_free
+    return result
+
+  @staticmethod
+  def GetShellCommandForConsole(instance_name):
+    """Return a command for connecting to the console of an instance.
+
+    """
+    return "echo Console not available for fake hypervisor"
+
+  def Verify(self):
+    """Verify the hypervisor.
+
+    For the fake hypervisor, it just checks the existence of the base
+    dir.
+
+    """
+    if not os.path.exists(self._ROOT_DIR):
+      return "The required directory '%s' does not exist." % self._ROOT_DIR
diff --git a/lib/logger.py b/lib/logger.py
new file mode 100644 (file)
index 0000000..875ba3e
--- /dev/null
@@ -0,0 +1,238 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2006, 2007 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.
+
+
+"""Logging for Ganeti
+
+This module abstracts the logging handling away from the rest of the
+Ganeti code. It offers some utility functions for easy logging.
+"""
+
+# pylint: disable-msg=W0603,C0103
+
+import sys
+import logging
+import os, os.path
+
+from ganeti import constants
+
+_program = '(unknown)'
+_errlog = None
+_inflog = None
+_dbglog = None
+_stdout = None
+_stderr = None
+_debug = False
+
+
+def _SetDestination(name, filename, stream=None):
+  """Configure the destination for a given logger
+
+  This function configures the logging destination for a given loger.
+  Parameters:
+    - name: the logger name
+    - filename: if not empty, log messages will be written (also) to this file
+    - stream: if not none, log messages will be output (also) to this stream
+
+  Returns:
+    - the logger identified by the `name` argument
+  """
+  ret = logging.getLogger(name)
+
+  if filename:
+    fmtr = logging.Formatter('%(asctime)s %(message)s')
+
+    hdlr = logging.FileHandler(filename)
+    hdlr.setFormatter(fmtr)
+    ret.addHandler(hdlr)
+
+  if stream:
+    if name in ('error', 'info', 'debug'):
+      fmtr = logging.Formatter('%(asctime)s %(message)s')
+    else:
+      fmtr = logging.Formatter('%(message)s')
+    hdlr = logging.StreamHandler(stream)
+    hdlr.setFormatter(fmtr)
+    ret.addHandler(hdlr)
+
+  ret.setLevel(logging.INFO)
+
+  return ret
+
+
+def _GenericSetup(program, errfile, inffile, dbgfile,
+                  twisted_workaround=False):
+  """Configure logging based on arguments
+
+  Arguments:
+    - name of program
+    - error log filename
+    - info log filename
+    - debug log filename
+    - twisted_workaround: if true, emit all messages to stderr
+  """
+  global _program
+  global _errlog
+  global _inflog
+  global _dbglog
+  global _stdout
+  global _stderr
+
+  _program = program
+  if twisted_workaround:
+    _errlog = _SetDestination('error', None, sys.stderr)
+    _inflog = _SetDestination('info', None, sys.stderr)
+    _dbglog = _SetDestination('debug', None, sys.stderr)
+  else:
+    _errlog = _SetDestination('error', errfile)
+    _inflog = _SetDestination('info', inffile)
+    _dbglog = _SetDestination('debug', dbgfile)
+
+  _stdout = _SetDestination('user', None, sys.stdout)
+  _stderr = _SetDestination('stderr', None, sys.stderr)
+
+
+def SetupLogging(twisted_workaround=False, debug=False, program='ganeti'):
+  """Setup logging for ganeti
+
+  On failure, a check is made whether process is run by root or not,
+  and an appropriate error message is printed on stderr, then process
+  exits.
+
+  This function is just a wraper over `_GenericSetup()` using specific
+  arguments.
+
+  Parameter:
+    twisted_workaround: passed to `_GenericSetup()`
+
+  """
+  try:
+    _GenericSetup(program,
+                  os.path.join(constants.LOG_DIR, "errors"),
+                  os.path.join(constants.LOG_DIR, "info"),
+                  os.path.join(constants.LOG_DIR, "debug"),
+                  twisted_workaround)
+  except IOError:
+    # The major reason to end up here is that we're being run as a
+    # non-root user.  We might also get here if xen has not been
+    # installed properly.  This is not the correct place to enforce
+    # being run by root; nevertheless, here makes sense because here
+    # is where we first notice it.
+    if os.getuid() != 0:
+      sys.stderr.write('This program must be run by the superuser.\n')
+    else:
+      sys.stderr.write('Unable to open log files.  Incomplete system?\n')
+
+    sys.exit(2)
+
+  global _debug
+  _debug = debug
+
+
+def _WriteEntry(log, txt):
+  """
+  Write a message to a given log.
+  Splits multi-line messages up into a series of log writes, to
+  keep consistent format on lines in file.
+
+  Parameters:
+    - log: the destination log
+    - txt: the message
+
+  """
+  if log is None:
+    sys.stderr.write("Logging system not initialized while processing"
+                     " message:\n")
+    sys.stderr.write("%s\n" % txt)
+    return
+
+  lines = txt.split('\n')
+
+  spaces = ' ' * len(_program) + '| '
+
+  lines = ([ _program + ': ' + lines[0] ] +
+           map(lambda a: spaces + a, lines[1:]))
+
+  for line in lines:
+    log.log(logging.INFO, line)
+
+
+def ToStdout(txt):
+  """Write a message to stdout only, bypassing the logging system
+
+  Parameters:
+    - txt: the message
+
+  """
+  sys.stdout.write(txt + '\n')
+  sys.stdout.flush()
+
+
+def ToStderr(txt):
+  """Write a message to stderr only, bypassing the logging system
+
+  Parameters:
+    - txt: the message
+
+  """
+  sys.stderr.write(txt + '\n')
+  sys.stderr.flush()
+
+
+def Error(txt):
+  """Write a message to our error log
+
+  Parameters:
+    - dbg: if true, the message will also be output to stderr
+    - txt: the log message
+
+  """
+  _WriteEntry(_errlog, txt)
+  sys.stderr.write(txt + '\n')
+
+
+def Info(txt):
+  """Write a message to our general messages log
+
+  If the global debug flag is true, the log message will also be
+  output to stderr.
+
+  Parameters:
+    - txt: the log message
+
+  """
+  _WriteEntry(_inflog, txt)
+  if _debug:
+    _WriteEntry(_stderr, txt)
+
+
+def Debug(txt):
+  """Write a message to the debug log
+
+  If the global debug flag is true, the log message will also be
+  output to stderr.
+
+  Parameters:
+    - txt: the log message
+
+  """
+  _WriteEntry(_dbglog, txt)
+  if _debug:
+    _WriteEntry(_stderr, txt)
diff --git a/lib/mcpu.py b/lib/mcpu.py
new file mode 100644 (file)
index 0000000..9537068
--- /dev/null
@@ -0,0 +1,238 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2006, 2007 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 logic behind the cluster operations
+
+This module implements the logic for doing operations in the cluster. There
+are two kinds of classes defined:
+  - logical units, which know how to deal with their specific opcode only
+  - the processor, which dispatches the opcodes to their logical units
+
+"""
+
+
+import os
+import os.path
+import time
+
+from ganeti import opcodes
+from ganeti import logger
+from ganeti import constants
+from ganeti import utils
+from ganeti import errors
+from ganeti import rpc
+from ganeti import cmdlib
+from ganeti import config
+from ganeti import ssconf
+
+class Processor(object):
+  """Object which runs OpCodes"""
+  DISPATCH_TABLE = {
+    # Cluster
+    opcodes.OpInitCluster: cmdlib.LUInitCluster,
+    opcodes.OpDestroyCluster: cmdlib.LUDestroyCluster,
+    opcodes.OpQueryClusterInfo: cmdlib.LUQueryClusterInfo,
+    opcodes.OpClusterCopyFile: cmdlib.LUClusterCopyFile,
+    opcodes.OpRunClusterCommand: cmdlib.LURunClusterCommand,
+    opcodes.OpVerifyCluster: cmdlib.LUVerifyCluster,
+    opcodes.OpMasterFailover: cmdlib.LUMasterFailover,
+    opcodes.OpDumpClusterConfig: cmdlib.LUDumpClusterConfig,
+    # node lu
+    opcodes.OpAddNode: cmdlib.LUAddNode,
+    opcodes.OpQueryNodes: cmdlib.LUQueryNodes,
+    opcodes.OpQueryNodeData: cmdlib.LUQueryNodeData,
+    opcodes.OpRemoveNode: cmdlib.LURemoveNode,
+    # instance lu
+    opcodes.OpCreateInstance: cmdlib.LUCreateInstance,
+    opcodes.OpRemoveInstance: cmdlib.LURem