/*
 * qemu_nbdkit.c: helpers for using nbdkit
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.  If not, see
 * <http://www.gnu.org/licenses/>.
 *
 */

#include <config.h>
#include <glib.h>
#if WITH_NBDKIT
# include <libnbd.h>
#endif
#include <sys/syscall.h>

#include "vircommand.h"
#include "virerror.h"
#include "virlog.h"
#include "virpidfile.h"
#include "virtime.h"
#include "virutil.h"
#include "qemu_block.h"
#include "qemu_conf.h"
#include "qemu_domain.h"
#include "qemu_extdevice.h"
#include "qemu_nbdkit.h"
#define LIBVIRT_QEMU_NBDKITPRIV_H_ALLOW
#include "qemu_nbdkitpriv.h"
#include "qemu_process.h"
#include "qemu_security.h"

#include <fcntl.h>

#define VIR_FROM_THIS VIR_FROM_QEMU

VIR_LOG_INIT("qemu.nbdkit");

#if WITH_NBDKIT
# define WITHOUT_NBDKIT_UNUSED
#else
# define WITHOUT_NBDKIT_UNUSED G_GNUC_UNUSED
#endif

VIR_ENUM_IMPL(qemuNbdkitCaps,
    QEMU_NBDKIT_CAPS_LAST,
    /* 0 */
    "plugin-curl", /* QEMU_NBDKIT_CAPS_PLUGIN_CURL */
    "plugin-ssh", /* QEMU_NBDKIT_CAPS_PLUGIN_SSH */
    "filter-readahead", /* QEMU_NBDKIT_CAPS_FILTER_READAHEAD */
);

struct _qemuNbdkitCaps {
    GObject parent;

    char *path;
    char *version;
    char *filterDir;
    char *pluginDir;

    time_t ctime;
    time_t libvirtCtime;
    time_t pluginDirMtime;
    time_t filterDirMtime;
    unsigned int libvirtVersion;

    virBitmap *flags;
};
G_DEFINE_TYPE(qemuNbdkitCaps, qemu_nbdkit_caps, G_TYPE_OBJECT);


static void
qemuNbdkitCheckCommandCap(qemuNbdkitCaps *nbdkit,
                          virCommand *cmd,
                          qemuNbdkitCapsFlags cap)
{
    if (virCommandRun(cmd, NULL) != 0)
        return;

    VIR_DEBUG("Setting nbdkit capability %i", cap);
    ignore_value(virBitmapSetBit(nbdkit->flags, cap));
}


static void
qemuNbdkitQueryFilter(qemuNbdkitCaps *nbdkit,
                      const char *filter,
                      qemuNbdkitCapsFlags cap)
{
    g_autoptr(virCommand) cmd = virCommandNewArgList(nbdkit->path,
                                                     "--version",
                                                     NULL);

    virCommandAddArgPair(cmd, "--filter", filter);

    /* use null plugin to check for filter */
    virCommandAddArg(cmd, "null");

    qemuNbdkitCheckCommandCap(nbdkit, cmd, cap);
}


static void
qemuNbdkitQueryPlugin(qemuNbdkitCaps *nbdkit,
                      const char *plugin,
                      qemuNbdkitCapsFlags cap)
{
    g_autoptr(virCommand) cmd = virCommandNewArgList(nbdkit->path,
                                                     plugin,
                                                     "--version",
                                                     NULL);

    qemuNbdkitCheckCommandCap(nbdkit, cmd, cap);
}


static void
qemuNbdkitCapsQueryPlugins(qemuNbdkitCaps *nbdkit)
{
    qemuNbdkitQueryPlugin(nbdkit, "curl", QEMU_NBDKIT_CAPS_PLUGIN_CURL);
    qemuNbdkitQueryPlugin(nbdkit, "ssh", QEMU_NBDKIT_CAPS_PLUGIN_SSH);
}


static void
qemuNbdkitCapsQueryFilters(qemuNbdkitCaps *nbdkit)
{
    qemuNbdkitQueryFilter(nbdkit, "readahead",
                          QEMU_NBDKIT_CAPS_FILTER_READAHEAD);
}


static int
qemuNbdkitCapsQueryBuildConfig(qemuNbdkitCaps *nbdkit)
{
    size_t i;
    g_autofree char *output = NULL;
    g_auto(GStrv) lines = NULL;
    char *line;
    g_autoptr(virCommand) cmd = virCommandNewArgList(nbdkit->path,
                                                     "--dump-config",
                                                     NULL);

    virCommandSetOutputBuffer(cmd, &output);

    if (virCommandRun(cmd, NULL) != 0)
        return -1;

    lines = g_strsplit(output, "\n", 0);
    if (!lines)
        return -1;

    for (i = 0; (line = lines[i]); i++) {
        const char *key;
        const char *val;
        char *p;

        p = strchr(line, '=');
        if (!p)
            continue;

        *p = '\0';
        key = line;
        val = p + 1;

        VIR_DEBUG("Got nbdkit config value %s=%s", key, val);

        if (STREQ(key, "version"))
            nbdkit->version = g_strdup(val);
        else if (STREQ(key, "filterdir"))
            nbdkit->filterDir = g_strdup(val);
        else if (STREQ(key, "plugindir"))
            nbdkit->pluginDir = g_strdup(val);
    }
    return 0;
}


static void
qemuNbdkitCapsFinalize(GObject *object)
{
    qemuNbdkitCaps *nbdkit = QEMU_NBDKIT_CAPS(object);

    g_clear_pointer(&nbdkit->path, g_free);
    g_clear_pointer(&nbdkit->version, g_free);
    g_clear_pointer(&nbdkit->filterDir, g_free);
    g_clear_pointer(&nbdkit->pluginDir, g_free);
    g_clear_pointer(&nbdkit->flags, virBitmapFree);

    G_OBJECT_CLASS(qemu_nbdkit_caps_parent_class)->finalize(object);
}


