// SPDX-License-Identifier: LGPL-3.0-or-later
/*

   Copyright 2017 Skytechnology sp. z o.o.
   Copyright 2023 Leil Storage OÜ

   This program 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 3 of the License, or (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   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, write to the Free Software
   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
   02110-1301 USA
*/


#include "fsal_pnfs.h"
#include "fsal_types.h"
#include "FSAL/fsal_init.h"
#include "FSAL/fsal_commonlib.h"
#include "fsal_api.h"
#include "pnfs_utils.h"

#include "nfs-ganesha/context_wrap.h"
#include "nfs-ganesha/saunafs_fsal_types.h"
#include "nfs-ganesha/saunafs_internal.h"

/* FSAL name determines name of shared library: libfsalsaunafs.so */
static const char *const module = "SaunaFS";

static const int millisecondsInOneSecond = 1000;

/**
 * my module private storage
 */

struct SaunaFSModule SaunaFS = {
    .fsal = {.fs_info = {
                 .maxfilesize = UINT64_MAX,
                 .maxlink = _POSIX_LINK_MAX,
                 .maxnamelen = SFS_NAME_MAX,
                 .maxpathlen = MAXPATHLEN,
                 .no_trunc = true,
                 .chown_restricted = false,
                 .case_insensitive = false,
                 .case_preserving = true,
                 .link_support = true,
                 .symlink_support = true,
                 .lock_support = true,
                 .lock_support_async_block = false,
                 .named_attr = true,
                 .unique_handles = true,
#ifdef ENABLE_NFS_ACL_SUPPORT
                 .acl_support = (unsigned)FSAL_ACLSUPPORT_ALLOW | (unsigned)FSAL_ACLSUPPORT_DENY,
#else
                 .acl_support = 0,
#endif
                 .cansettime = true,
                 .homogenous = true,  // pragma: codespell-ignore
                 .supported_attrs = SAUNAFS_SUPPORTED_ATTRS,
                 .maxread = (uint64_t)FSAL_MAXIOSIZE,
                 .maxwrite = (uint64_t)FSAL_MAXIOSIZE,
                 .umask = 0,
                 .auth_exportpath_xdev = false,
                 .pnfs_mds = true,
                 .pnfs_ds = true,
                 .fsal_trace = false,
                 .fsal_grace = false,
                 .link_supports_permission_checks = true,
                 .xattr_support = true,
             }}};

static struct config_item export_params[] = {
    CONF_ITEM_MODE("umask", 0, fsal_staticfsinfo_t, umask),
    CONF_ITEM_BOOL("link_support", true, fsal_staticfsinfo_t, link_support),
    CONF_ITEM_BOOL("symlink_support", true, fsal_staticfsinfo_t, symlink_support),
    CONF_ITEM_BOOL("cansettime", true, fsal_staticfsinfo_t, cansettime),
    CONF_ITEM_BOOL("auth_xdev_export", false, fsal_staticfsinfo_t, auth_exportpath_xdev),
    CONF_ITEM_UI64("maxread", 512, (uint64_t)FSAL_MAXIOSIZE, (uint64_t)FSAL_MAXIOSIZE,
                   fsal_staticfsinfo_t, maxread),
    CONF_ITEM_UI64("maxwrite", 512, (uint64_t)FSAL_MAXIOSIZE, (uint64_t)FSAL_MAXIOSIZE,
                   fsal_staticfsinfo_t, maxwrite),
    CONF_ITEM_BOOL("PNFS_MDS", false, fsal_staticfsinfo_t, pnfs_mds),
    CONF_ITEM_BOOL("PNFS_DS", false, fsal_staticfsinfo_t, pnfs_ds),
    CONF_ITEM_BOOL("fsal_trace", true, fsal_staticfsinfo_t, fsal_trace),
    CONF_ITEM_BOOL("fsal_grace", false, fsal_staticfsinfo_t, fsal_grace),
    CONFIG_EOL};

static struct config_block export_param = {
	.dbus_interface_name = "org.ganesha.nfsd.config.fsal.saunafs",
	.blk_desc.name = "SaunaFS",
	.blk_desc.type = CONFIG_BLOCK,
	.blk_desc.flags = CONFIG_UNIQUE,  /* too risky to have more */
	.blk_desc.u.blk.init = noop_conf_init,
	.blk_desc.u.blk.params = export_params,
	.blk_desc.u.blk.commit = noop_conf_commit
};

