
/*
 * esx_storage_backend_iscsi.c: ESX storage backend for iSCSI handling
 *
 * Copyright (C) 2012 Ata E Husain Bohra <ata.husain@hotmail.com>
 *
 * 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 <string.h>
#include <stdio.h>
#include <unistd.h>

#include "internal.h"
#include "md5.h"
#include "viralloc.h"
#include "virlog.h"
#include "viruuid.h"
#include "storage_conf.h"
#include "virstoragefile.h"
#include "esx_storage_backend_iscsi.h"
#include "esx_private.h"
#include "esx_vi.h"
#include "esx_vi_methods.h"
#include "esx_util.h"
#include "virstring.h"

#define VIR_FROM_THIS VIR_FROM_ESX

/*
 * The UUID of a storage pool is the MD5 sum of it's mount path. Therefore,
 * verify that UUID and MD5 sum match in size, because we rely on that.
 */
verify(MD5_DIGEST_SIZE == VIR_UUID_BUFLEN);



static int
esxConnectNumOfStoragePools(virConnectPtr conn)
{
    bool success = false;
    int count = 0;
    esxPrivate *priv = conn->storagePrivateData;
    esxVI_HostInternetScsiHba *hostInternetScsiHba = NULL;
    esxVI_HostInternetScsiHbaStaticTarget *target;

    if (esxVI_LookupHostInternetScsiHba(priv->primary,
                                        &hostInternetScsiHba) < 0) {
        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
                       _("Unable to obtain iSCSI adapter"));
        goto cleanup;
    }

    /* FIXME: code looks for software iSCSI adapter only */
    if (hostInternetScsiHba == NULL) {
        /* iSCSI adapter may not be enabled for this host */
        return 0;
    }

    /*
     * ESX has two kind of targets:
     * 1. staticIscsiTargets
     * 2. dynamicIscsiTargets
     * For each dynamic target if its reachable a static target is added.
     * return iSCSI names for all static targets to avoid duplicate names.
     */
    for (target = hostInternetScsiHba->configuredStaticTarget;
         target != NULL; target = target->_next) {
        ++count;
    }

    success = true;

  cleanup:
    esxVI_HostInternetScsiHba_Free(&hostInternetScsiHba);

    return success ? count : -1;
}



static int
esxConnectListStoragePools(virConnectPtr conn, char **const names,
                           const int maxnames)
{
    bool success = false;
    int count = 0;
    esxPrivate *priv = conn->storagePrivateData;
    esxVI_HostInternetScsiHba *hostInternetScsiHba = NULL;
    esxVI_HostInternetScsiHbaStaticTarget *target;
    size_t i;

    if (maxnames == 0) {
        return 0;
    }

    if (esxVI_LookupHostInternetScsiHba(priv->primary,
                                        &hostInternetScsiHba) < 0) {
        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
                       _("Unable to obtain iSCSI adapter"));
        goto cleanup;
    }

    /* FIXME: code looks for software iSCSI adapter only */
    if (hostInternetScsiHba == NULL) {
        /* iSCSI adapter may not be enabled for this host */
        return 0;
    }

    /*
     * ESX has two kind of targets:
     * 1. staticIscsiTargets
     * 2. dynamicIscsiTargets
     * For each dynamic target if its reachable a static target is added.
     * return iSCSI names for all static targets to avoid duplicate names.
     */
    for (target = hostInternetScsiHba->configuredStaticTarget;
         target != NULL && count < maxnames; target = target->_next) {
        if (VIR_STRDUP(names[count], target->iScsiName) < 0)
            goto cleanup;

        ++count;
    }

    success = true;

  cleanup:
    if (! success) {
        for (i = 0; i < count; ++i) {
            VIR_FREE(names[i]);
        }
    }

    esxVI_HostInternetScsiHba_Free(&hostInternetScsiHba);

    return success ? count : -1;
}