void
qemu_nbdkit_caps_init(qemuNbdkitCaps *caps)
{
    caps->flags = virBitmapNew(QEMU_NBDKIT_CAPS_LAST);
    caps->version = NULL;
}


static void
qemu_nbdkit_caps_class_init(qemuNbdkitCapsClass *klass)
{
    GObjectClass *obj = G_OBJECT_CLASS(klass);

    obj->finalize = qemuNbdkitCapsFinalize;
}


qemuNbdkitCaps *
qemuNbdkitCapsNew(const char *path)
{
    qemuNbdkitCaps *caps = g_object_new(QEMU_TYPE_NBDKIT_CAPS, NULL);
    caps->path = g_strdup(path);

    return caps;
}


static time_t
qemuNbdkitGetDirMtime(const char *moddir)
{
    struct stat st;

    if (stat(moddir, &st) < 0) {
        VIR_DEBUG("Failed to stat nbdkit module directory '%s': %s",
                  moddir,
                  g_strerror(errno));
        return 0;
    }

    return st.st_mtime;
}


static void
qemuNbdkitCapsQuery(qemuNbdkitCaps *caps)
{
    struct stat st;

    if (stat(caps->path, &st) < 0) {
        VIR_DEBUG("Failed to stat nbdkit binary '%s': %s",
                  caps->path,
                  g_strerror(errno));
        caps->ctime  = 0;
        return;
    }

    qemuNbdkitCapsQueryBuildConfig(caps);
    qemuNbdkitCapsQueryPlugins(caps);
    qemuNbdkitCapsQueryFilters(caps);

    caps->ctime = st.st_ctime;
    caps->filterDirMtime = qemuNbdkitGetDirMtime(caps->filterDir);
    caps->pluginDirMtime = qemuNbdkitGetDirMtime(caps->pluginDir);
    caps->libvirtCtime = virGetSelfLastChanged();
    caps->libvirtVersion = LIBVIR_VERSION_NUMBER;
}


bool
qemuNbdkitCapsGet(qemuNbdkitCaps *nbdkitCaps,
                  qemuNbdkitCapsFlags flag)
{
    return virBitmapIsBitSet(nbdkitCaps->flags, flag);
}


void
qemuNbdkitCapsSet(qemuNbdkitCaps *nbdkitCaps,
                  qemuNbdkitCapsFlags flag)
{
    ignore_value(virBitmapSetBit(nbdkitCaps->flags, flag));
}


static bool
virNbkditCapsCheckModdir(const char *moddir,
                         time_t expectedMtime)
{
    time_t mtime = qemuNbdkitGetDirMtime(moddir);

    if (mtime != expectedMtime) {
        VIR_DEBUG("Outdated capabilities for nbdkit: module directory '%s' changed (%lld vs %lld)",
                  moddir, (long long)mtime, (long long)expectedMtime);
        return false;
    }
    return true;
}


static bool
virNbdkitCapsIsValid(void *data,
                     void *privData)
{
    qemuNbdkitCaps *nbdkitCaps = data;
    struct stat st;
    /* when run under test, we will use privData as a signal to indicate that
     * we shouldn't touch the filesystem */
    bool skipValidation = (privData != NULL);

    if (skipValidation)
        return true;

    if (!nbdkitCaps->path)
        return true;

    if (!virNbkditCapsCheckModdir(nbdkitCaps->pluginDir, nbdkitCaps->pluginDirMtime))
        return false;
    if (!virNbkditCapsCheckModdir(nbdkitCaps->filterDir, nbdkitCaps->filterDirMtime))
        return false;

    if (nbdkitCaps->libvirtCtime != virGetSelfLastChanged() ||
        nbdkitCaps->libvirtVersion != LIBVIR_VERSION_NUMBER) {
        VIR_DEBUG("Outdated capabilities for '%s': libvirt changed (%lld vs %lld, %lu vs %lu)",
                  nbdkitCaps->path,
                  (long long)nbdkitCaps->libvirtCtime,
                  (long long)virGetSelfLastChanged(),
                  (unsigned long)nbdkitCaps->libvirtVersion,
                  (unsigned long)LIBVIR_VERSION_NUMBER);
        return false;
    }

    if (stat(nbdkitCaps->path, &st) < 0) {
        VIR_DEBUG("Failed to stat nbdkit binary '%s': %s",
                  nbdkitCaps->path,
                  g_strerror(errno));
        return false;
    }

    if (st.st_ctime != nbdkitCaps->ctime) {
        VIR_DEBUG("Outdated capabilities for '%s': nbdkit binary changed (%lld vs %lld)",
                  nbdkitCaps->path,
                  (long long)st.st_ctime, (long long)nbdkitCaps->ctime);
        return false;
    }

    return true;
}


static void*
virNbdkitCapsNewData(const char *binary,
                     void *privData)
{
    /* when run under test, we will use privData as a signal to indicate that
     * we shouldn't touch the filesystem */
    bool skipNewData = (privData != NULL);
    qemuNbdkitCaps *caps = NULL;

    if (skipNewData)
        return NULL;

    caps = qemuNbdkitCapsNew(binary);
    qemuNbdkitCapsQuery(caps);

    return caps;
}


static int
qemuNbdkitCapsValidateBinary(qemuNbdkitCaps *nbdkitCaps,
                             xmlXPathContextPtr ctxt)
{
    g_autofree char *str = NULL;

    if (!(str = virXPathString("string(./path)", ctxt))) {
        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
                       _("missing path in nbdkit capabilities cache"));
        return -1;
    }

    if (STRNEQ(str, nbdkitCaps->path)) {
        virReportError(VIR_ERR_INTERNAL_ERROR,
                       _("Expected caps for '%1$s' but saw '%2$s'"),
                       nbdkitCaps->path, str);
        return -1;
    }

    return 0;
}