static struct config_item fsal_export_params[] = {
    CONF_ITEM_NOOP("name"),
    CONF_MAND_STR("hostname", 1, MAXPATHLEN, NULL, SaunaFSExport, parameters.host),
    CONF_ITEM_STR("port", 1, MAXPATHLEN, "9421", SaunaFSExport, parameters.port),
    CONF_ITEM_STR("mountpoint", 1, MAXPATHLEN, "nfs-ganesha", SaunaFSExport, parameters.mountpoint),
    CONF_ITEM_STR("subfolder", 1, MAXPATHLEN, "/", SaunaFSExport, parameters.subfolder),
    CONF_ITEM_BOOL("delayed_init", false, SaunaFSExport, parameters.delayed_init),
    CONF_ITEM_UI32("io_retries", 0, 1024, 30, SaunaFSExport, parameters.io_retries),
    CONF_ITEM_UI32("chunkserver_round_time_ms", 0, 65536, 200, SaunaFSExport,
                   parameters.chunkserver_round_time_ms),
    CONF_ITEM_UI32("chunkserver_connect_timeout_ms", 0, 65536, 2000, SaunaFSExport,
                   parameters.chunkserver_connect_timeout_ms),
    CONF_ITEM_UI32("chunkserver_wave_read_timeout_ms", 0, 65536, 500, SaunaFSExport,
                   parameters.chunkserver_wave_read_timeout_ms),
    CONF_ITEM_UI32("total_read_timeout_ms", 0, 65536, 2000, SaunaFSExport,
                   parameters.total_read_timeout_ms),
    CONF_ITEM_UI32("cache_expiration_time_ms", 0, 65536, 1000, SaunaFSExport,
                   parameters.cache_expiration_time_ms),
    CONF_ITEM_UI32("readahead_max_window_size_kB", 0, 65536, 16384, SaunaFSExport,
                   parameters.readahead_max_window_size_kB),
    CONF_ITEM_UI32("write_cache_size", 0, 1024, 64, SaunaFSExport, parameters.write_cache_size),
    CONF_ITEM_UI32("write_workers", 0, 32, 10, SaunaFSExport, parameters.write_workers),
    CONF_ITEM_UI32("write_window_size", 0, 256, 32, SaunaFSExport, parameters.write_window_size),
    CONF_ITEM_UI32("chunkserver_write_timeout_ms", 0, 60000, 5000, SaunaFSExport,
                   parameters.chunkserver_write_timeout_ms),
    CONF_ITEM_UI32("cache_per_inode_percentage", 0, 80, 25, SaunaFSExport,
                   parameters.cache_per_inode_percentage),
    CONF_ITEM_UI32("symlink_cache_timeout_s", 0, 60000, 3600, SaunaFSExport,
                   parameters.symlink_cache_timeout_s),
    CONF_ITEM_BOOL("debug_mode", false, SaunaFSExport, parameters.debug_mode),
    CONF_ITEM_I32("keep_cache", 0, 2, 0, SaunaFSExport, parameters.keep_cache),
    CONF_ITEM_BOOL("verbose", false, SaunaFSExport, parameters.verbose),
    CONF_ITEM_UI32("fileinfo_cache_timeout", 1, 3600, 60, SaunaFSExport, cacheTimeout),
    CONF_ITEM_UI32("fileinfo_cache_max_size", 100, 1000000, 1000, SaunaFSExport, cacheMaximumSize),
    CONF_ITEM_STR("password", 1, 128, NULL, SaunaFSExport, parameters.password),
    CONF_ITEM_STR("md5_pass", 32, 32, NULL, SaunaFSExport, parameters.md5_pass),
    CONFIG_EOL};

static struct config_block fsal_export_param_block = {
	.dbus_interface_name = "org.ganesha.nfsd.config.fsal.saunafs-export%d",
	.blk_desc.name = "FSAL",
	.blk_desc.type = CONFIG_BLOCK,
	.blk_desc.u.blk.init = noop_conf_init,
	.blk_desc.u.blk.params = fsal_export_params,
	.blk_desc.u.blk.commit = noop_conf_commit
};