static virStoragePoolPtr
esxStoragePoolLookupByName(virConnectPtr conn,
                           const char *name)
{
    esxPrivate *priv = conn->storagePrivateData;
    esxVI_HostInternetScsiHbaStaticTarget *target = NULL;
    /* MD5_DIGEST_SIZE = VIR_UUID_BUFLEN = 16 */
    unsigned char md5[MD5_DIGEST_SIZE];
    virStoragePoolPtr pool = NULL;

    /*
     * Lookup routine are used by the base driver to determine
     * appropriate backend driver, lookup targetName as optional
     * parameter
     */
    if (esxVI_LookupHostInternetScsiHbaStaticTargetByName
          (priv->primary, name, &target, esxVI_Occurrence_OptionalItem) < 0) {
        goto cleanup;
    }

    if (target == NULL) {
        /* pool not found, error handling done by the base driver */
        goto cleanup;
    }

    /*
     * HostInternetScsiHbaStaticTarget does not provide a uuid field,
     * but iScsiName (or widely known as IQN) is unique across the multiple
     * hosts, using it to compute key
     */
    md5_buffer(target->iScsiName, strlen(target->iScsiName), md5);

    pool = virGetStoragePool(conn, name, md5, &esxStorageBackendISCSI, NULL);

  cleanup:
    esxVI_HostInternetScsiHbaStaticTarget_Free(&target);

    return pool;
}



static virStoragePoolPtr
esxStoragePoolLookupByUUID(virConnectPtr conn,
                           const unsigned char *uuid)
{
    virStoragePoolPtr pool = NULL;
    esxPrivate *priv = conn->storagePrivateData;
    esxVI_HostInternetScsiHba *hostInternetScsiHba = NULL;
    esxVI_HostInternetScsiHbaStaticTarget *target;
    /* MD5_DIGEST_SIZE = VIR_UUID_BUFLEN = 16 */
    unsigned char md5[MD5_DIGEST_SIZE];

    if (esxVI_LookupHostInternetScsiHba(priv->primary,
                                        &hostInternetScsiHba) < 0) {
        virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
                       _("Unable to obtain iSCSI adapter"));
        goto cleanup;
    }

    /* FIXME: code just looks for software iSCSI adapter */
    if (hostInternetScsiHba == NULL) {
        /* iSCSI adapter may not be enabled for this host */
        return NULL;
    }

    for (target = hostInternetScsiHba->configuredStaticTarget;
         target != NULL; target = target->_next) {
        md5_buffer(target->iScsiName, strlen(target->iScsiName), md5);

        if (memcmp(uuid, md5, VIR_UUID_STRING_BUFLEN) == 0) {
            break;
        }
    }

    if (target == NULL) {
        /* pool not found, error handling done by the base driver */
        goto cleanup;
    }

    pool = virGetStoragePool(conn, target->iScsiName, md5,
                             &esxStorageBackendISCSI, NULL);

  cleanup:
    esxVI_HostInternetScsiHba_Free(&hostInternetScsiHba);

    return pool;
}



static int
esxStoragePoolRefresh(virStoragePoolPtr pool,
                      unsigned int flags)
{
    int result = -1;
    esxPrivate *priv = pool->conn->storagePrivateData;
    esxVI_HostInternetScsiHba *hostInternetScsiHba = NULL;

    virCheckFlags(0, -1);

    if (esxVI_LookupHostInternetScsiHba(priv->primary,
                                        &hostInternetScsiHba) < 0) {
        goto cleanup;
    }

    /*
     * ESX does not allow rescan on a particular target,
     * rescan all the static targets
     */
    if (esxVI_RescanHba(priv->primary,
                        priv->primary->hostSystem->configManager->storageSystem,
                        hostInternetScsiHba->device) < 0) {
        goto cleanup;
    }

    result = 0;

  cleanup:
    esxVI_HostInternetScsiHba_Free(&hostInternetScsiHba);

    return result;
}



static int
esxStoragePoolGetInfo(virStoragePoolPtr pool ATTRIBUTE_UNUSED,
                      virStoragePoolInfoPtr info)
{
    /* These fields are not valid for iSCSI pool */
    info->allocation = info->capacity = info->available = 0;
    info->state = VIR_STORAGE_POOL_RUNNING;

    return 0;
}