static int
qemuNbdkitCapsParseFlags(qemuNbdkitCaps *nbdkitCaps,
                         xmlXPathContextPtr ctxt)
{
    g_autofree xmlNodePtr *nodes = NULL;
    size_t i;
    int n;

    if ((n = virXPathNodeSet("./flag", ctxt, &nodes)) < 0)
        return -1;

    VIR_DEBUG("Got flags %d", n);
    for (i = 0; i < n; i++) {
        unsigned int flag;

        if (virXMLPropEnum(nodes[i], "name", qemuNbdkitCapsTypeFromString,
                           VIR_XML_PROP_REQUIRED, &flag) < 0)
            return -1;

        qemuNbdkitCapsSet(nbdkitCaps, flag);
    }

    return 0;
}


/*
 * Parsing a doc that looks like
 *
 * <nbdkitCaps>
 *   <path>/some/path</path>
 *   <nbdkitctime>234235253</nbdkitctime>
 *   <plugindirmtime>234235253</plugindirmtime>
 *   <filterdirmtime>234235253</filterdirmtime>
 *   <selfctime>234235253</selfctime>
 *   <selfvers>1002016</selfvers>
 *   <flag name='foo'/>
 *   <flag name='bar'/>
 *   ...
 * </nbdkitCaps>
 *
 * Returns 0 on success, 1 if outdated, -1 on error
 */
static int
qemuNbdkitCapsLoadCache(qemuNbdkitCaps *nbdkitCaps,
                        const char *filename)
{
    g_autoptr(xmlDoc) doc = NULL;
    g_autoptr(xmlXPathContext) ctxt = NULL;
    long long int l;

    if (!(doc = virXMLParse(filename, NULL, NULL, "nbdkitCaps", &ctxt, NULL, false)))
        return -1;

    if (virXPathLongLong("string(./selfctime)", ctxt, &l) < 0) {
        VIR_DEBUG("missing selfctime in nbdkit capabilities XML");
        return -1;
    }
    nbdkitCaps->libvirtCtime = (time_t)l;

    nbdkitCaps->libvirtVersion = 0;
    virXPathUInt("string(./selfvers)", ctxt, &nbdkitCaps->libvirtVersion);

    if (nbdkitCaps->libvirtCtime != virGetSelfLastChanged() ||
        nbdkitCaps->libvirtVersion != LIBVIR_VERSION_NUMBER) {
        VIR_DEBUG("Outdated capabilities in %s: libvirt changed (%lld vs %lld, %lu vs %lu), stopping load",
                  nbdkitCaps->path,
                  (long long)nbdkitCaps->libvirtCtime,
                  (long long)virGetSelfLastChanged(),
                  (unsigned long)nbdkitCaps->libvirtVersion,
                  (unsigned long)LIBVIR_VERSION_NUMBER);
        return 1;
    }

    if (qemuNbdkitCapsValidateBinary(nbdkitCaps, ctxt) < 0)
        return -1;

    if (virXPathLongLong("string(./nbdkitctime)", ctxt, &l) < 0) {
        VIR_DEBUG("missing nbdkitctime in nbdkit capabilities XML");
        return -1;
    }
    nbdkitCaps->ctime = (time_t)l;

    if ((nbdkitCaps->pluginDir = virXPathString("string(./plugindir)", ctxt)) == NULL) {
        VIR_DEBUG("missing plugindir in nbdkit capabilities cache");
        return -1;
    }

    if (virXPathLongLong("string(./plugindirmtime)", ctxt, &l) < 0) {
        VIR_DEBUG("missing plugindirmtime in nbdkit capabilities XML");
        return -1;
    }
    nbdkitCaps->pluginDirMtime = (time_t)l;

    if ((nbdkitCaps->filterDir = virXPathString("string(./filterdir)", ctxt)) == NULL) {
        VIR_DEBUG("missing filterdir in nbdkit capabilities cache");
        return -1;
    }

    if (virXPathLongLong("string(./filterdirmtime)", ctxt, &l) < 0) {
        VIR_DEBUG("missing filterdirmtime in nbdkit capabilities XML");
        return -1;
    }
    nbdkitCaps->filterDirMtime = (time_t)l;

    if (qemuNbdkitCapsParseFlags(nbdkitCaps, ctxt) < 0)
        return -1;

    if ((nbdkitCaps->version = virXPathString("string(./version)", ctxt)) == NULL) {
        VIR_DEBUG("missing version in nbdkit capabilities cache");
        return -1;
    }

    return 0;
}


static void*
virNbdkitCapsLoadFile(const char *filename,
                      const char *binary,
                      void *privData G_GNUC_UNUSED,
                      bool *outdated)
{
    g_autoptr(qemuNbdkitCaps) nbdkitCaps = qemuNbdkitCapsNew(binary);
    int ret;

    if (!nbdkitCaps)
        return NULL;

    ret = qemuNbdkitCapsLoadCache(nbdkitCaps, filename);
    if (ret < 0)
        return NULL;
    if (ret == 1) {
        *outdated = true;
        return NULL;
    }

    return g_steal_pointer(&nbdkitCaps);
}


