/*
 * This file and its contents are supplied under the terms of the
 * Common Development and Distribution License ("CDDL"), version 1.0.
 * You may only use this file in accordance with the terms of version
 * 1.0 of the CDDL.
 *
 * A full copy of the text of the CDDL should have accompanied this
 * source.  A copy of the CDDL is also available via the Internet at
 * http://www.illumos.org/license/CDDL.
 */

/*
 * Copyright 2015 Joyent, Inc.
 */

/*
 * Overlay device encapsulation plugin management
 *
 * For more information, see the big theory statement in
 * uts/common/io/overlay/overlay.c
 */

#include <sys/types.h>
#include <sys/kmem.h>
#include <sys/ddi.h>
#include <sys/sunddi.h>
#include <sys/errno.h>
#include <sys/sysmacros.h>
#include <sys/modctl.h>

#include <sys/overlay_impl.h>

static kmem_cache_t *overlay_plugin_cache;
static kmutex_t overlay_plugin_lock;
static list_t overlay_plugin_list;

#define	OVERLAY_MODDIR	"overlay"

/* ARGSUSED */
static int
overlay_plugin_cache_constructor(void *buf, void *arg, int kmflags)
{
	overlay_plugin_t *opp = buf;

	mutex_init(&opp->ovp_mutex, NULL, MUTEX_DRIVER, NULL);
	list_link_init(&opp->ovp_link);

	return (0);
}

/* ARGSUSED */
static void
overlay_plugin_cache_destructor(void *buf, void *arg)
{
	overlay_plugin_t *opp = buf;
	ASSERT(list_link_active(&opp->ovp_link) == 0);
	mutex_destroy(&opp->ovp_mutex);
}

void
overlay_plugin_init(void)
{
	mutex_init(&overlay_plugin_lock, NULL, MUTEX_DRIVER, 0);

	/*
	 * In the future we may want to have a reaper to unload unused modules
	 * to help the kernel be able to reclaim memory.
	 */
	overlay_plugin_cache = kmem_cache_create("overlay_plugin_cache",
	    sizeof (overlay_plugin_t), 0, overlay_plugin_cache_constructor,
	    overlay_plugin_cache_destructor, NULL, NULL, NULL, 0);
	list_create(&overlay_plugin_list, sizeof (overlay_plugin_t),
	    offsetof(overlay_plugin_t, ovp_link));
}

void
overlay_plugin_fini(void)
{
	mutex_enter(&overlay_plugin_lock);
	VERIFY(list_is_empty(&overlay_plugin_list));
	mutex_exit(&overlay_plugin_lock);

	list_destroy(&overlay_plugin_list);
	kmem_cache_destroy(overlay_plugin_cache);
	mutex_destroy(&overlay_plugin_lock);
}

overlay_plugin_register_t *
overlay_plugin_alloc(uint_t version)
{
	overlay_plugin_register_t *ovrp;
	/* Version 1 is the only one that exists */
	if (version != OVEP_VERSION_ONE)
		return (NULL);

	ovrp = kmem_zalloc(sizeof (overlay_plugin_register_t), KM_SLEEP);
	ovrp->ovep_version = version;
	return (ovrp);
}

void
overlay_plugin_free(overlay_plugin_register_t *ovrp)
{
	kmem_free(ovrp, sizeof (overlay_plugin_register_t));
}

