Subject: [PATCH 1/4] gnu: Add ganeti.

From: Marius Bakke <[email-masked]>
Date: Wed, 8 Jul 2020 12:11:14 +0200

* gnu/packages/virtualization.scm (system->qemu-target, ganeti): New variables.
* gnu/packages/patches/ganeti-copy-hmac.patch,
gnu/packages/patches/ganeti-disable-version-symlinks.patch,
gnu/packages/patches/ganeti-drbd-compat.patch,
gnu/packages/patches/ganeti-haskell-pythondir.patch,
gnu/packages/patches/ganeti-openvswitch-may-exist.patch,
gnu/packages/patches/ganeti-preserve-PYTHONPATH.patch,
gnu/packages/patches/ganeti-shepherd-master-failover.patch,
gnu/packages/patches/ganeti-shepherd-support.patch: New files.
* gnu/local.mk (dist_patch_DATA): Adjust accordingly.
---
 gnu/local.mk                                  |   8 +
 gnu/packages/patches/ganeti-copy-hmac.patch   |  83 ++++
 .../ganeti-disable-version-symlinks.patch     | 136 +++++++
 gnu/packages/patches/ganeti-drbd-compat.patch | 168 ++++++++
 .../patches/ganeti-haskell-pythondir.patch    |  66 +++
 .../ganeti-openvswitch-may-exist.patch        |  25 ++
 .../patches/ganeti-preserve-PYTHONPATH.patch  |  21 +
 .../ganeti-shepherd-master-failover.patch     |  26 ++
 .../patches/ganeti-shepherd-support.patch     |  87 ++++
 gnu/packages/virtualization.scm               | 378 ++++++++++++++++++
 10 files changed, 998 insertions(+)
 create mode 100644 gnu/packages/patches/ganeti-copy-hmac.patch
 create mode 100644 gnu/packages/patches/ganeti-disable-version-symlinks.patch
 create mode 100644 gnu/packages/patches/ganeti-drbd-compat.patch
 create mode 100644 gnu/packages/patches/ganeti-haskell-pythondir.patch
 create mode 100644 gnu/packages/patches/ganeti-openvswitch-may-exist.patch
 create mode 100644 gnu/packages/patches/ganeti-preserve-PYTHONPATH.patch
 create mode 100644 gnu/packages/patches/ganeti-shepherd-master-failover.patch
 create mode 100644 gnu/packages/patches/ganeti-shepherd-support.patch

diff --git a/gnu/local.mk b/gnu/local.mk
index a277e63fa4..143aae2bea 100644
--- a/gnu/local.mk
+++ b/gnu/local.mk
@@ -944,6 +944,14 @@ dist_patch_DATA =						\
   %D%/packages/patches/fontconfig-hurd-path-max.patch		\
   %D%/packages/patches/freeimage-unbundle.patch		\
   %D%/packages/patches/fuse-overlapping-headers.patch				\
+  %D%/packages/patches/ganeti-copy-hmac.patch			\
+  %D%/packages/patches/ganeti-drbd-compat.patch			\
+  %D%/packages/patches/ganeti-disable-version-symlinks.patch	\
+  %D%/packages/patches/ganeti-haskell-pythondir.patch		\
+  %D%/packages/patches/ganeti-openvswitch-may-exist.patch	\
+  %D%/packages/patches/ganeti-preserve-PYTHONPATH.patch		\
+  %D%/packages/patches/ganeti-shepherd-master-failover.patch	\
+  %D%/packages/patches/ganeti-shepherd-support.patch		\
   %D%/packages/patches/gash-utils-ls-test.patch			\
   %D%/packages/patches/gawk-shell.patch				\
   %D%/packages/patches/gcc-arm-bug-71399.patch			\