static char*
qemuNbdkitCapsFormatCache(qemuNbdkitCaps *nbdkitCaps)
{
    g_auto(virBuffer) buf = VIR_BUFFER_INITIALIZER;
    size_t i;

    virBufferAddLit(&buf, "<nbdkitCaps>\n");
    virBufferAdjustIndent(&buf, 2);

    virBufferEscapeString(&buf, "<path>%s</path>\n",
                          nbdkitCaps->path);
    virBufferAsprintf(&buf, "<nbdkitctime>%lld</nbdkitctime>\n",
                      (long long)nbdkitCaps->ctime);
    virBufferEscapeString(&buf, "<plugindir>%s</plugindir>\n",
                          nbdkitCaps->pluginDir);
    virBufferAsprintf(&buf, "<plugindirmtime>%lld</plugindirmtime>\n",
                      (long long)nbdkitCaps->pluginDirMtime);
    virBufferEscapeString(&buf, "<filterdir>%s</filterdir>\n",
                          nbdkitCaps->filterDir);
    virBufferAsprintf(&buf, "<filterdirmtime>%lld</filterdirmtime>\n",
                      (long long)nbdkitCaps->filterDirMtime);
    virBufferAsprintf(&buf, "<selfctime>%lld</selfctime>\n",
                      (long long)nbdkitCaps->libvirtCtime);
    virBufferAsprintf(&buf, "<selfvers>%u</selfvers>\n",
                      nbdkitCaps->libvirtVersion);

    for (i = 0; i < QEMU_NBDKIT_CAPS_LAST; i++) {
        if (qemuNbdkitCapsGet(nbdkitCaps, i)) {
            virBufferAsprintf(&buf, "<flag name='%s'/>\n",
                              qemuNbdkitCapsTypeToString(i));
        }
    }

    virBufferAsprintf(&buf, "<version>%s</version>\n",
                      nbdkitCaps->version);

    virBufferAdjustIndent(&buf, -2);
    virBufferAddLit(&buf, "</nbdkitCaps>\n");

    return virBufferContentAndReset(&buf);
}


static int
virNbdkitCapsSaveFile(void *data,
                      const char *filename,
                      void *privData G_GNUC_UNUSED)
{
    qemuNbdkitCaps *nbdkitCaps = data;
    g_autofree char *xml = NULL;

    xml = qemuNbdkitCapsFormatCache(nbdkitCaps);

    if (virFileWriteStr(filename, xml, 0600) < 0) {
        virReportSystemError(errno,
                             _("Failed to save '%1$s' for '%2$s'"),
                             filename, nbdkitCaps->path);
        return -1;
    }

    VIR_DEBUG("Saved caps '%s' for '%s' with (%lld, %lld)",
              filename, nbdkitCaps->path,
              (long long)nbdkitCaps->ctime,
              (long long)nbdkitCaps->libvirtCtime);

    return 0;
}


virFileCacheHandlers nbdkitCapsCacheHandlers = {
    .isValid = virNbdkitCapsIsValid,
    .newData = virNbdkitCapsNewData,
    .loadFile = virNbdkitCapsLoadFile,
    .saveFile = virNbdkitCapsSaveFile,
    .privFree = NULL,
};


virFileCache*
qemuNbdkitCapsCacheNew(const char *cachedir)
{
    g_autofree char *dir = g_build_filename(cachedir, "nbdkitcapabilities", NULL);
    return virFileCacheNew(dir, "xml", &nbdkitCapsCacheHandlers);
}


int
qemuNbdkitProcessRestart(qemuNbdkitProcess *proc,
                         virDomainObj *vm)
{
    qemuDomainObjPrivate *vmpriv = vm->privateData;
    virQEMUDriver *driver = vmpriv->driver;

    /* clean up resources associated with process */
    qemuNbdkitProcessStop(proc, vm);

    return qemuNbdkitProcessStart(proc, vm, driver);
}


#if WITH_NBDKIT
typedef struct {
    qemuNbdkitProcess *proc;
    virDomainObj *vm;
} qemuNbdkitProcessEventData;


static qemuNbdkitProcessEventData*
qemuNbdkitProcessEventDataNew(qemuNbdkitProcess *proc,
                              virDomainObj *vm)
{
    qemuNbdkitProcessEventData *d = g_new0(qemuNbdkitProcessEventData, 1);
    d->proc = proc;
    d->vm = virObjectRef(vm);
    return d;
}


static void
qemuNbdkitProcessEventDataFree(qemuNbdkitProcessEventData *d)
{
    virObjectUnref(d->vm);
    g_free(d);
}


static void
qemuNbdkitProcessPidfdCb(int watch G_GNUC_UNUSED,
                         int fd,
                         int events G_GNUC_UNUSED,
                         void *opaque)
{
    qemuNbdkitProcessEventData *d = opaque;

    VIR_FORCE_CLOSE(fd);
    /* submit an event so that it is handled in the per-vm event thread */
    qemuProcessHandleNbdkitExit(d->proc, d->vm);
}
#endif /* WITH_NBDKIT */


static int
qemuNbdkitProcessStartMonitor(qemuNbdkitProcess *proc WITHOUT_NBDKIT_UNUSED,
                              virDomainObj *vm WITHOUT_NBDKIT_UNUSED)
{
#if WITH_NBDKIT
    int pidfd;
    qemuNbdkitProcessEventData *data;

    pidfd = syscall(SYS_pidfd_open, proc->pid, 0);
    if (pidfd < 0) {
        virReportSystemError(errno, _("pidfd_open failed for %1$i"), proc->pid);
        return -1;
    }

    data = qemuNbdkitProcessEventDataNew(proc, vm);
    if ((proc->eventwatch = virEventAddHandle(pidfd,
                                              VIR_EVENT_HANDLE_READABLE,
                                              qemuNbdkitProcessPidfdCb,
                                              data,
                                              (virFreeCallback)qemuNbdkitProcessEventDataFree)) < 0) {
        virReportError(VIR_ERR_INTERNAL_ERROR,
                       _("failed to monitor nbdkit process %1$i"),
                       proc->pid);
        VIR_FORCE_CLOSE(pidfd);
        qemuNbdkitProcessEventDataFree(data);
        return -1;
    }

    VIR_DEBUG("Monitoring nbdkit process %i for exit", proc->pid);

    return 0;
#else
    /* This should not be reachable */
    virReportError(VIR_ERR_NO_SUPPORT, "%s",
                   _("nbdkit support is not enabled"));
    return -1;
#endif /* WITH_NBDKIT */
}