static char *
esxStoragePoolGetXMLDesc(virStoragePoolPtr pool, unsigned int flags)
{
    char *xml = NULL;
    esxPrivate *priv = pool->conn->storagePrivateData;
    esxVI_HostInternetScsiHba *hostInternetScsiHba = NULL;
    esxVI_HostInternetScsiHbaStaticTarget *target;
    virStoragePoolDef def;

    virCheckFlags(0, NULL);

    memset(&def, 0, sizeof(def));

    if (esxVI_LookupHostInternetScsiHba(priv->primary, &hostInternetScsiHba)) {
        goto cleanup;
    }

    for (target = hostInternetScsiHba->configuredStaticTarget;
         target != NULL; target = target->_next) {
        if (STREQ(target->iScsiName, pool->name)) {
            break;
        }
    }

    if (target == NULL) {
        /* pool not found */
        virReportError(VIR_ERR_INTERNAL_ERROR,
                       _("Could not find storage pool with name '%s'"),
                       pool->name);
        goto cleanup;
    }

    def.name = pool->name;

    memcpy(def.uuid, pool->uuid, VIR_UUID_BUFLEN);

    def.type = VIR_STORAGE_POOL_ISCSI;

    def.source.initiator.iqn = target->iScsiName;

    def.source.nhost = 1;

    if (VIR_ALLOC_N(def.source.hosts, def.source.nhost) < 0)
        goto cleanup;

    def.source.hosts[0].name = target->address;

    if (target->port != NULL) {
        def.source.hosts[0].port = target->port->value;
    }

    /* TODO: add CHAP authentication params */
    xml = virStoragePoolDefFormat(&def);

  cleanup:
    VIR_FREE(def.source.hosts);
    esxVI_HostInternetScsiHba_Free(&hostInternetScsiHba);

    return xml;
}



static int
esxStoragePoolNumOfVolumes(virStoragePoolPtr pool)
{
    int count = 0;
    esxPrivate *priv = pool->conn->storagePrivateData;
    esxVI_HostScsiTopologyLun *hostScsiTopologyLunList = NULL;
    esxVI_HostScsiTopologyLun *hostScsiTopologyLun;

    if (esxVI_LookupHostScsiTopologyLunListByTargetName
          (priv->primary, pool->name, &hostScsiTopologyLunList) < 0) {
        return -1;
    }

    for (hostScsiTopologyLun = hostScsiTopologyLunList;
         hostScsiTopologyLun != NULL;
         hostScsiTopologyLun = hostScsiTopologyLun->_next) {
        ++count;
    }

    esxVI_HostScsiTopologyLun_Free(&hostScsiTopologyLunList);

    return count;
}



static int
esxStoragePoolListVolumes(virStoragePoolPtr pool, char **const names,
                          int maxnames)
{
    bool success = false;
    int count = 0;
    esxPrivate *priv = pool->conn->storagePrivateData;
    esxVI_HostScsiTopologyLun *hostScsiTopologyLunList = NULL;
    esxVI_HostScsiTopologyLun *hostScsiTopologyLun;
    esxVI_ScsiLun *scsiLunList = NULL;
    esxVI_ScsiLun *scsiLun = NULL;
    size_t i;

    if (esxVI_LookupHostScsiTopologyLunListByTargetName
          (priv->primary, pool->name, &hostScsiTopologyLunList) < 0) {
        goto cleanup;
    }

    if (hostScsiTopologyLunList == NULL) {
        /* iSCSI adapter may not be enabled on ESX host */
        return 0;
    }

    if (esxVI_LookupScsiLunList(priv->primary, &scsiLunList) < 0) {
        goto cleanup;
    }

    for (scsiLun = scsiLunList; scsiLun != NULL && count < maxnames;
         scsiLun = scsiLun->_next) {
        for (hostScsiTopologyLun = hostScsiTopologyLunList;
             hostScsiTopologyLun != NULL && count < maxnames;
             hostScsiTopologyLun = hostScsiTopologyLun->_next) {
            if (STREQ(hostScsiTopologyLun->scsiLun, scsiLun->key)) {
                if (VIR_STRDUP(names[count], scsiLun->deviceName) < 0)
                    goto cleanup;

                ++count;
            }
        }
    }

    success = true;

  cleanup:
    if (! success) {
        for (i = 0; i < count; ++i) {
            VIR_FREE(names[i]);
        }

        count = -1;
    }

    esxVI_HostScsiTopologyLun_Free(&hostScsiTopologyLunList);
    esxVI_ScsiLun_Free(&scsiLunList);

    return count;
}