diff --git a/gnu/packages/patches/ganeti-copy-hmac.patch b/gnu/packages/patches/ganeti-copy-hmac.patch
new file mode 100644
index 0000000000..c1a758afe9
--- /dev/null
+++ b/gnu/packages/patches/ganeti-copy-hmac.patch
@@ -0,0 +1,83 @@
+This is a cherry-pick of three upstream commits that got lost in the
+transition when Ganeti was donated to the community.
+
+Submitted upstream: <https://github.com/ganeti/ganeti/pull/1494>.
+
+diff --git a/lib/bootstrap.py b/lib/bootstrap.py
+--- a/lib/bootstrap.py
++++ b/lib/bootstrap.py
+@@ -934,6 +934,8 @@ def SetupNodeDaemon(opts, cluster_name, node, ssh_port):
+     constants.NDS_CLUSTER_NAME: cluster_name,
+     constants.NDS_NODE_DAEMON_CERTIFICATE:
+       utils.ReadFile(pathutils.NODED_CERT_FILE),
++    constants.NDS_HMAC:
++      utils.ReadFile(pathutils.CONFD_HMAC_KEY),
+     constants.NDS_SSCONF: ssconf.SimpleStore().ReadAll(),
+     constants.NDS_START_NODE_DAEMON: True,
+     constants.NDS_NODE_NAME: node,
+diff --git a/lib/tools/common.py b/lib/tools/common.py
+--- a/lib/tools/common.py
++++ b/lib/tools/common.py
+@@ -184,6 +184,19 @@ def VerifyClusterName(data, error_fn, cluster_name_constant,
+   return name
+ 
+ 
++def VerifyHmac(data, error_fn):
++  """Verifies the presence of the hmac secret.
++
++  @type data: dict
++
++  """
++  hmac = data.get(constants.NDS_HMAC)
++  if not hmac:
++    raise error_fn("Hmac key must be provided")
++
++  return hmac
++
++
+ def LoadData(raw, data_check):
+   """Parses and verifies input data.
+ 
+diff --git a/lib/tools/node_daemon_setup.py b/lib/tools/node_daemon_setup.py
+--- a/lib/tools/node_daemon_setup.py
++++ b/lib/tools/node_daemon_setup.py
+@@ -51,6 +51,7 @@ from ganeti.tools import common
+ _DATA_CHECK = ht.TStrictDict(False, True, {
+   constants.NDS_CLUSTER_NAME: ht.TNonEmptyString,
+   constants.NDS_NODE_DAEMON_CERTIFICATE: ht.TNonEmptyString,
++  constants.NDS_HMAC: ht.TNonEmptyString,
+   constants.NDS_SSCONF: ht.TDictOf(ht.TNonEmptyString, ht.TString),
+   constants.NDS_START_NODE_DAEMON: ht.TBool,
+   constants.NDS_NODE_NAME: ht.TString,
+@@ -127,11 +128,18 @@ def Main():
+     cluster_name = common.VerifyClusterName(data, SetupError,
+                                             constants.NDS_CLUSTER_NAME)
+     cert_pem = common.VerifyCertificateStrong(data, SetupError)
++    hmac_key = common.VerifyHmac(data, SetupError)
+     ssdata = VerifySsconf(data, cluster_name)
+ 
+     logging.info("Writing ssconf files ...")
+     ssconf.WriteSsconfFiles(ssdata, dry_run=opts.dry_run)
+ 
++    logging.info("Writing hmac.key ...")
++    utils.WriteFile(pathutils.CONFD_HMAC_KEY, data=hmac_key,
++                    mode=pathutils.NODED_CERT_MODE,
++                    uid=getent.masterd_uid, gid=getent.masterd_gid,
++                    dry_run=opts.dry_run)
++
+     logging.info("Writing node daemon certificate ...")
+     utils.WriteFile(pathutils.NODED_CERT_FILE, data=cert_pem,
+                     mode=pathutils.NODED_CERT_MODE,
+diff --git a/src/Ganeti/Constants.hs b/src/Ganeti/Constants.hs
+--- a/src/Ganeti/Constants.hs
++++ b/src/Ganeti/Constants.hs
+@@ -4833,6 +4833,9 @@ ndsNodeDaemonCertificate = "node_daemon_certificate"
+ ndsSsconf :: String
+ ndsSsconf = "ssconf"
+ 
++ndsHmac :: String
++ndsHmac = "hmac_key"
++
+ ndsStartNodeDaemon :: String
+ ndsStartNodeDaemon = "start_node_daemon"
+ 
diff --git a/gnu/packages/patches/ganeti-disable-version-symlinks.patch b/gnu/packages/patches/ganeti-disable-version-symlinks.patch
new file mode 100644
index 0000000000..a5f347cfc6
--- /dev/null
+++ b/gnu/packages/patches/ganeti-disable-version-symlinks.patch
@@ -0,0 +1,136 @@
+This patch adds a new "--disable-version-links" configuration option
+that allows installing to the standard GNU installation directories
+instead of having to add symlinks in /etc/ganeti/{lib,share} that
+points to the right $ganeti/{lib,share}/$version.  Mainly to reduce
+service complexity, and because Guix users can install as many versions
+of Ganeti they can muster without resorting to such hacks.
+
+diff --git a/Makefile.am b/Makefile.am
+--- a/Makefile.am
++++ b/Makefile.am
+@@ -66,11 +66,16 @@ SHELL_ENV_INIT = autotools/shell-env-init
+ # so, if some currently architecture-independent executable is replaced by an
+ # architecture-dependent one (and hence has to go under $(versiondir)), add a link
+ # under $(versionedsharedir) but do not change the external links.
++#
++# As of Ganeti 3.0, it is possible to disable this behavior by passing
++# --disable-version-links, in which case the standard GNU installation
++# directories are used.
+ if USE_VERSION_FULL
+ DIRVERSION=$(VERSION_FULL)
+ else
+ DIRVERSION=$(VERSION_MAJOR).$(VERSION_MINOR)
+ endif
++if USE_VERSION_LINKS
+ versiondir = $(libdir)/ganeti/$(DIRVERSION)
+ defaultversiondir = $(libdir)/ganeti/default
+ versionedsharedir = $(prefix)/share/ganeti/$(DIRVERSION)
+@@ -90,6 +95,18 @@ gntpythondir = $(versionedsharedir)
+ pkgpython_bindir = $(versionedsharedir)
+ gnt_python_sbindir = $(versionedsharedir)
+ tools_pythondir = $(versionedsharedir)
++else
++myexeclibdir = $(pkglibdir)
++pkgpython_rpc_stubdir = $(pkgpythondir)/rpc/stub
++gntpythondir = $(sbindir)
++pkgpython_bindir = $(pkglibdir)
++gnt_python_sbindir = $(sbindir)
++tools_pythondir = $(pkglibdir)
++versionedsharedir = $(pkglibdir)
++# This is a hack but works because the only user does $(versiondir)$(datadir).
++versiondir =
++endif !USE_VERSION_LINKS
++
+ 
+ clientdir = $(pkgpythondir)/client
+ cmdlibdir = $(pkgpythondir)/cmdlib
+@@ -2356,6 +2373,7 @@ src/AutoConf.hs: Makefile src/AutoConf.hs.in $(PRINT_PY_CONSTANTS) \
+ 	    -DVERSION_SUFFIX="$(VERSION_SUFFIX)" \
+ 	    -DVERSION_FULL="$(VERSION_FULL)" \
+ 	    -DDIRVERSION="$(DIRVERSION)" \
++	    -DUSE_VERSION_LINKS="$(USE_VERSION_LINKS)" \
+ 	    -DLOCALSTATEDIR="$(localstatedir)" \
+ 	    -DSYSCONFDIR="$(sysconfdir)" \
+ 	    -DSSH_CONFIG_DIR="$(SSH_CONFIG_DIR)" \
+@@ -2857,6 +2875,7 @@ install-exec-local:
+ 	@mkdir_p@ "$(DESTDIR)${localstatedir}/lib/ganeti" \
+ 	  "$(DESTDIR)${localstatedir}/log/ganeti" \
+ 	  "$(DESTDIR)${localstatedir}/run/ganeti"
++if USE_VERSION_LINKS
+ 	for dir in $(SYMLINK_TARGET_DIRS); do \
+ 	  @mkdir_p@  $(DESTDIR)$$dir; \
+ 	done
+@@ -2892,7 +2911,8 @@ install-exec-local:
+ if INSTALL_SYMLINKS
+ 	$(LN_S) -f $(versionedsharedir) $(DESTDIR)$(sysconfdir)/ganeti/share
+ 	$(LN_S) -f $(versiondir) $(DESTDIR)$(sysconfdir)/ganeti/lib
+-endif
++endif INSTALL_SYMLINKS
++endif USE_VERSION_LINKS
+ 
+ .PHONY: apidoc
+ if WANT_HSAPIDOC
+diff --git a/configure.ac b/configure.ac
+--- a/configure.ac
++++ b/configure.ac
+@@ -29,6 +29,23 @@ AC_SUBST([BINDIR], $bindir)
+ AC_SUBST([SBINDIR], $sbindir)
+ AC_SUBST([MANDIR], $mandir)
+ 
++# --enable-version-links
++AC_ARG_ENABLE([version-links],
++  [AS_HELP_STRING([--enable-version-links],
++                  m4_normalize([install ganeti to version-specific
++                  subdirectories to allow installing multiple versions
++                  in parallel (default: enabled)]))],
++  [[if test "$enableval" != no; then
++      USE_VERSION_LINKS=True
++    else
++      USE_VERSION_LINKS=False
++    fi
++  ]],
++  [USE_VERSION_LINKS=True
++  ])
++AC_SUBST(USE_VERSION_LINKS, $USE_VERSION_LINKS)
++AM_CONDITIONAL([USE_VERSION_LINKS], [test "$USE_VERSION_LINKS" = True])
++
+ # --enable-versionfull
+ AC_ARG_ENABLE([versionfull],
+   [AS_HELP_STRING([--enable-versionfull],
+diff --git a/lib/bootstrap.py b/lib/bootstrap.py
+--- a/lib/bootstrap.py
++++ b/lib/bootstrap.py
+@@ -944,7 +944,7 @@ def SetupNodeDaemon(opts, cluster_name, node, ssh_port):
+                          debug=opts.debug, verbose=opts.verbose,
+                          use_cluster_key=True, ask_key=opts.ssh_key_check,
+                          strict_host_check=opts.ssh_key_check,
+-                         ensure_version=True)
++                         ensure_version=constants.USE_VERSION_LINKS)
+ 
+   _WaitForSshDaemon(node, ssh_port)
+   _WaitForNodeDaemon(node)
+diff --git a/src/AutoConf.hs.in b/src/AutoConf.hs.in
+--- a/src/AutoConf.hs.in
++++ b/src/AutoConf.hs.in
+@@ -64,6 +64,9 @@ versionFull = "VERSION_FULL"
+ dirVersion :: String
+ dirVersion = "DIRVERSION"
+ 
++useVersionLinks :: Bool
++useVersionLinks = USE_VERSION_LINKS
++
+ localstatedir :: String
+ localstatedir = "LOCALSTATEDIR"
+ 
+diff --git a/src/Ganeti/Constants.hs b/src/Ganeti/Constants.hs
+--- a/src/Ganeti/Constants.hs
++++ b/src/Ganeti/Constants.hs
+@@ -164,5 +164,8 @@ versionRevision = AutoConf.versionRevision
+ dirVersion :: String
+ dirVersion = AutoConf.dirVersion
+ 
++useVersionLinks :: Bool
++useVersionLinks = AutoConf.useVersionLinks
++
+ osApiV10 :: Int
+ osApiV10 = 10
diff --git a/gnu/packages/patches/ganeti-drbd-compat.patch b/gnu/packages/patches/ganeti-drbd-compat.patch
new file mode 100644
index 0000000000..e06e04c5ef
--- /dev/null
+++ b/gnu/packages/patches/ganeti-drbd-compat.patch
@@ -0,0 +1,168 @@
+This patch adds support for newer versions of DRBD.
+
+Submitted upstream: <https://github.com/ganeti/ganeti/pull/1496>.
+
+diff --git a/lib/storage/drbd.py b/lib/storage/drbd.py
+--- a/lib/storage/drbd.py
++++ b/lib/storage/drbd.py
+@@ -315,6 +315,13 @@ class DRBD8Dev(base.BlockDev):
+     """
+     return self._show_info_cls.GetDevInfo(self._GetShowData(minor))
+ 
++  @staticmethod
++  def _NeedsLocalSyncerParams():
++    # For DRBD >= 8.4, syncer init must be done after local, not in net.
++    info = DRBD8.GetProcInfo()
++    version = info.GetVersion()
++    return version["k_minor"] >= 4
++
+   def _MatchesLocal(self, info):
+     """Test if our local config matches with an existing device.
+ 
+@@ -397,6 +404,22 @@ class DRBD8Dev(base.BlockDev):
+         base.ThrowError("drbd%d: can't attach local disk: %s",
+                         minor, result.output)
+ 
++    def _WaitForMinorSyncParams():
++      """Call _SetMinorSyncParams and raise RetryAgain on errors.
++      """
++      if self._SetMinorSyncParams(minor, self.params):
++        raise utils.RetryAgain()
++      else:
++        return []
++
++    if self._NeedsLocalSyncerParams():
++      # Retry because disk config for DRBD resource may be still uninitialized.
++      try:
++        utils.Retry(_WaitForMinorSyncParams, 1.0, 5.0)
++      except utils.RetryTimeout:
++        base.ThrowError("drbd%d: can't set the synchronization parameters: %s" %
++                        (minor, utils.CommaJoin(sync_errors)))
++
+   def _AssembleNet(self, minor, net_info, dual_pri=False, hmac=None,
+                    secret=None):
+     """Configure the network part of the device.
+@@ -432,21 +455,24 @@ class DRBD8Dev(base.BlockDev):
+     # sync speed only after setting up both sides can race with DRBD
+     # connecting, hence we set it here before telling DRBD anything
+     # about its peer.
+-    sync_errors = self._SetMinorSyncParams(minor, self.params)
+-    if sync_errors:
+-      base.ThrowError("drbd%d: can't set the synchronization parameters: %s" %
+-                      (minor, utils.CommaJoin(sync_errors)))
++
++    if not self._NeedsLocalSyncerParams():
++      sync_errors = self._SetMinorSyncParams(minor, self.params)
++      if sync_errors:
++        base.ThrowError("drbd%d: can't set the synchronization parameters: %s" %
++                        (minor, utils.CommaJoin(sync_errors)))
+ 
+     family = self._GetNetFamily(minor, lhost, rhost)
+ 
+-    cmd = self._cmd_gen.GenNetInitCmd(minor, family, lhost, lport,
++    cmds = self._cmd_gen.GenNetInitCmds(minor, family, lhost, lport,
+                                       rhost, rport, protocol,
+                                       dual_pri, hmac, secret, self.params)
+ 
+-    result = utils.RunCmd(cmd)
+-    if result.failed:
+-      base.ThrowError("drbd%d: can't setup network: %s - %s",
+-                      minor, result.fail_reason, result.output)
++    for cmd in cmds:
++      result = utils.RunCmd(cmd)
++      if result.failed:
++        base.ThrowError("drbd%d: can't setup network: %s - %s",
++                         minor, result.fail_reason, result.output)
+ 
+     def _CheckNetworkConfig():
+       info = self._GetShowInfo(minor)
+@@ -463,19 +489,20 @@ class DRBD8Dev(base.BlockDev):
+       base.ThrowError("drbd%d: timeout while configuring network", minor)
+ 
+     # Once the assembly is over, try to set the synchronization parameters
+-    try:
+-      # The minor may not have been set yet, requiring us to set it at least
+-      # temporarily
+-      old_minor = self.minor
+-      self._SetFromMinor(minor)
+-      sync_errors = self.SetSyncParams(self.params)
+-      if sync_errors:
+-        base.ThrowError("drbd%d: can't set the synchronization parameters: %s" %
+-                        (self.minor, utils.CommaJoin(sync_errors)))
+-    finally:
+-      # Undo the change, regardless of whether it will have to be done again
+-      # soon
+-      self._SetFromMinor(old_minor)
++    if not self._NeedsLocalSyncerParams():
++      try:
++        # The minor may not have been set yet, requiring us to set it at least
++        # temporarily
++        old_minor = self.minor
++        self._SetFromMinor(minor)
++        sync_errors = self.SetSyncParams(self.params)
++        if sync_errors:
++          base.ThrowError("drbd%d: can't set the synchronization parameters: %s" %
++                          (self.minor, utils.CommaJoin(sync_errors)))
++      finally:
++        # Undo the change, regardless of whether it will have to be done again
++        # soon
++        self._SetFromMinor(old_minor)
+ 
+   @staticmethod
+   def _GetNetFamily(minor, lhost, rhost):
+diff --git a/lib/storage/drbd_cmdgen.py b/lib/storage/drbd_cmdgen.py
+--- a/lib/storage/drbd_cmdgen.py
++++ b/lib/storage/drbd_cmdgen.py
+@@ -56,7 +56,7 @@ class BaseDRBDCmdGenerator(object):
+   def GenLocalInitCmds(self, minor, data_dev, meta_dev, size_mb, params):
+     raise NotImplementedError
+ 
+-  def GenNetInitCmd(self, minor, family, lhost, lport, rhost, rport, protocol,
++  def GenNetInitCmds(self, minor, family, lhost, lport, rhost, rport, protocol,
+                     dual_pri, hmac, secret, params):
+     raise NotImplementedError
+ 
+@@ -138,7 +138,7 @@ class DRBD83CmdGenerator(BaseDRBDCmdGenerator):
+ 
+     return [args]
+ 
+-  def GenNetInitCmd(self, minor, family, lhost, lport, rhost, rport, protocol,
++  def GenNetInitCmds(self, minor, family, lhost, lport, rhost, rport, protocol,
+                     dual_pri, hmac, secret, params):
+     args = ["drbdsetup", self._DevPath(minor), "net",
+             "%s:%s:%s" % (family, lhost, lport),
+@@ -155,7 +155,7 @@ class DRBD83CmdGenerator(BaseDRBDCmdGenerator):
+     if params[constants.LDP_NET_CUSTOM]:
+       args.extend(shlex.split(params[constants.LDP_NET_CUSTOM]))
+ 
+-    return args
++    return [args]
+ 
+   def GenSyncParamsCmd(self, minor, params):
+     args = ["drbdsetup", self._DevPath(minor), "syncer"]
+@@ -345,8 +345,14 @@ class DRBD84CmdGenerator(BaseDRBDCmdGenerator):
+ 
+     return cmds
+ 
+-  def GenNetInitCmd(self, minor, family, lhost, lport, rhost, rport, protocol,
++  def GenNetInitCmds(self, minor, family, lhost, lport, rhost, rport, protocol,
+                     dual_pri, hmac, secret, params):
++    cmds = []
++
++    cmds.append(["drbdsetup", "new-resource", self._GetResource(minor)])
++    cmds.append(["drbdsetup", "new-minor", self._GetResource(minor),
++                 str(minor), "0"])
++
+     args = ["drbdsetup", "connect", self._GetResource(minor),
+             "%s:%s:%s" % (family, lhost, lport),
+             "%s:%s:%s" % (family, rhost, rport),
+@@ -362,7 +368,8 @@ class DRBD84CmdGenerator(BaseDRBDCmdGenerator):
+     if params[constants.LDP_NET_CUSTOM]:
+       args.extend(shlex.split(params[constants.LDP_NET_CUSTOM]))
+ 
+-    return args
++    cmds.append(args)
++    return cmds
+ 
+   def GenSyncParamsCmd(self, minor, params):
+     args = ["drbdsetup", "disk-options", minor]
diff --git a/gnu/packages/patches/ganeti-haskell-pythondir.patch b/gnu/packages/patches/ganeti-haskell-pythondir.patch
new file mode 100644
index 0000000000..fa77771839
--- /dev/null
+++ b/gnu/packages/patches/ganeti-haskell-pythondir.patch
@@ -0,0 +1,66 @@
+This patch allows the Haskell daemons to locate Python libraries
+installed to a non-standard pythondir.  It is necessary because Guix
+does not use versionedsharedir (see related patch that disables it).
+
+diff --git a/Makefile.am b/Makefile.am
+--- a/Makefile.am
++++ b/Makefile.am
+@@ -83,6 +83,7 @@ myexeclibdir = $(pkglibdir)
+ bindir = $(versiondir)/$(BINDIR)
+ sbindir = $(versiondir)$(SBINDIR)
+ mandir = $(versionedsharedir)/root$(MANDIR)
++pythondir = $(versionedsharedir)
+ pkgpythondir = $(versionedsharedir)/ganeti
+ pkgpython_rpc_stubdir = $(versionedsharedir)/ganeti/rpc/stub
+ gntpythondir = $(versionedsharedir)
+@@ -2386,6 +2387,7 @@ src/AutoConf.hs: Makefile src/AutoConf.hs.in $(PRINT_PY_CONSTANTS) \
+ 	    -DPKGLIBDIR="$(libdir)/ganeti" \
+ 	    -DSHAREDIR="$(prefix)/share/ganeti" \
+ 	    -DVERSIONEDSHAREDIR="$(versionedsharedir)" \
++	    -DPYTHONDIR="$(pythondir)" \
+ 	    -DDRBD_BARRIERS="$(DRBD_BARRIERS)" \
+ 	    -DDRBD_NO_META_FLUSH="$(DRBD_NO_META_FLUSH)" \
+ 	    -DSYSLOG_USAGE="$(SYSLOG_USAGE)" \
+diff --git a/src/AutoConf.hs.in b/src/AutoConf.hs.in
+--- a/src/AutoConf.hs.in
++++ b/src/AutoConf.hs.in
+@@ -157,6 +157,9 @@ sharedir = "SHAREDIR"
+ versionedsharedir :: String
+ versionedsharedir = "VERSIONEDSHAREDIR"
+ 
++pythondir :: String
++pythondir = "PYTHONDIR"
++
+ drbdBarriers :: String
+ drbdBarriers = "DRBD_BARRIERS"
+ 
+diff --git a/src/Ganeti/Path.hs b/src/Ganeti/Path.hs
+--- a/src/Ganeti/Path.hs
++++ b/src/Ganeti/Path.hs
+@@ -188,5 +188,5 @@ getInstReasonFilename instName = instanceReasonDir `pjoin` instName
+ 
+ -- | The path to the Python executable for starting jobs.
+ jqueueExecutorPy :: IO FilePath
+-jqueueExecutorPy = return $ versionedsharedir
+-                            </> "ganeti" </> "jqueue" </> "exec.py"
++jqueueExecutorPy = return $ pythondir
++                           </> "ganeti" </> "jqueue" </> "exec.py"
+diff --git a/src/Ganeti/Query/Exec.hs b/src/Ganeti/Query/Exec.hs
+--- a/src/Ganeti/Query/Exec.hs
++++ b/src/Ganeti/Query/Exec.hs
+@@ -99,12 +99,12 @@ spawnJobProcess jid = withErrorLogAt CRITICAL (show jid) $
+   do
+     use_debug <- isDebugMode
+     env_ <- (M.toList . M.insert "GNT_DEBUG" (if use_debug then "1" else "0")
+-            . M.insert "PYTHONPATH" AC.versionedsharedir
++            . M.insert "PYTHONPATH" AC.pythondir
+             . M.fromList)
+            `liftM` getEnvironment
+     execPy <- P.jqueueExecutorPy
+     logDebug $ "Executing " ++ AC.pythonPath ++ " " ++ execPy
+-               ++ " with PYTHONPATH=" ++ AC.versionedsharedir
++               ++ " with PYTHONPATH=" ++ AC.pythondir
+ 
+     (master, child) <- pipeClient connectConfig
+     let (rh, wh) = clientToHandle child
+
diff --git a/gnu/packages/patches/ganeti-openvswitch-may-exist.patch b/gnu/packages/patches/ganeti-openvswitch-may-exist.patch
new file mode 100644
index 0000000000..03543167f6
--- /dev/null
+++ b/gnu/packages/patches/ganeti-openvswitch-may-exist.patch
@@ -0,0 +1,25 @@
+It is possible that the switch is already configured by external tools.
+Do not error out when that is the case.
+
+Submitted upstream: <https://github.com/ganeti/ganeti/pull/1495>.
+
+diff --git a/lib/backend.py b/lib/backend.py
+--- a/lib/backend.py
++++ b/lib/backend.py
+@@ -5865,14 +5865,14 @@ def ConfigureOVS(ovs_name, ovs_link):
+ 
+   """
+   # Initialize the OpenvSwitch
+-  result = utils.RunCmd(["ovs-vsctl", "add-br", ovs_name])
++  result = utils.RunCmd(["ovs-vsctl", "--may-exist", "add-br", ovs_name])
+   if result.failed:
+     _Fail("Failed to create openvswitch. Script return value: %s, output: '%s'"
+           % (result.exit_code, result.output), log=True)
+ 
+   # And connect it to a physical interface, if given
+   if ovs_link:
+-    result = utils.RunCmd(["ovs-vsctl", "add-port", ovs_name, ovs_link])
++    result = utils.RunCmd(["ovs-vsctl", "--may-exist", "add-port", ovs_name, ovs_link])
+     if result.failed:
+       _Fail("Failed to connect openvswitch to  interface %s. Script return"
+             " value: %s, output: '%s'" % (ovs_link, result.exit_code,
diff --git a/gnu/packages/patches/ganeti-preserve-PYTHONPATH.patch b/gnu/packages/patches/ganeti-preserve-PYTHONPATH.patch
new file mode 100644
index 0000000000..1358e30633
--- /dev/null
+++ b/gnu/packages/patches/ganeti-preserve-PYTHONPATH.patch
@@ -0,0 +1,21 @@
+Do not override PYTHONPATH when calling Python code from the Haskell
+daemons.  This is necessary because the Python library dependencies are
+only available through PYTHONPATH.
+
+diff --git a/src/Ganeti/Query/Exec.hs b/src/Ganeti/Query/Exec.hs
+--- a/src/Ganeti/Query/Exec.hs
++++ b/src/Ganeti/Query/Exec.hs
+@@ -99,12 +99,10 @@ spawnJobProcess jid = withErrorLogAt CRITICAL (show jid) $
+   do
+     use_debug <- isDebugMode
+     env_ <- (M.toList . M.insert "GNT_DEBUG" (if use_debug then "1" else "0")
+-            . M.insert "PYTHONPATH" AC.pythondir
+             . M.fromList)
+            `liftM` getEnvironment
+     execPy <- P.jqueueExecutorPy
+     logDebug $ "Executing " ++ AC.pythonPath ++ " " ++ execPy
+-               ++ " with PYTHONPATH=" ++ AC.pythondir
+ 
+     (master, child) <- pipeClient connectConfig
+     let (rh, wh) = clientToHandle child
+
diff --git a/gnu/packages/patches/ganeti-shepherd-master-failover.patch b/gnu/packages/patches/ganeti-shepherd-master-failover.patch
new file mode 100644
index 0000000000..d255233fc1
--- /dev/null
+++ b/gnu/packages/patches/ganeti-shepherd-master-failover.patch
@@ -0,0 +1,26 @@
+By default, master-failover will call "herd start ganeti-wconfd" with
+extra arguments such as --force-node.  That does not work with the
+Shepherd, so Guix has a "ganeti-wconfd-forced" service for this purpose.
+
+diff --git a/lib/bootstrap.py b/lib/bootstrap.py
+--- a/lib/bootstrap.py
++++ b/lib/bootstrap.py
+@@ -1010,8 +1010,7 @@ def MasterFailover(no_voting=False):
+   try:
+     # Forcefully start WConfd so that we can access the configuration
+     result = utils.RunCmd([pathutils.DAEMON_UTIL,
+-                           "start", constants.WCONFD, "--force-node",
+-                           "--no-voting", "--yes-do-it"])
++                           "start", "ganeti-wconfd-forced"])
+     if result.failed:
+       raise errors.OpPrereqError("Could not start the configuration daemon,"
+                                  " command %s had exitcode %s and error %s" %
+@@ -1074,7 +1073,7 @@ def MasterFailover(no_voting=False):
+     return 1, warnings
+   finally:
+     # stop WConfd again:
+-    result = utils.RunCmd([pathutils.DAEMON_UTIL, "stop", constants.WCONFD])
++    result = utils.RunCmd([pathutils.DAEMON_UTIL, "stop", "ganeti-wconfd-forced"])
+     if result.failed:
+       warning = ("Could not stop the configuration daemon,"
+                  " command %s had exitcode %s and error %s"
diff --git a/gnu/packages/patches/ganeti-shepherd-support.patch b/gnu/packages/patches/ganeti-shepherd-support.patch
new file mode 100644
index 0000000000..f750604344
--- /dev/null
+++ b/gnu/packages/patches/ganeti-shepherd-support.patch
@@ -0,0 +1,87 @@
+Ganeti uses an internal tool to start/stop daemons during init and
+upgrade.  This patch makes the tool use native Shepherd facilities.
+
+diff --git a/daemons/daemon-util.in b/daemons/daemon-util.in
+--- a/daemons/daemon-util.in
++++ b/daemons/daemon-util.in
+@@ -184,6 +184,21 @@ use_systemctl() {
+   return 1
+ }
+ 
++# Checks if we should use the Shepherd to start/stop daemons
++use_shepherd() {
++  # Is Shepherd running as PID 1?
++  ps --no-headers -p 1 -o cmd | grep -q shepherd || return 1
++
++  type -p herd >/dev/null || return 1
++
++  # Does Shepherd know about Ganeti at all?
++  if herd status | grep -q ganeti; then
++    return 0
++  fi
++
++  return 1
++}
++
+ # Prints path to PID file for a daemon.
+ daemon_pidfile() {
+   if [[ "$#" -lt 1 ]]; then
+@@ -261,6 +276,13 @@ check() {
+     else
+       return 1
+     fi
++  elif use_shepherd; then
++    activestate="$(herd status ${name})"
++    if echo $activestate | grep -q Running ; then
++      return 0
++    else
++      return 1
++    fi
+   elif type -p start-stop-daemon >/dev/null; then
+     start-stop-daemon --stop --signal 0 --quiet \
+       --pidfile $pidfile --name "$name"
+@@ -291,6 +313,20 @@ start() {
+     return $?
+   fi
+ 
++  if use_shepherd; then
++    if herd status "$name" | grep -q "disabled"; then
++      # The Shepherd will disable a service that has stopped, even if it exits
++      # gracefully.  Thus, we must re-enable it in case of a master failover.
++      herd enable "${name}"
++    fi
++    # Note: unlike systemd, which happily starts a service and returns success
++    # even if the daemon immediately exits, the Shepherd actually waits for it
++    # to come up.  Thus, ignore the exit status from 'herd start' in case of
++    # master daemons running on the wrong node, or ganeti-kvmd disabled, etc.
++    herd start "${name}"
++    return 0
++  fi
++
+   # Read $<daemon>_ARGS and $EXTRA_<daemon>_ARGS
+   eval local args="\"\$${ucname}_ARGS \$EXTRA_${ucname}_ARGS\""
+ 
+@@ -336,6 +372,13 @@ stop() {
+ 
+   if use_systemctl; then
+     systemctl stop "${name}.service"
++  elif use_shepherd; then
++    if herd status | grep -q "$name"; then
++      herd stop "$name"
++    else
++      # Do not raise an error if the service has not been enabled.
++      return 0
++    fi
+   elif type -p start-stop-daemon >/dev/null; then
+     start-stop-daemon --stop --quiet --oknodo --retry 30 \
+       --pidfile $pidfile --name "$name"
+@@ -352,6 +395,9 @@ check_and_start() {
+     if use_systemctl; then
+       echo "${name} supervised by systemd but not running, will not restart."
+       return 1
++    elif use_shepherd; then
++      echo "${name} supervised by shepherd but not running, will not restart."
++      return 1
+     fi
+ 
+     start $name
diff --git a/gnu/packages/virtualization.scm b/gnu/packages/virtualization.scm
index 64cd815bb8..3ef9b8470a 100644
--- a/gnu/packages/virtualization.scm
+++ b/gnu/packages/virtualization.scm
@@ -38,6 +38,7 @@
   #:use-module (gnu packages attr)
   #:use-module (gnu packages autotools)
   #:use-module (gnu packages backup)
+  #:use-module (gnu packages base)
   #:use-module (gnu packages bison)
   #:use-module (gnu packages check)
   #:use-module (gnu packages cmake)
@@ -61,10 +62,17 @@
   #:use-module (gnu packages gnupg)
   #:use-module (gnu packages golang)
   #:use-module (gnu packages gtk)
+  #:use-module (gnu packages haskell)
+  #:use-module (gnu packages haskell-apps)
+  #:use-module (gnu packages haskell-check)
+  #:use-module (gnu packages haskell-crypto)
+  #:use-module (gnu packages haskell-web)
+  #:use-module (gnu packages haskell-xyz)
   #:use-module (gnu packages image)
   #:use-module (gnu packages libbsd)
   #:use-module (gnu packages libusb)
   #:use-module (gnu packages linux)
+  #:use-module (gnu packages m4)
   #:use-module (gnu packages ncurses)
   #:use-module (gnu packages nettle)
   #:use-module (gnu packages networking)
@@ -75,6 +83,7 @@
   #:use-module (gnu packages polkit)
   #:use-module (gnu packages protobuf)
   #:use-module (gnu packages python)
+  #:use-module (gnu packages python-crypto)
   #:use-module (gnu packages python-web)
   #:use-module (gnu packages python-xyz)
   #:use-module (gnu packages pulseaudio)
@@ -82,6 +91,7 @@
   #:use-module (gnu packages sdl)
   #:use-module (gnu packages sphinx)
   #:use-module (gnu packages spice)
+  #:use-module (gnu packages ssh)
   #:use-module (gnu packages texinfo)
   #:use-module (gnu packages textutils)
   #:use-module (gnu packages tls)
@@ -349,6 +359,374 @@ server and embedded PowerPC, and S390 guests.")
                     "usbredir" "libdrm" "libepoxy" "pulseaudio" "vde2"
                     "libcacard")))))
 
+(define (system->qemu-target system)
+  (cond
+   ((string-prefix? "i686" system)
+    "qemu-system-i386")
+   ((string-prefix? "arm" system)
+    "qemu-system-arm")
+   (else
+    (string-append "qemu-system-" (match (string-split system #\-)
+                                    ((arch kernel) arch)
+                                    (_ system))))))
+
+(define-public ganeti
+  (package
+    (name "ganeti")
+    (version "3.0.0.beta1")
+    (source (origin
+              (method url-fetch)
+              (uri (string-append "https://github.com/ganeti/ganeti/releases"
+                                  "/download/v3.0.0beta1/ganeti-"
+                                  version ".tar.gz"))
+              (sha256
+               (base32 "194mddbbcp8kvqhjxpx3sgvrzhlwfpdwk7m7brvdcrifrblbcd92"))
+              (patches (search-patches "ganeti-shepherd-support.patch"
+                                       "ganeti-shepherd-master-failover.patch"
+                                       "ganeti-copy-hmac.patch"
+                                       "ganeti-openvswitch-may-exist.patch"
+                                       "ganeti-drbd-compat.patch"
+                                       "ganeti-haskell-pythondir.patch"
+                                       "ganeti-disable-version-symlinks.patch"
+                                       "ganeti-preserve-PYTHONPATH.patch"))))
+    (build-system gnu-build-system)
+    (arguments
+     `(#:imported-modules ((guix build haskell-build-system)
+                           (guix build python-build-system)
+                           ,@%gnu-build-system-modules)
+       #:modules ((ice-9 rdelim)
+                  ((guix build haskell-build-system) #:prefix haskell:)
+                  ((guix build python-build-system) #:select (python-version))
+                  ,@%gnu-build-system-modules)
+
+       ;; The default test target includes a lot of checks that are only really
+       ;; relevant for developers such as NEWS file checking, line lengths, etc.
+       ;; We are only interested in the "py-tests" and "hs-tests" targets: this
+       ;; is the closest we've got even though it includes a little more.
+       #:test-target "check-TESTS"
+
+       #:configure-flags
+       (list "--localstatedir=/var"
+             "--sharedstatedir=/var"
+             "--sysconfdir=/etc"
+             "--enable-haskell-tests"
+
+             ;; By default, the build system installs everything to versioned
+             ;; directories such as $libdir/3.0 and relies on a $libdir/default
+             ;; symlink pointed from /etc/ganeti/{lib,share} to actually function.
+             ;; This is done to accommodate installing multiple versions in
+             ;; parallel, but is of little use to us as Guix users can just
+             ;; roll back and forth.  Thus, disable it for simplicity.
+             "--disable-version-links"
+
+             ;; Ganeti can optionally take control over SSH keys and rotate
+             ;; them with 'gnt-cluster renew-crypto', and thus needs to know
+             ;; how to restart the SSH server.
+             "--with-sshd-restart-command='herd restart ssh-daemon'"
+
+             ;; Look for OS definitions in this directory by default.  It can
+             ;; be changed in the cluster configuration.
+             "--with-os-search-path=/run/current-system/profile/share/ganeti/os"
+
+             ;; The default QEMU executable to use.  We don't use the package
+             ;; here because this entry is stored in the cluster configuration.
+             (string-append "--with-kvm-path=/run/current-system/profile/bin/"
+                            ,(system->qemu-target (%current-system))))
+       #:phases
+       (modify-phases %standard-phases
+         (add-before 'bootstrap 'force-bootstrap
+           (lambda _
+             (delete-file "configure")
+             #t))
+         (add-after 'unpack 'adjust-quickcheck-requirement
+           (lambda _
+             ;; The test suite wants QuickCheck 2.12 or lower, but works fine
+             ;; with 2.13.  Just bump the requirement.
+             (substitute* '("ganeti.cabal" "cabal/ganeti.template.cabal")
+               (("&& < 2\\.13")
+                "&& < 2.14"))
+             #t))
+         (add-after 'unpack 'patch-absolute-file-names
+           (lambda _
+             (substitute* '("lib/utils/process.py"
+                            "lib/utils/text.py"
+                            "src/Ganeti/Constants.hs"
+                            "src/Ganeti/HTools/CLI.hs"
+                            "test/py/ganeti.config_unittest.py"
+                            "test/py/ganeti.hooks_unittest.py"
+                            "test/py/ganeti.utils.process_unittest.py"
+                            "test/py/ganeti.utils.text_unittest.py"
+                            "test/py/ganeti.utils.wrapper_unittest.py")
+               (("/bin/sh") (which "sh"))
+               (("/bin/bash") (which "bash"))
+               (("/usr/bin/env") (which "env"))
+               (("/bin/true") (which "true")))
+
+             ;; This script is called by the node daemon at startup to perform
+             ;; sanity checks on the cluster IP addresses, and it is also used
+             ;; in a master-failover scenario.  Add absolute references to
+             ;; avoid propagating these executables.
+             (substitute* "tools/master-ip-setup"
+               (("arping") (which "arping"))
+               (("ndisc6") (which "ndisc6"))
+               (("fping") (which "fping"))
+               (("grep") (which "grep"))
+               (("ip addr") (string-append (which "ip") " addr")))
+             #t))
+         (add-after 'unpack 'override-builtin-PATH
+           (lambda _
+             ;; Ganeti runs OS install scripts and similar with a built-in
+             ;; hard coded PATH.  Patch so it works on Guix System.
+             (substitute* "src/Ganeti/Constants.hs"
+               (("/sbin:/bin:/usr/sbin:/usr/bin")
+                "/run/setuid-programs:/run/current-system/profile/sbin:\
+/run/current-system/profile/bin"))
+             #t))
+
+         ;; The build system invokes Cabal and GHC, which do not work with
+         ;; GHC_PACKAGE_PATH: <https://github.com/haskell/cabal/issues/3728>.
+         ;; Tweak the build system to do roughly what haskell-build-system does.
+         (add-before 'configure 'configure-haskell
+           (assoc-ref haskell:%standard-phases 'setup-compiler))
+         (add-after 'configure 'do-not-use-GHC_PACKAGE_PATH
+           (lambda _
+             (unsetenv "GHC_PACKAGE_PATH")
+             (substitute* "Makefile"
+               (("\\$\\(CABAL\\)")
+                "$(CABAL) --package-db=../package.conf.d")
+               (("\\$\\(GHC\\)")
+                "$(GHC) -package-db=../package.conf.d"))
+             #t))
+
+         (add-after 'configure 'fix-installation-directories
+           (lambda _
+             (substitute* "Makefile"
+               ;; Do not attempt to create /var during install.
+               (("\\$\\(DESTDIR\\)\\$\\{localstatedir\\}")
+                "$(DESTDIR)${prefix}${localstatedir}")
+               ;; Similarly, do not attempt to install the sample ifup scripts
+               ;; to /etc/ganeti.
+               (("\\$\\(DESTDIR\\)\\$\\(ifupdir\\)")
+                "$(DESTDIR)${prefix}$(ifupdir)"))
+             #t))
+         (add-before 'build 'adjust-tests
+           (lambda _
+             ;; Disable tests that can not run.  Do it early to prevent
+             ;; touching the Makefile later and triggering a needless rebuild.
+             (substitute* "Makefile"
+               ;; These tests expect the presence of a 'root' user (via
+               ;; ganeti/runtime.py), which fails in the build environment.
+               (("test/py/ganeti\\.asyncnotifier_unittest\\.py") "")
+               (("test/py/ganeti\\.backend_unittest\\.py") "")
+               (("test/py/ganeti\\.daemon_unittest\\.py") "")
+               (("test/py/ganeti\\.tools\\.ensure_dirs_unittest\\.py") "")
+               (("test/py/ganeti\\.utils\\.io_unittest-runasroot\\.py") "")
+               ;; Disable the bash_completion test, as it requires the full
+               ;; bash instead of bash-minimal.
+               (("test/py/bash_completion\\.bash")
+                "")
+               ;; This test requires networking.
+               (("test/py/import-export_unittest\\.bash")
+                ""))
+
+             ;; Many of the Makefile targets reset PYTHONPATH before running
+             ;; the Python interpreter, which does not work very well for us.
+             (substitute* "Makefile"
+               (("PYTHONPATH=")
+                (string-append "PYTHONPATH=" (getenv "PYTHONPATH") ":")))
+             #t))
+         (add-after 'build 'build-bash-completions
+           (lambda _
+             (let ((orig-pythonpath (getenv "PYTHONPATH")))
+               (setenv "PYTHONPATH" (string-append ".:" orig-pythonpath))
+               (invoke "./autotools/build-bash-completion")
+               (setenv "PYTHONPATH" orig-pythonpath)
+               #t)))
+         (add-before 'check 'pre-check
+           (lambda* (#:key inputs #:allow-other-keys)
+             ;; Set TZDIR so that time zones are found.
+             (setenv "TZDIR" (string-append (assoc-ref inputs "tzdata")
+                                            "/share/zoneinfo"))
+
+             ;; This test checks whether PYTHONPATH is untouched, and extends
+             ;; it to include test directories if so.  Add an else branch for
+             ;; our modified PYTHONPATH, in order to prevent a confusing test
+             ;; failure where expired certificates are not cleaned because
+             ;; check-cert-expired is silently crashing.
+             (substitute* "test/py/ganeti-cleaner_unittest.bash"
+               (("then export PYTHONPATH=(.*)" all testpath)
+                (string-append all "else export PYTHONPATH="
+                               (getenv "PYTHONPATH") ":" testpath "\n")))
+
+             (substitute* "test/py/ganeti.utils.process_unittest.py"
+               ;; This test attempts to run an executable with
+               ;; RunCmd(..., reset_env=True), which fails because the default
+               ;; PATH from Constants.hs does not exist in the build container.
+               ((".*def testResetEnv.*" all)
+                (string-append "  @unittest.skipIf(True, "
+                               "\"cannot reset env in the build container\")\n"
+                               all))
+
+               ;; XXX: Somehow this test fails in the build container, but
+               ;; works in 'guix environment -C', even without /bin/sh?
+               ((".*def testPidFile.*" all)
+                (string-append "  @unittest.skipIf(True, "
+                               "\"testPidFile fails in the build container\")\n"
+                               all)))
+
+             ;; XXX: Why are these links not added automatically.
+             (with-directory-excursion "test/hs"
+               (for-each (lambda (file)
+                           (symlink "../../src/htools" file))
+                         '("hspace" "hscan" "hinfo" "hbal" "hroller"
+                           "hcheck" "hail" "hsqueeze")))
+             #t))
+         (add-after 'install 'install-bash-completions
+           (lambda* (#:key outputs #:allow-other-keys)
+             (let* ((out (assoc-ref outputs "out"))
+                    (compdir (string-append out "/etc/bash_completion.d")))
+               (mkdir-p compdir)
+               (copy-file "doc/examples/bash_completion"
+                             (string-append compdir "/ganeti"))
+               ;; The one file contains completions for many different
+               ;; executables.  Create symlinks for found completions.
+               (with-directory-excursion compdir
+                 (for-each
+                  (lambda (prog) (symlink "ganeti" prog))
+                  (call-with-input-file "ganeti"
+                    (lambda (port)
+                      (let loop ((line (read-line port))
+                                 (progs '()))
+                        (if (eof-object? line)
+                            progs
+                            (if (string-prefix? "complete" line)
+                                (loop (read-line port)
+                                      ;; Extract "prog" from lines of the form:
+                                      ;; "complete -F _prog -o filenames prog".
+                                      ;; Note that 'burnin' is listed with the
+                                      ;; absolute file name, which is why we
+                                      ;; run everything through 'basename'.
+                                      (cons (basename (car (reverse (string-split
+                                                                     line #\ ))))
+                                            progs))
+                                (loop (read-line port) progs))))))))
+               #t)))
+         ;; Wrap all executables with PYTHONPATH.  We can't borrow the phase
+         ;; from python-build-system because we also need to wrap the scripts
+         ;; in $out/lib/ganeti such as "node-daemon-setup".
+         (add-after 'install 'wrap
+           (lambda* (#:key inputs outputs #:allow-other-keys)
+             (let* ((out (assoc-ref outputs "out"))
+                    (sbin (string-append out "/sbin"))
+                    (lib (string-append out "/lib"))
+                    (python (assoc-ref inputs "python"))
+                    (major+minor (python-version python))
+                    (PYTHONPATH (string-append lib "/python" major+minor
+                                               "/site-packages:"
+                                               (getenv "PYTHONPATH"))))
+               (define (shell-script? file)
+                 (call-with-ascii-input-file file
+                   (lambda (port)
+                     (let ((shebang (false-if-exception (read-line port))))
+                       (and shebang
+                            (string-prefix? "#!" shebang)
+                            (or (string-contains shebang "/bin/bash")
+                                (string-contains shebang "/bin/sh")))))))
+
+               (define (wrap? file)
+                 ;; Do not wrap shell scripts because some are meant to be
+                 ;; sourced, which breaks if they are wrapped.  We do wrap
+                 ;; the Haskell executables because some call out to Python
+                 ;; directly.
+                 (and (executable-file? file)
+                      (not (symbolic-link? file))
+                      (not (shell-script? file))))
+
+               (for-each (lambda (file)
+                           (wrap-program file
+                             `("PYTHONPATH" ":" prefix (,PYTHONPATH))))
+                         (filter wrap?
+                                 (append (find-files (string-append lib "/ganeti"))
+                                         (find-files sbin))))
+               #t))))))
+    (native-inputs
+     `(("haskell" ,ghc)
+       ("cabal" ,cabal-install)
+       ("m4" ,m4)
+
+       ;; These inputs are necessary to bootstrap the package, because we
+       ;; have patched the build system.
+       ("autoconf" ,autoconf)
+       ("automake" ,automake)
+
+       ;; Test dependencies.
+       ("fakeroot" ,fakeroot)
+       ("ghc-temporary" ,ghc-temporary)
+       ("ghc-test-framework" ,ghc-test-framework)
+       ("ghc-test-framework-hunit" ,ghc-test-framework-hunit)
+       ("ghc-test-framework-quickcheck2" ,ghc-test-framework-quickcheck2)
+       ("python-mock" ,python-mock)
+       ("python-pyyaml" ,python-pyyaml)
+       ("openssh" ,openssh)
+       ("procps" ,procps)
+       ("shelltestrunner" ,shelltestrunner)
+       ("tzdata" ,tzdata-for-tests)))
+    (inputs
+     `(("arping" ,iputils)              ;must be the iputils version
+       ("curl" ,curl)
+       ("fping" ,fping)
+       ("iproute2" ,iproute)
+       ("ndisc6" ,ndisc6)
+       ("socat" ,socat)
+       ("qemu" ,qemu-minimal)           ;for qemu-img
+       ("ghc-attoparsec" ,ghc-attoparsec)
+       ("ghc-base64-bytestring" ,ghc-base64-bytestring)
+       ("ghc-cryptonite" ,ghc-cryptonite)
+       ("ghc-curl" ,ghc-curl)
+       ("ghc-hinotify" ,ghc-hinotify)
+       ("ghc-hslogger" ,ghc-hslogger)
+       ("ghc-json" ,ghc-json)
+       ("ghc-lens" ,ghc-lens)
+       ("ghc-lifted-base" ,ghc-lifted-base)
+       ("ghc-network" ,ghc-network)
+       ("ghc-old-time" ,ghc-old-time)
+       ("ghc-psqueue" ,ghc-psqueue)
+       ("ghc-regex-pcre" ,ghc-regex-pcre)
+       ("ghc-utf8-string" ,ghc-utf8-string)
+       ("ghc-zlib" ,ghc-zlib)
+
+       ;; For the optional metadata daemon.
+       ("ghc-snap-core" ,ghc-snap-core)
+       ("ghc-snap-server" ,ghc-snap-server)
+
+       ("python" ,python)
+       ("python-pyopenssl" ,python-pyopenssl)
+       ("python-simplejson" ,python-simplejson)
+       ("python-pyparsing" ,python-pyparsing)
+       ("python-pyinotify" ,python-pyinotify)
+       ("python-pycurl" ,python-pycurl)
+       ("python-bitarray" ,python-bitarray)
+       ("python-paramiko" ,python-paramiko)
+       ("python-psutil" ,python-psutil)))
+    (home-page "http://www.ganeti.org/")
+    (synopsis "Cluster-based virtual machine management system")
+    (description
+     "Ganeti is a virtual machine management tool built on top of existing
+virtualization technologies such as Xen or KVM.  Once installed, Ganeti
+assumes management of the virtual instances (Xen DomU).  Ganeti controls:
+
+@itemize @bullet
+@item Disk creation management;
+@item Operating system installation for instances (in co-operation with
+OS-specific install scripts); and
+@item Startup, shutdown, and failover between physical systems.
+@end itemize
+
+Ganeti is designed to facilitate cluster management of virtual servers and
+to provide fast and simple recovery after physical failures, using
+commodity hardware.")
+    (license license:bsd-2)))
+
 (define-public libosinfo
   (package
     (name "libosinfo")
-- 
2.27.0