static void
qemuNbdkitProcessStopMonitor(qemuNbdkitProcess *proc WITHOUT_NBDKIT_UNUSED)
{
#if WITH_NBDKIT
    if (proc->eventwatch > 0) {
        virEventRemoveHandle(proc->eventwatch);
        proc->eventwatch = 0;
    }
#endif /* WITH_NBDKIT */
}


static qemuNbdkitProcess *
qemuNbdkitProcessNew(virStorageSource *source,
                     const char *pidfile,
                     const char *socketfile)
{
    qemuNbdkitProcess *nbdkit = g_new0(qemuNbdkitProcess, 1);
    /* weak reference -- source owns this object, so it will always outlive us */
    nbdkit->source = source;
    nbdkit->user = -1;
    nbdkit->group = -1;
    nbdkit->pid = -1;
    nbdkit->pidfile = g_strdup(pidfile);
    nbdkit->socketfile = g_strdup(socketfile);

    return nbdkit;
}

/**
 * qemuNbdkitReconnectStorageSource:
 * @source: a storage source
 * @pidfile: a pidfile for an nbdkit process
 * @socketfile: the socket file associated with the nbdkit process
 *
 * This function constructs a new qemuNbdkitProcess object with the given values for @pidfile and
 * @socketfile and stores it in @source. This is intended to be called when the libvirt daemon is
 * restarted and tries to reconnect to all currently-running domains. Since this function is called
 * from the code that parses the current daemon state, it should not perform any filesystem
 * operations, or anything else that might fail. Additional initialization will be done later by
 * calling qemuNbdkitStorageSourceManageProcess().
 */
void
qemuNbdkitReconnectStorageSource(virStorageSource *source,
                                 const char *pidfile,
                                 const char *socketfile)
{
    qemuDomainStorageSourcePrivate *srcpriv = qemuDomainStorageSourcePrivateFetch(source);

    if (srcpriv->nbdkitProcess) {
        VIR_WARN("source already has an nbdkit process");
        return;
    }

    srcpriv->nbdkitProcess = qemuNbdkitProcessNew(source, pidfile, socketfile);
}


static int
qemuNbdkitStorageSourceManageProcessOne(virStorageSource *source,
                                        virDomainObj *vm)
{
    qemuDomainStorageSourcePrivate *srcpriv = QEMU_DOMAIN_STORAGE_SOURCE_PRIVATE(source);
    qemuDomainObjPrivate *vmpriv = vm->privateData;
    qemuNbdkitProcess *proc;

    if (!srcpriv)
        return 0;

    proc = srcpriv->nbdkitProcess;

    if (!proc)
        return 0;

    if (!proc->caps)
        proc->caps = qemuGetNbdkitCaps(vmpriv->driver);

    if (proc->pid <= 0) {
        if (virPidFileReadPath(proc->pidfile, &proc->pid) < 0) {
            virReportError(VIR_ERR_INTERNAL_ERROR,
                           _("Unable to read pidfile '%1$s'"),
                           proc->pidfile);
            return -1;
        }
    }

    if (virProcessKill(proc->pid, 0) < 0) {
        VIR_DEBUG("nbdkit process %i is not alive", proc->pid);
        return qemuNbdkitProcessRestart(proc, vm);
    }

    return qemuNbdkitProcessStartMonitor(proc, vm);
}

/**
 * qemuNbdkitStorageSourceManageProcess:
 * @source: a storage source
 * @vm: the vm that owns this storage source
 *
 * This function re-enables monitoring of any nbdkit processes associated with the backing chain of
 * @source. It is intended to be called after libvirt restarts and has loaded its current state from
 * disk and is attempting to re-connect to active domains.
 */
int
qemuNbdkitStorageSourceManageProcess(virStorageSource *source,
                                     virDomainObj *vm)
{
    virStorageSource *backing;
    for (backing = source; backing != NULL; backing = backing->backingStore)
        if (qemuNbdkitStorageSourceManageProcessOne(backing, vm) < 0)
            return -1;

    return 0;
}