static virStorageVolPtr
esxStorageVolLookupByName(virStoragePoolPtr pool,
                          const char *name)
{
    virStorageVolPtr volume = NULL;
    esxPrivate *priv = pool->conn->storagePrivateData;
    esxVI_ScsiLun *scsiLunList = NULL;
    esxVI_ScsiLun *scsiLun;
    /* MD5_DIGEST_SIZE = VIR_UUID_BUFLEN = 16 */
    unsigned char md5[MD5_DIGEST_SIZE];
    char uuid_string[VIR_UUID_STRING_BUFLEN] = "";

    if (esxVI_LookupScsiLunList(priv->primary, &scsiLunList) < 0) {
        goto cleanup;
    }

    for (scsiLun = scsiLunList; scsiLun != NULL;
         scsiLun = scsiLun->_next) {
        if (STREQ(scsiLun->deviceName, name)) {
            /*
             * ScsiLun provides an UUID field that is unique accross
             * multiple servers. But this field length is ~55 characters
             * compute MD5 hash to transform it to an acceptable
             * libvirt format
             */
            md5_buffer(scsiLun->uuid, strlen(scsiLun->uuid), md5);
            virUUIDFormat(md5, uuid_string);

            /*
             * ScsiLun provides displayName and canonicalName but both are
             * optional and its observed that they can be NULL, using
             * deviceName to create volume.
             */
            volume = virGetStorageVol(pool->conn, pool->name, name, uuid_string,
                                      &esxStorageBackendISCSI, NULL);
            break;
        }
    }

  cleanup:
    esxVI_ScsiLun_Free(&scsiLunList);

    return volume;
}



static virStorageVolPtr
esxStorageVolLookupByPath(virConnectPtr conn, const char *path)
{
    virStorageVolPtr volume = NULL;
    esxPrivate *priv = conn->storagePrivateData;
    esxVI_ScsiLun *scsiLunList = NULL;
    esxVI_ScsiLun *scsiLun;
    esxVI_HostScsiDisk *hostScsiDisk = NULL;
    char *poolName = NULL;
    /* MD5_DIGEST_SIZE = VIR_UUID_BUFLEN = 16 */
    unsigned char md5[MD5_DIGEST_SIZE];
    char uuid_string[VIR_UUID_STRING_BUFLEN] = "";

    if (esxVI_LookupScsiLunList(priv->primary, &scsiLunList) < 0) {
        goto cleanup;
    }

    for (scsiLun = scsiLunList; scsiLun != NULL; scsiLun = scsiLun->_next) {
        hostScsiDisk = esxVI_HostScsiDisk_DynamicCast(scsiLun);

        if (hostScsiDisk != NULL && STREQ(hostScsiDisk->devicePath, path)) {
            /* Found matching device */
            VIR_FREE(poolName);

            if (esxVI_LookupStoragePoolNameByScsiLunKey(priv->primary,
                                                        hostScsiDisk->key,
                                                        &poolName) < 0) {
                goto cleanup;
            }

            md5_buffer(scsiLun->uuid, strlen(scsiLun->uuid), md5);
            virUUIDFormat(md5, uuid_string);

            volume = virGetStorageVol(conn, poolName, path, uuid_string,
                                      &esxStorageBackendISCSI, NULL);
            break;
        }
    }

  cleanup:
    esxVI_ScsiLun_Free(&scsiLunList);
    VIR_FREE(poolName);

    return volume;
}