/**
 * @brief Release an export
 *
 * @param[in] export SaunaFS export
 */
static inline void releaseExport(struct SaunaFSExport *export) {
	if (export) {
		if (export->fsInstance) {
			sau_destroy(export->fsInstance);
		}

		if (export->cache) {
			destroyFileInfoCache(export->cache);
		}

		gsh_free(export);
	}
}

/**
 * @brief Create a new export.
 *
 * This function creates a new export in the FSAL using the supplied path and
 * options. The function is expected to allocate its own export (the full,
 * private structure).
 *
 * @param [in]  module               FSAL module.
 * @param [in]  parseNode            Opaque pointer to parse tree node for
 *                                   export options to be passed to
 *                                   load_config_from_node.
 * @param [out] errorType            Config processing error reporting.
 * @param [in]  operations           Upcall operations.
 *
 * \see fsal_api.h for more information
 *
 * @returns: FSAL status
 */
static fsal_status_t createExport(struct fsal_module *module, void *parseNode,
                                  struct config_error_type *errorType,
                                  const struct fsal_up_vector *operations) {
	fsal_status_t status;
	struct fsal_pnfs_ds *pnfsDs = NULL;
	int retvalue = 0;

	struct SaunaFSExport *export = gsh_calloc(1, sizeof(struct SaunaFSExport));

	fsal_export_init(&export->export);
	exportOperationsInit(&export->export.exp_ops);

	/* parse parameters for this export */
	sau_set_default_init_params(&export->parameters, "", "", "");

	if (parseNode) {
		retvalue =
		    load_config_from_node(parseNode, &fsal_export_param_block, export, true, errorType);

		if (retvalue != 0) {
			LogCrit(COMPONENT_FSAL, "Failed to parse export configuration for %s",
			        CTX_FULLPATH(op_ctx));

			releaseExport(export);
			return fsalstat(ERR_FSAL_INVAL, 0);
		}
	}

	export->parameters.subfolder = gsh_strdup(CTX_FULLPATH(op_ctx));
	export->fsInstance = sau_init_with_params(&export->parameters);

	if (export->fsInstance == NULL) {
		LogCrit(COMPONENT_FSAL, "Unable to mount SaunaFS cluster for %s.", CTX_FULLPATH(op_ctx));

		releaseExport(export);
		return fsalstat(ERR_FSAL_SERVERFAULT, 0);
	}

	if (fsal_attach_export(module, &export->export.exports) != 0) {
		LogCrit(COMPONENT_FSAL, "Unable to attach export for %s.", CTX_FULLPATH(op_ctx));

		releaseExport(export);
		return fsalstat(ERR_FSAL_SERVERFAULT, 0);
	}

	export->export.fsal = module;
	export->export.up_ops = operations;

	export->pnfsDsEnabled = export->export.exp_ops.fs_supports(
	    &export->export, fso_pnfs_ds_supported);

	if (export->pnfsDsEnabled) {
		export->cache = createFileInfoCache(export->cacheMaximumSize,
		                                    (int)export->cacheTimeout * millisecondsInOneSecond);

		if (export->cache == NULL) {
			LogCrit(COMPONENT_FSAL, "Unable to create fileinfo cache for %s.",
			        CTX_FULLPATH(op_ctx));

			releaseExport(export);
			return fsalstat(ERR_FSAL_SERVERFAULT, 0);
		}

		status = module->m_ops.create_fsal_pnfs_ds(module, parseNode, &pnfsDs);

		if (status.major != ERR_FSAL_NO_ERROR) {
			releaseExport(export);
			return status;
		}

		/* special case: server_id matches export_id */
		pnfsDs->id_servers = op_ctx->ctx_export->export_id;
		pnfsDs->mds_export = op_ctx->ctx_export;
		pnfsDs->mds_fsal_export = &export->export;

		if (!pnfs_ds_insert(pnfsDs)) {
			LogCrit(COMPONENT_CONFIG, "Server id %d already in use.", pnfsDs->id_servers);

			status.major = ERR_FSAL_EXIST;

			/* Return the ref taken by create_fsal_pnfs_ds */
			pnfs_ds_put(pnfsDs);

			releaseExport(export);
			return status;
		}

		LogDebug(COMPONENT_PNFS, "pnfs ds was enabled for [%s]", CTX_FULLPATH(op_ctx));
	}

	export->pnfsMdsEnabled =
	    export->export.exp_ops.fs_supports(&export->export, fso_pnfs_mds_supported);

	if (export->pnfsMdsEnabled) {
		LogDebug(COMPONENT_PNFS, "pnfs mds was enabled for [%s]", CTX_FULLPATH(op_ctx));

		exportOperationsPnfs(&export->export.exp_ops);
	}

	/* get attributes for root inode */
	sau_attr_reply_t reply;

	retvalue = saunafs_getattr(export->fsInstance, &op_ctx->creds, SPECIAL_INODE_ROOT, &reply);

	if (retvalue < 0) {
		status = fsalLastError();

		if (pnfsDs != NULL) {
			/* Remove and destroy the fsal_pnfs_ds */
			pnfs_ds_remove(pnfsDs->id_servers);
		}

		if (pnfsDs != NULL) {
			/* Return the ref taken by create_fsal_pnfs_ds */
			pnfs_ds_put(pnfsDs);
		}

		releaseExport(export);
		return status;
	}

	export->root = allocateHandle(&reply.attr, export);
	op_ctx->fsal_export = &export->export;

	LogDebug(COMPONENT_FSAL, "SaunaFS module export %s.", CTX_FULLPATH(op_ctx));

	return fsalstat(ERR_FSAL_NO_ERROR, 0);
}