bool
qemuNbdkitInitStorageSource(qemuNbdkitCaps *caps WITHOUT_NBDKIT_UNUSED,
                            virStorageSource *source WITHOUT_NBDKIT_UNUSED,
                            char *statedir WITHOUT_NBDKIT_UNUSED,
                            const char *alias WITHOUT_NBDKIT_UNUSED,
                            uid_t user WITHOUT_NBDKIT_UNUSED,
                            gid_t group WITHOUT_NBDKIT_UNUSED)
{
#if !WITH_NBDKIT
    /* if nbdkit support is not enabled, don't construct the object so the
     * calling function will fall back to qemu storage drivers */
    return false;
#else
    qemuDomainStorageSourcePrivate *srcPriv = qemuDomainStorageSourcePrivateFetch(source);
    g_autofree char *pidname = g_strdup_printf("nbdkit-%s.pid", alias);
    g_autofree char *socketname = g_strdup_printf("nbdkit-%s.socket", alias);
    g_autofree char *pidfile = g_build_filename(statedir, pidname, NULL);
    g_autofree char *socketfile = g_build_filename(statedir, socketname, NULL);
    qemuNbdkitProcess *proc;

    if (srcPriv->nbdkitProcess)
        return false;

    switch (source->protocol) {
        case VIR_STORAGE_NET_PROTOCOL_HTTP:
        case VIR_STORAGE_NET_PROTOCOL_HTTPS:
        case VIR_STORAGE_NET_PROTOCOL_FTP:
        case VIR_STORAGE_NET_PROTOCOL_FTPS:
        case VIR_STORAGE_NET_PROTOCOL_TFTP:
            if (!virBitmapIsBitSet(caps->flags, QEMU_NBDKIT_CAPS_PLUGIN_CURL))
                return false;
            break;
        case VIR_STORAGE_NET_PROTOCOL_SSH:
            if (!virBitmapIsBitSet(caps->flags, QEMU_NBDKIT_CAPS_PLUGIN_SSH))
                return false;
            break;
        case VIR_STORAGE_NET_PROTOCOL_NONE:
        case VIR_STORAGE_NET_PROTOCOL_NBD:
        case VIR_STORAGE_NET_PROTOCOL_RBD:
        case VIR_STORAGE_NET_PROTOCOL_SHEEPDOG:
        case VIR_STORAGE_NET_PROTOCOL_GLUSTER:
        case VIR_STORAGE_NET_PROTOCOL_ISCSI:
        case VIR_STORAGE_NET_PROTOCOL_VXHS:
        case VIR_STORAGE_NET_PROTOCOL_NFS:
        case VIR_STORAGE_NET_PROTOCOL_LAST:
            return false;
    }

    proc = qemuNbdkitProcessNew(source, pidfile, socketfile);
    proc->caps = g_object_ref(caps);
    proc->user = user;
    proc->group = group;

    srcPriv->nbdkitProcess = proc;

    return true;
#endif /* WITH_NBDKIT */
}


static int
qemuNbdkitStartStorageSourceOne(virQEMUDriver *driver,
                                virDomainObj *vm,
                                virStorageSource *src)
{
    qemuDomainStorageSourcePrivate *priv = QEMU_DOMAIN_STORAGE_SOURCE_PRIVATE(src);

    if (priv && priv->nbdkitProcess &&
        qemuNbdkitProcessStart(priv->nbdkitProcess, vm, driver) < 0)
        return -1;

    return 0;
}


int
qemuNbdkitStartStorageSource(virQEMUDriver *driver,
                             virDomainObj *vm,
                             virStorageSource *src,
                             bool chain)
{
    virStorageSource *backing;

    for (backing = src; backing != NULL; backing = backing->backingStore) {
        if (qemuNbdkitStartStorageSourceOne(driver, vm, backing) < 0) {
            /* roll back any previously-started sources */
            qemuNbdkitStopStorageSource(src, vm, chain);
            return -1;
        }
        if (!chain)
            break;
    }

    return 0;
}


static void
qemuNbdkitStopStorageSourceOne(virStorageSource *src,
                               virDomainObj *vm)
{
    qemuDomainStorageSourcePrivate *priv = QEMU_DOMAIN_STORAGE_SOURCE_PRIVATE(src);

    if (priv && priv->nbdkitProcess &&
        qemuNbdkitProcessStop(priv->nbdkitProcess, vm) < 0)
        VIR_WARN("Unable to stop nbdkit for storage source '%s'",
                 qemuBlockStorageSourceGetStorageNodename(src));
}


void
qemuNbdkitStopStorageSource(virStorageSource *src,
                            virDomainObj *vm,
                            bool chain)
{
    virStorageSource *backing;

    for (backing = src; backing != NULL; backing = backing->backingStore) {
        qemuNbdkitStopStorageSourceOne(backing, vm);
        if (!chain)
            break;
    }
}


static int
qemuNbdkitCommandPassDataByPipe(virCommand *cmd,
                                const char *argName,
                                unsigned char **buf,
                                size_t buflen)
{
    g_autofree char *fdfmt = NULL;
    int fd = virCommandSetSendBuffer(cmd, buf, buflen);

    if (fd < 0)
        return -1;

    /* some nbdkit arguments accept a variation where nbdkit will read the data
     * from a file descriptor, e.g. password=-FD */
    fdfmt = g_strdup_printf("-%i", fd);
    virCommandAddArgPair(cmd, argName, fdfmt);

    virCommandDoAsyncIO(cmd);

    return 0;
}


static int
qemuNbdkitProcessBuildCommandAuth(virStorageAuthDef *authdef,
                                  virCommand *cmd)
{
    g_autoptr(virConnect) conn = NULL;
    g_autofree uint8_t *secret = NULL;
    size_t secretlen = 0;
    int secrettype;

    if (!authdef)
        return 0;

    if ((secrettype = virSecretUsageTypeFromString(authdef->secrettype)) < 0) {
        virReportError(VIR_ERR_CONFIG_UNSUPPORTED,
                       _("invalid secret type %1$s"),
                       authdef->secrettype);
        return -1;
    }

    conn = virGetConnectSecret();
    if (!conn)
        return -1;

    if (virSecretGetSecretString(conn,
                                 &authdef->seclookupdef,
                                 secrettype,
                                 &secret,
                                 &secretlen) < 0)
        return -1;

    virCommandAddArgPair(cmd, "user", authdef->username);

    if (qemuNbdkitCommandPassDataByPipe(cmd, "password",
                                        &secret, secretlen) < 0)
        return -1;

    return 0;
}