static virStorageVolPtr
esxStorageVolLookupByKey(virConnectPtr conn, const char *key)
{
    virStorageVolPtr volume = NULL;
    esxPrivate *priv = conn->storagePrivateData;
    char *poolName = NULL;
    esxVI_ScsiLun *scsiLunList = NULL;
    esxVI_ScsiLun *scsiLun;
    /* MD5_DIGEST_SIZE = VIR_UUID_BUFLEN = 16 */
    unsigned char md5[MD5_DIGEST_SIZE];
    char uuid_string[VIR_UUID_STRING_BUFLEN] = "";

    /* key may be LUN device path */
    if (STRPREFIX(key, "/")) {
        return esxStorageVolLookupByPath(conn, key);
    }

    if (esxVI_LookupScsiLunList(priv->primary, &scsiLunList) < 0) {
        goto cleanup;
    }

    for (scsiLun = scsiLunList; scsiLun != NULL;
         scsiLun = scsiLun->_next) {
        memset(uuid_string, '\0', sizeof(uuid_string));
        memset(md5, '\0', sizeof(md5));

        md5_buffer(scsiLun->uuid, strlen(scsiLun->uuid), md5);
        virUUIDFormat(md5, uuid_string);

        if (STREQ(key, uuid_string)) {
            /* Found matching UUID */
            VIR_FREE(poolName);

            if (esxVI_LookupStoragePoolNameByScsiLunKey(priv->primary,
                                                        scsiLun->key,
                                                        &poolName) < 0) {
                goto cleanup;
            }

            volume = virGetStorageVol(conn, poolName, scsiLun->deviceName,
                                      uuid_string, &esxStorageBackendISCSI,
                                      NULL);
            break;
        }
    }

  cleanup:
    esxVI_ScsiLun_Free(&scsiLunList);
    VIR_FREE(poolName);

    return volume;
}



static virStorageVolPtr
esxStorageVolCreateXML(virStoragePoolPtr pool ATTRIBUTE_UNUSED,
                       const char *xmldesc ATTRIBUTE_UNUSED,
                       unsigned int flags)
{
    virCheckFlags(0, NULL);

    virReportError(VIR_ERR_NO_SUPPORT, "%s",
                   _("iSCSI storage pool does not support volume creation"));

    return NULL;
}



static virStorageVolPtr
esxStorageVolCreateXMLFrom(virStoragePoolPtr pool ATTRIBUTE_UNUSED,
                           const char *xmldesc ATTRIBUTE_UNUSED,
                           virStorageVolPtr sourceVolume ATTRIBUTE_UNUSED,
                           unsigned int flags)
{
    virCheckFlags(0, NULL);

    virReportError(VIR_ERR_NO_SUPPORT, "%s",
                   _("iSCSI storage pool does not support volume creation"));

    return NULL;
}