/**
 * @brief Initialize the configuration.
 *
 * @param [in]  module         FSAL module.
 * @param [in]  configFile     Parsed ganesha configuration file.
 * @param [out] errorType      config processing error reporting.
 *
 * @returns: FSAL status
 */
static fsal_status_t initialize(struct fsal_module *module, config_file_t configFile,
                                struct config_error_type *errorType) {
	struct SaunaFSModule *myself = NULL;

	myself = container_of(module, struct SaunaFSModule, fsal);

	(void)load_config_from_parse(configFile, &export_param, &myself->filesystemInfo, true,
	                             errorType);

	if (!config_error_is_harmless(errorType)) {
		LogDebug(COMPONENT_FSAL, "config_error_is_harmless failed.");
		return fsalstat(ERR_FSAL_INVAL, 0);
	}

	display_fsinfo(&myself->fsal);

	LogDebug(COMPONENT_FSAL, "FSAL INIT: Supported attributes mask = 0x%" PRIx64,
	         myself->fsal.fs_info.supported_attrs);

	return fsalstat(ERR_FSAL_NO_ERROR, 0);
}

/**
 * @brief Initialize and register SaunaFS FSAL
 *
 * Module initialization.
 * Called by dlopen() to register the module
 * keep a private pointer to me in myself
 */
MODULE_INIT void initializeSaunaFS(void) {
	struct fsal_module *myself = &SaunaFS.fsal;

	int retval =
	    register_fsal(myself, module, FSAL_MAJOR_VERSION, FSAL_MINOR_VERSION, FSAL_ID_SAUNAFS);

	if (retval) {
		LogCrit(COMPONENT_FSAL, "SaunaFS module failed to register.");
		return;
	}

	/* Set up module operations */
	myself->m_ops.create_export = createExport;
	myself->m_ops.init_config = initialize;
	myself->m_ops.fsal_pnfs_ds_ops = pnfsDsOperationsInit;

	pnfsMdsOperationsInit(&myself->m_ops);

	/* Initialize fsal_obj_handle ops for FSAL SaunaFS */
	handleOperationsInit(&SaunaFS.handleOperations);
}

/**
 * @brief Release FSAL resources
 *
 * This function unregisters the FSAL and frees its module handle.
 */
MODULE_FINI void finish(void) {
	int retval = unregister_fsal(&SaunaFS.fsal);

	if (retval != 0) {
		LogCrit(COMPONENT_FSAL, "Unable to unload SaunaFS FSAL. Dying with extreme prejudice.");
		abort();
	}
}