static int
qemuNbdkitProcessBuildCommandCurl(qemuNbdkitProcess *proc,
                                  virCommand *cmd)
{
    g_autoptr(virURI) uri = qemuBlockStorageSourceGetURI(proc->source);
    g_autofree char *uristring = virURIFormat(uri);

    /* nbdkit plugin name */
    virCommandAddArg(cmd, "curl");
    if (proc->source->protocol == VIR_STORAGE_NET_PROTOCOL_HTTP) {
        /* allow http to be upgraded to https via e.g. redirect */
        virCommandAddArgPair(cmd, "protocols", "http,https");
    } else {
        virCommandAddArgPair(cmd, "protocols",
                             virStorageNetProtocolTypeToString(proc->source->protocol));
    }
    virCommandAddArgPair(cmd, "url", uristring);

    if (proc->source->auth && qemuNbdkitProcessBuildCommandAuth(proc->source->auth, cmd) < 0)
        return -1;

    /* Create a pipe to send the cookies to the nbdkit process. */
    if (proc->source->ncookies) {
        g_autofree char *cookies = qemuBlockStorageSourceGetCookieString(proc->source);

        if (qemuNbdkitCommandPassDataByPipe(cmd, "cookie",
                                            (unsigned char**)&cookies,
                                            strlen(cookies)) < 0)
            return -1;
    }

    if (proc->source->sslverify == VIR_TRISTATE_BOOL_NO) {
        virCommandAddArgPair(cmd, "sslverify", "false");
    }

    if (proc->source->timeout > 0) {
        g_autofree char *timeout = g_strdup_printf("%llu", proc->source->timeout);
        virCommandAddArgPair(cmd, "timeout", timeout);
    }

    return 0;
}


static int
qemuNbdkitProcessBuildCommandSSH(qemuNbdkitProcess *proc,
                                 virCommand *cmd)
{
    virStorageNetHostDef *host = &proc->source->hosts[0];
    g_autofree char *portstr = g_strdup_printf("%u", host->port);

    /* nbdkit plugin name */
    virCommandAddArg(cmd, "ssh");

    virCommandAddArgPair(cmd, "host", host->name);
    virCommandAddArgPair(cmd, "port", portstr);
    virCommandAddArgPair(cmd, "path", proc->source->path);

    if (proc->source->auth) {
        if (qemuNbdkitProcessBuildCommandAuth(proc->source->auth, cmd) < 0)
            return -1;
    } else {
        if (proc->source->ssh_keyfile)
            virCommandAddArgPair(cmd, "identity", proc->source->ssh_keyfile);

        if (proc->source->ssh_user)
            virCommandAddArgPair(cmd, "user", proc->source->ssh_user);
    }

    if (proc->source->ssh_agent)
        virCommandAddEnvPair(cmd, "SSH_AUTH_SOCK", proc->source->ssh_agent);

    if (proc->source->ssh_host_key_check_disabled)
        virCommandAddArgPair(cmd, "verify-remote-host", "false");

    if (proc->source->ssh_known_hosts_file)
        virCommandAddArgPair(cmd, "known-hosts", proc->source->ssh_known_hosts_file);

    return 0;
}


virCommand *
qemuNbdkitProcessBuildCommand(qemuNbdkitProcess *proc)
{
    g_autoptr(virCommand) cmd = virCommandNewArgList(proc->caps->path,
                                                     "--unix",
                                                     proc->socketfile,
                                                     "--foreground",
                                                     NULL);

    if (proc->source->readonly)
        virCommandAddArg(cmd, "--readonly");

    if (qemuNbdkitCapsGet(proc->caps, QEMU_NBDKIT_CAPS_FILTER_READAHEAD) &&
        proc->source->readahead > 0)
        virCommandAddArgPair(cmd, "--filter", "readahead");

    switch (proc->source->protocol) {
        case VIR_STORAGE_NET_PROTOCOL_HTTP:
        case VIR_STORAGE_NET_PROTOCOL_HTTPS:
        case VIR_STORAGE_NET_PROTOCOL_FTP:
        case VIR_STORAGE_NET_PROTOCOL_FTPS:
        case VIR_STORAGE_NET_PROTOCOL_TFTP:
            if (qemuNbdkitProcessBuildCommandCurl(proc, cmd) < 0)
                return NULL;
            break;
        case VIR_STORAGE_NET_PROTOCOL_SSH:
            if (qemuNbdkitProcessBuildCommandSSH(proc, cmd) < 0)
                return NULL;
            break;

        case VIR_STORAGE_NET_PROTOCOL_NONE:
        case VIR_STORAGE_NET_PROTOCOL_NBD:
        case VIR_STORAGE_NET_PROTOCOL_RBD:
        case VIR_STORAGE_NET_PROTOCOL_SHEEPDOG:
        case VIR_STORAGE_NET_PROTOCOL_GLUSTER:
        case VIR_STORAGE_NET_PROTOCOL_ISCSI:
        case VIR_STORAGE_NET_PROTOCOL_VXHS:
        case VIR_STORAGE_NET_PROTOCOL_NFS:
        case VIR_STORAGE_NET_PROTOCOL_LAST:
            virReportError(VIR_ERR_NO_SUPPORT,
                           _("protocol '%1$s' is not supported by nbdkit"),
                           virStorageNetProtocolTypeToString(proc->source->protocol));
            return NULL;
    }

    virCommandDaemonize(cmd);

    return g_steal_pointer(&cmd);
}


void
qemuNbdkitProcessFree(qemuNbdkitProcess *proc)
{
    qemuNbdkitProcessStopMonitor(proc);

    g_clear_pointer(&proc->pidfile, g_free);
    g_clear_pointer(&proc->socketfile, g_free);
    g_clear_object(&proc->caps);
    g_free(proc);
}


int
qemuNbdkitProcessSetupCgroup(qemuNbdkitProcess *proc,
                             virCgroup *cgroup)
{
    return virCgroupAddProcess(cgroup, proc->pid);
}