static char *
esxStorageVolGetXMLDesc(virStorageVolPtr volume,
                        unsigned int flags)
{
    char *xml = NULL;
    esxPrivate *priv = volume->conn->storagePrivateData;
    virStoragePoolDef pool;
    esxVI_ScsiLun *scsiLunList = NULL;
    esxVI_ScsiLun *scsiLun;
    esxVI_HostScsiDisk *hostScsiDisk = NULL;
    virStorageVolDef def;
    /* MD5_DIGEST_SIZE = VIR_UUID_BUFLEN = 16 */
    unsigned char md5[MD5_DIGEST_SIZE];
    char uuid_string[VIR_UUID_STRING_BUFLEN] = "";

    virCheckFlags(0, NULL);

    memset(&pool, 0, sizeof(pool));
    memset(&def, 0, sizeof(def));

    if (esxVI_LookupScsiLunList(priv->primary, &scsiLunList) < 0) {
        goto cleanup;
    }

    for (scsiLun = scsiLunList; scsiLun != NULL;
         scsiLun = scsiLun->_next) {
        hostScsiDisk = esxVI_HostScsiDisk_DynamicCast(scsiLun);

        if (hostScsiDisk != NULL &&
            STREQ(hostScsiDisk->deviceName, volume->name)) {
            break;
        }
    }

    if (hostScsiDisk == NULL) {
        virReportError(VIR_ERR_INTERNAL_ERROR,
                       _("Could find volume with name: %s"), volume->name);
        goto cleanup;
    }

    pool.type = VIR_STORAGE_POOL_ISCSI;

    def.name = volume->name;

    md5_buffer(scsiLun->uuid, strlen(hostScsiDisk->uuid), md5);

    virUUIDFormat(md5, uuid_string);

    if (VIR_STRDUP(def.key, uuid_string) < 0) {
        goto cleanup;
    }

    /* iSCSI LUN exposes a block device */
    def.type = VIR_STORAGE_VOL_BLOCK;

    def.target.path = hostScsiDisk->devicePath;

    def.capacity = hostScsiDisk->capacity->block->value *
                   hostScsiDisk->capacity->blockSize->value;

    def.allocation = def.capacity;

    /* iSCSI LUN(s) hosting a datastore will be auto-mounted by ESX host */
    def.target.format = VIR_STORAGE_FILE_RAW;

    xml = virStorageVolDefFormat(&pool, &def);

  cleanup:
    esxVI_ScsiLun_Free(&scsiLunList);
    VIR_FREE(def.key);

    return xml;
}



static int
esxStorageVolDelete(virStorageVolPtr volume ATTRIBUTE_UNUSED,
                    unsigned int flags)
{
    virCheckFlags(0, -1);

    virReportError(VIR_ERR_NO_SUPPORT, "%s",
                   _("iSCSI storage pool does not support volume deletion"));

    return -1;
}



static int
esxStorageVolWipe(virStorageVolPtr volume ATTRIBUTE_UNUSED,
                  unsigned int flags)
{
    virCheckFlags(0, -1);


    virReportError(VIR_ERR_NO_SUPPORT, "%s",
                   _("iSCSI storage pool does not support volume wiping"));

    return -1;
}



static char *
esxStorageVolGetPath(virStorageVolPtr volume)
{
    char *path;

    ignore_value(VIR_STRDUP(path, volume->name));
    return path;
}



virStorageDriver esxStorageBackendISCSI = {
    .connectNumOfStoragePools = esxConnectNumOfStoragePools, /* 1.0.1 */
    .connectListStoragePools = esxConnectListStoragePools, /* 1.0.1 */
    .storagePoolLookupByName = esxStoragePoolLookupByName, /* 1.0.1 */
    .storagePoolLookupByUUID = esxStoragePoolLookupByUUID, /* 1.0.1 */
    .storagePoolRefresh = esxStoragePoolRefresh, /* 1.0.1 */
    .storagePoolGetInfo = esxStoragePoolGetInfo, /* 1.0.1 */
    .storagePoolGetXMLDesc = esxStoragePoolGetXMLDesc, /* 1.0.1 */
    .storagePoolNumOfVolumes = esxStoragePoolNumOfVolumes, /* 1.0.1 */
    .storagePoolListVolumes = esxStoragePoolListVolumes, /* 1.0.1 */
    .storageVolLookupByName = esxStorageVolLookupByName, /* 1.0.1 */
    .storageVolLookupByPath = esxStorageVolLookupByPath, /* 1.0.1 */
    .storageVolLookupByKey = esxStorageVolLookupByKey, /* 1.0.1 */
    .storageVolCreateXML = esxStorageVolCreateXML, /* 1.0.1 */
    .storageVolCreateXMLFrom = esxStorageVolCreateXMLFrom, /* 1.0.1 */
    .storageVolGetXMLDesc = esxStorageVolGetXMLDesc, /* 1.0.1 */
    .storageVolDelete = esxStorageVolDelete, /* 1.0.1 */
    .storageVolWipe = esxStorageVolWipe, /* 1.0.1 */
    .storageVolGetPath = esxStorageVolGetPath, /* 1.0.1 */
};