int
overlay_plugin_register(overlay_plugin_register_t *ovrp)
{
	overlay_plugin_t *opp, *ipp;

	/* Sanity check parameters of the registration */
	if (ovrp->ovep_version != OVEP_VERSION_ONE)
		return (EINVAL);

	if (ovrp->ovep_name == NULL || ovrp->ovep_ops == NULL)
		return (EINVAL);

	if ((ovrp->ovep_flags & ~(OVEP_F_VLAN_TAG)) != 0)
		return (EINVAL);

	if (ovrp->ovep_id_size < 1)
		return (EINVAL);

	/* Don't support anything that has an id size larger than 8 bytes */
	if (ovrp->ovep_id_size > 8)
		return (ENOTSUP);

	if (ovrp->ovep_dest == OVERLAY_PLUGIN_D_INVALID)
		return (EINVAL);

	if ((ovrp->ovep_dest & ~OVERLAY_PLUGIN_D_MASK) != 0)
		return (EINVAL);

	if (ovrp->ovep_ops->ovpo_callbacks != 0)
		return (EINVAL);
	if (ovrp->ovep_ops->ovpo_init == NULL)
		return (EINVAL);
	if (ovrp->ovep_ops->ovpo_fini == NULL)
		return (EINVAL);
	if (ovrp->ovep_ops->ovpo_encap == NULL)
		return (EINVAL);
	if (ovrp->ovep_ops->ovpo_decap == NULL)
		return (EINVAL);
	if (ovrp->ovep_ops->ovpo_socket == NULL)
		return (EINVAL);
	if (ovrp->ovep_ops->ovpo_getprop == NULL)
		return (EINVAL);
	if (ovrp->ovep_ops->ovpo_setprop == NULL)
		return (EINVAL);
	if (ovrp->ovep_ops->ovpo_propinfo == NULL)
		return (EINVAL);


	opp = kmem_cache_alloc(overlay_plugin_cache, KM_SLEEP);
	opp->ovp_active = 0;
	opp->ovp_name = ovrp->ovep_name;
	opp->ovp_ops = ovrp->ovep_ops;
	opp->ovp_props = ovrp->ovep_props;
	opp->ovp_id_size = ovrp->ovep_id_size;
	opp->ovp_flags = ovrp->ovep_flags;
	opp->ovp_dest = ovrp->ovep_dest;

	opp->ovp_nprops = 0;
	if (ovrp->ovep_props != NULL) {
		while (ovrp->ovep_props[opp->ovp_nprops] != NULL) {
			if (strlen(ovrp->ovep_props[opp->ovp_nprops]) >=
			    OVERLAY_PROP_NAMELEN) {
				mutex_exit(&overlay_plugin_lock);
				kmem_cache_free(overlay_plugin_cache, opp);
				return (EINVAL);
			}
			opp->ovp_nprops++;
		}
	}

	mutex_enter(&overlay_plugin_lock);
	for (ipp = list_head(&overlay_plugin_list); ipp != NULL;
	    ipp = list_next(&overlay_plugin_list, ipp)) {
		if (strcmp(ipp->ovp_name, opp->ovp_name) == 0) {
			mutex_exit(&overlay_plugin_lock);
			kmem_cache_free(overlay_plugin_cache, opp);
			return (EEXIST);
		}
	}
	list_insert_tail(&overlay_plugin_list, opp);
	mutex_exit(&overlay_plugin_lock);

	return (0);
}

int
overlay_plugin_unregister(const char *name)
{
	overlay_plugin_t *opp;

	mutex_enter(&overlay_plugin_lock);
	for (opp = list_head(&overlay_plugin_list); opp != NULL;
	    opp = list_next(&overlay_plugin_list, opp)) {
		if (strcmp(opp->ovp_name, name) == 0)
			break;
	}

	if (opp == NULL) {
		mutex_exit(&overlay_plugin_lock);
		return (ENOENT);
	}

	mutex_enter(&opp->ovp_mutex);
	if (opp->ovp_active > 0) {
		mutex_exit(&opp->ovp_mutex);
		mutex_exit(&overlay_plugin_lock);
		return (EBUSY);
	}
	mutex_exit(&opp->ovp_mutex);

	list_remove(&overlay_plugin_list, opp);
	mutex_exit(&overlay_plugin_lock);

	kmem_cache_free(overlay_plugin_cache, opp);
	return (0);
}

overlay_plugin_t *
overlay_plugin_lookup(const char *name)
{
	overlay_plugin_t *opp;
	boolean_t trymodload = B_FALSE;

	for (;;) {
		mutex_enter(&overlay_plugin_lock);
		for (opp = list_head(&overlay_plugin_list); opp != NULL;
		    opp = list_next(&overlay_plugin_list, opp)) {
			if (strcmp(name, opp->ovp_name) == 0) {
				mutex_enter(&opp->ovp_mutex);
				opp->ovp_active++;
				mutex_exit(&opp->ovp_mutex);
				mutex_exit(&overlay_plugin_lock);
				return (opp);
			}
		}
		mutex_exit(&overlay_plugin_lock);

		if (trymodload == B_TRUE)
			return (NULL);

		/*
		 * If we didn't find it, it may still exist, but just not have
		 * been a loaded module. In that case, we'll do one attempt to
		 * load it.
		 */
		if (modload(OVERLAY_MODDIR, (char *)name) == -1)
			return (NULL);
		trymodload = B_TRUE;
	}

}

void
overlay_plugin_rele(overlay_plugin_t *opp)
{
	mutex_enter(&opp->ovp_mutex);
	ASSERT(opp->ovp_active > 0);
	opp->ovp_active--;
	mutex_exit(&opp->ovp_mutex);
}

void
overlay_plugin_walk(overlay_plugin_walk_f func, void *arg)
{
	overlay_plugin_t *opp;
	mutex_enter(&overlay_plugin_lock);
	for (opp = list_head(&overlay_plugin_list); opp != NULL;
	    opp = list_next(&overlay_plugin_list, opp)) {
		if (func(opp, arg) != 0) {
			mutex_exit(&overlay_plugin_lock);
			return;
		}
	}
	mutex_exit(&overlay_plugin_lock);
}