int
qemuNbdkitProcessStart(qemuNbdkitProcess *proc,
                       virDomainObj *vm,
                       virQEMUDriver *driver)
{
    g_autoptr(virCommand) cmd = NULL;
    g_autoptr(virQEMUDriverConfig) cfg = virQEMUDriverGetConfig(driver);
    int rc;
    int exitstatus = 0;
    g_autofree char *errbuf = NULL;
    virTimeBackOffVar timebackoff;
    g_autoptr(virURI) uri = NULL;
    g_autofree char *uristring = NULL;
    g_autofree char *basename = g_strdup_printf("%s-nbdkit-%i", vm->def->name, proc->source->id);
    int logfd = -1;
    g_autoptr(domainLogContext) logContext = NULL;
#if WITH_NBDKIT
    struct nbd_handle *nbd = NULL;
#endif

    /* don't try to start nbdkit again if we've already started it */
    if (proc->pid > 0) {
        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
                       _("Attempting to start nbdkit twice"));
        return -1;
    }

    if (!(cmd = qemuNbdkitProcessBuildCommand(proc)))
        return -1;

    if (!(logContext = domainLogContextNew(cfg->stdioLogD, cfg->logDir,
                                           QEMU_DRIVER_NAME,
                                           vm, driver->privileged,
                                           basename))) {
        virLastErrorPrefixMessage("%s", _("can't connect to virtlogd"));
        return -1;
    }

    logfd = domainLogContextGetWriteFD(logContext);

    VIR_DEBUG("starting nbdkit process for %s", qemuBlockStorageSourceGetStorageNodename(proc->source));
    virCommandSetErrorFD(cmd, &logfd);
    virCommandSetOutputFD(cmd, &logfd);
    virCommandSetPidFile(cmd, proc->pidfile);

    if (qemuExtDeviceLogCommand(driver, vm, cmd, "nbdkit") < 0)
        goto error;

    if (proc->source->ssh_keyfile &&
        qemuSecurityDomainSetPathLabel(driver, vm, proc->source->ssh_keyfile, false) < 0)
        goto error;

    if (proc->source->ssh_agent &&
        qemuSecurityDomainSetPathLabel(driver, vm, proc->source->ssh_agent, false) < 0)
        goto error;

    if (proc->source->ssh_known_hosts_file &&
        qemuSecurityDomainSetPathLabel(driver, vm, proc->source->ssh_known_hosts_file, false) < 0)
        goto error;

    if (qemuSecurityCommandRun(driver, vm, cmd, proc->user, proc->group, true, &exitstatus) < 0)
        goto error;

    if (exitstatus != 0) {
        virReportError(VIR_ERR_INTERNAL_ERROR,
                       _("Could not start 'nbdkit'. exitstatus: %1$d"), exitstatus);
        goto error;
    }

    if ((rc = virPidFileReadPath(proc->pidfile, &proc->pid)) < 0) {
        virReportSystemError(-rc,
                             _("Failed to read pidfile %1$s"),
                             proc->pidfile);
        goto error;
    }

    if (virTimeBackOffStart(&timebackoff, 1, 1000) < 0)
        goto error;

    while (virTimeBackOffWait(&timebackoff)) {
        if (virFileExists(proc->socketfile)) {
#if WITH_NBDKIT
            /* if the disk source was misconfigured, nbdkit will not produce an error
             * until somebody connects to the socket and tries to access the nbd
             * export. This results in poor user experience because the only error we
             * would get from qemu is something like "Requested export not available".
             * So let's try to access it ourselves so that we can error out early and
             * provide a useful message to the user.
             */
            nbd = nbd_create();
            if (nbd_connect_unix(nbd, proc->socketfile) < 0) {
                VIR_WARN("nbd_connect_unix failed: %s", nbd_get_error());
                nbd_close(nbd);
                goto errorlog;
            }
            nbd_close(nbd);

#endif
            if (qemuNbdkitProcessStartMonitor(proc, vm) < 0)
                goto error;
            return 0;
        }

        if (virProcessKill(proc->pid, 0) == 0)
            continue;

        VIR_WARN("nbdkit died unexpectedly");
        goto errorlog;
    }

    VIR_WARN("nbdkit socket did not show up");

 errorlog:
    if ((uri = qemuBlockStorageSourceGetURI(proc->source)))
        uristring = virURIFormat(uri);

    if (domainLogContextReadFiltered(logContext, &errbuf, 1024) < 0)
        VIR_WARN("Unable to read from nbdkit log");

    virReportError(VIR_ERR_OPERATION_FAILED,
                   _("Failed to connect to nbdkit for '%1$s': %2$s"),
                   NULLSTR(uristring), NULLSTR(errbuf));

 error:
    qemuNbdkitProcessStop(proc, vm);
    return -1;
}


int
qemuNbdkitProcessStop(qemuNbdkitProcess *proc,
                      virDomainObj *vm)
{
    qemuDomainObjPrivate *vmpriv = vm->privateData;
    virQEMUDriver *driver = vmpriv->driver;

    qemuNbdkitProcessStopMonitor(proc);

    if (proc->source->ssh_known_hosts_file)
        qemuSecurityDomainRestorePathLabel(driver, vm, proc->source->ssh_known_hosts_file);

    if (proc->source->ssh_keyfile)
        qemuSecurityDomainRestorePathLabel(driver, vm, proc->source->ssh_keyfile);

    if (proc->source->ssh_agent)
        qemuSecurityDomainRestorePathLabel(driver, vm, proc->source->ssh_agent);

    if (proc->pid < 0)
        return 0;

    VIR_DEBUG("Stopping nbdkit process %i", proc->pid);
    virProcessKill(proc->pid, SIGTERM);

    unlink(proc->pidfile);
    unlink(proc->socketfile);
    proc->pid = -1;

    return 0;
}
