/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

/*
 * vnode ops for the /dev/net directory
 *
 *	The lookup is based on the internal vanity naming node table.  We also
 *	override readdir in order to delete net nodes no longer	in-use.
 */

#include <sys/types.h>
#include <sys/param.h>
#include <sys/sysmacros.h>
#include <sys/sunndi.h>
#include <fs/fs_subr.h>
#include <sys/fs/dv_node.h>
#include <sys/fs/sdev_impl.h>
#include <sys/policy.h>
#include <sys/zone.h>
#include <sys/dls.h>

struct vnodeops		*devnet_vnodeops;

/*
 * Check if a net sdev_node is still valid - i.e. it represents a current
 * network link.
 * This serves two purposes
 *	- only valid net nodes are returned during lookup() and readdir().
 *	- since net sdev_nodes are not actively destroyed when a network link
 *	  goes away, we use the validator to do deferred cleanup i.e. when such
 *	  nodes are encountered during subsequent lookup() and readdir().
 */
int
devnet_validate(struct sdev_node *dv)
{
	datalink_id_t linkid;
	zoneid_t zoneid;

	ASSERT(!(dv->sdev_flags & SDEV_STALE));
	ASSERT(dv->sdev_state == SDEV_READY);

	if (dls_mgmt_get_linkid(dv->sdev_name, &linkid) != 0)
		return (SDEV_VTOR_INVALID);
	if (SDEV_IS_GLOBAL(dv))
		return (SDEV_VTOR_VALID);
	zoneid = getzoneid();
	return (zone_check_datalink(&zoneid, linkid) == 0 ?
	    SDEV_VTOR_VALID : SDEV_VTOR_INVALID);
}

/*
 * This callback is invoked from devname_lookup_func() to create
 * a net entry when the node is not found in the cache.
 */
static int
devnet_create_rvp(const char *nm, struct vattr *vap, dls_dl_handle_t *ddhp)
{
	timestruc_t now;
	dev_t dev;
	int error;

	if ((error = dls_devnet_open(nm, ddhp, &dev)) != 0) {
		sdcmn_err12(("devnet_create_rvp: not a valid vanity name "
		    "network node: %s\n", nm));
		return (error);
	}

	/*
	 * This is a valid network device (at least at this point in time).
	 * Create the node by setting the attribute; the rest is taken care
	 * of by devname_lookup_func().
	 */
	*vap = sdev_vattr_chr;
	vap->va_mode |= 0666;
	vap->va_rdev = dev;

	gethrestime(&now);
	vap->va_atime = now;
	vap->va_mtime = now;
	vap->va_ctime = now;
	return (0);
}

/*
 * Lookup for /dev/net directory
 *	If the entry does not exist, the devnet_create_rvp() callback
 *	is invoked to create it.  Nodes do not persist across reboot.
 */
/*ARGSUSED3*/
static int
devnet_lookup(struct vnode *dvp, char *nm, struct vnode **vpp,
    struct pathname *pnp, int flags, struct vnode *rdir, struct cred *cred,
    caller_context_t *ct, int *direntflags, pathname_t *realpnp)
{
	struct sdev_node *ddv = VTOSDEV(dvp);
	struct sdev_node *dv = NULL;
	dls_dl_handle_t ddh = NULL;
	struct vattr vattr;
	int nmlen;
	int error = ENOENT;

	if (SDEVTOV(ddv)->v_type != VDIR)
		return (ENOTDIR);

	/*
	 * Empty name or ., return node itself.
	 */
	nmlen = strlen(nm);
	if ((nmlen == 0) || ((nmlen == 1) && (nm[0] == '.'))) {
		*vpp = SDEVTOV(ddv);
		VN_HOLD(*vpp);
		return (0);
	}

	/*
	 * .., return the parent directory
	 */
	if ((nmlen == 2) && (strcmp(nm, "..") == 0)) {
		*vpp = SDEVTOV(ddv->sdev_dotdot);
		VN_HOLD(*vpp);
		return (0);
	}

	rw_enter(&ddv->sdev_contents, RW_WRITER);

	/*
	 * directory cache lookup:
	 */
	if ((dv = sdev_cache_lookup(ddv, nm)) != NULL) {
		if (dv->sdev_state == SDEV_READY) {
			if (!(dv->sdev_flags & SDEV_ATTR_INVALID))
				goto found;
		} else {
			ASSERT(dv->sdev_state == SDEV_ZOMBIE);
			goto failed;
		}
	}

	/*
	 * ZOMBIED parent does not allow new node creation, bail out early.
	 */
	if (ddv->sdev_state == SDEV_ZOMBIE)
		goto failed;

	error = devnet_create_rvp(nm, &vattr, &ddh);
	if (error != 0)
		goto failed;

	error = sdev_mknode(ddv, nm, &dv, &vattr, NULL, NULL, cred, SDEV_READY);
	if (error != 0) {
		ASSERT(dv == NULL);
		dls_devnet_close(ddh);
		goto failed;
	}

	ASSERT(dv != NULL);

	rw_enter(&dv->sdev_contents, RW_WRITER);
	if (dv->sdev_flags & SDEV_ATTR_INVALID) {
		/*
		 * SDEV_ATTR_INVALID means that this device has been
		 * detached, and its dev_t might've been changed too.
		 * Therefore, sdev_node's 'vattr' needs to be updated.
		 */
		SDEVTOV(dv)->v_rdev = vattr.va_rdev;
		ASSERT(dv->sdev_attr != NULL);
		dv->sdev_attr->va_rdev = vattr.va_rdev;
		dv->sdev_flags &= ~SDEV_ATTR_INVALID;
	}
	ASSERT(dv->sdev_private == NULL);
	dv->sdev_private = ddh;
	rw_exit(&dv->sdev_contents);

found:
	ASSERT(SDEV_HELD(dv));
	rw_exit(&ddv->sdev_contents);
	return (sdev_to_vp(dv, vpp));

failed:
	rw_exit(&ddv->sdev_contents);

	if (dv != NULL)
		SDEV_RELE(dv);

	*vpp = NULL;
	return (error);
}

static int
devnet_filldir_datalink(datalink_id_t linkid, void *arg)
{
	struct sdev_node	*ddv = arg;
	struct vattr		vattr;
	struct sdev_node	*dv;
	dls_dl_handle_t		ddh = NULL;
	char			link[MAXLINKNAMELEN];

	ASSERT(RW_WRITE_HELD(&ddv->sdev_contents));

	if (dls_mgmt_get_linkinfo(linkid, link, NULL, NULL, NULL) != 0)
		return (0);

	if ((dv = sdev_cache_lookup(ddv, (char *)link)) != NULL)
		goto found;

	if (devnet_create_rvp(link, &vattr, &ddh) != 0)
		return (0);

	ASSERT(ddh != NULL);
	dls_devnet_close(ddh);

	if (sdev_mknode(ddv, (char *)link, &dv, &vattr, NULL, NULL, kcred,
	    SDEV_READY) != 0) {
		return (0);
	}

	/*
	 * As there is no reference holding the network device, it could be
	 * detached. Set SDEV_ATTR_INVALID so that the 'vattr' will be updated
	 * later.
	 */
	rw_enter(&dv->sdev_contents, RW_WRITER);
	dv->sdev_flags |= SDEV_ATTR_INVALID;
	rw_exit(&dv->sdev_contents);

found:
	SDEV_SIMPLE_RELE(dv);
	return (0);
}

static void
devnet_filldir(struct sdev_node *ddv)
{
	sdev_node_t	*dv, *next;
	datalink_id_t	linkid;

	ASSERT(RW_READ_HELD(&ddv->sdev_contents));
	if (rw_tryupgrade(&ddv->sdev_contents) == NULL) {
		rw_exit(&ddv->sdev_contents);
		rw_enter(&ddv->sdev_contents, RW_WRITER);
	}

	for (dv = SDEV_FIRST_ENTRY(ddv); dv; dv = next) {
		next = SDEV_NEXT_ENTRY(ddv, dv);

		/* validate and prune only ready nodes */
		if (dv->sdev_state != SDEV_READY)
			continue;

		switch (devnet_validate(dv)) {
		case SDEV_VTOR_VALID:
		case SDEV_VTOR_SKIP:
			continue;
		case SDEV_VTOR_INVALID:
		case SDEV_VTOR_STALE:
			sdcmn_err12(("devnet_filldir: destroy invalid "
			    "node: %s(%p)\n", dv->sdev_name, (void *)dv));
			break;
		}

		if (SDEVTOV(dv)->v_count > 0)
			continue;
		SDEV_HOLD(dv);
		/* remove the cache node */
		(void) sdev_cache_update(ddv, &dv, dv->sdev_name,
		    SDEV_CACHE_DELETE);
	}

	if (((ddv->sdev_flags & SDEV_BUILD) == 0) && !dls_devnet_rebuild())
		goto done;

	if (SDEV_IS_GLOBAL(ddv)) {
		linkid = DATALINK_INVALID_LINKID;
		do {
			linkid = dls_mgmt_get_next(linkid, DATALINK_CLASS_ALL,
			    DATALINK_ANY_MEDIATYPE, DLMGMT_ACTIVE);
			if (linkid != DATALINK_INVALID_LINKID)
				(void) devnet_filldir_datalink(linkid, ddv);
		} while (linkid != DATALINK_INVALID_LINKID);
	} else {
		(void) zone_datalink_walk(getzoneid(),
		    devnet_filldir_datalink, ddv);
	}

	ddv->sdev_flags &= ~SDEV_BUILD;

done:
	rw_downgrade(&ddv->sdev_contents);
}

/*
 * Display all instantiated network datalink device nodes.
 * A /dev/net entry will be created only after the first lookup of
 * the network datalink device succeeds.
 */
/*ARGSUSED4*/
static int
devnet_readdir(struct vnode *dvp, struct uio *uiop, struct cred *cred,
    int *eofp, caller_context_t *ct, int flags)
{
	struct sdev_node *sdvp = VTOSDEV(dvp);

	ASSERT(sdvp);

	if (uiop->uio_offset == 0)
		devnet_filldir(sdvp);

	return (devname_readdir_func(dvp, uiop, cred, eofp, 0));
}

/*
 * This callback is invoked from devname_inactive_func() to release
 * the net entry which was held in devnet_create_rvp().
 */
static void
devnet_inactive_callback(struct vnode *dvp)
{
	struct sdev_node *sdvp = VTOSDEV(dvp);
	dls_dl_handle_t ddh;

	if (dvp->v_type == VDIR)
		return;

	ASSERT(dvp->v_type == VCHR);
	rw_enter(&sdvp->sdev_contents, RW_WRITER);
	ddh = sdvp->sdev_private;
	sdvp->sdev_private = NULL;
	sdvp->sdev_flags |= SDEV_ATTR_INVALID;
	rw_exit(&sdvp->sdev_contents);

	/*
	 * "ddh" (sdev_private) could be NULL if devnet_lookup fails.
	 */
	if (ddh != NULL)
		dls_devnet_close(ddh);
}

/*ARGSUSED*/
static void
devnet_inactive(struct vnode *dvp, struct cred *cred, caller_context_t *ct)
{
	devname_inactive_func(dvp, cred, devnet_inactive_callback);
}

/*
 * We override lookup and readdir to build entries based on the
 * in kernel vanity naming node table.
 */
const fs_operation_def_t devnet_vnodeops_tbl[] = {
	VOPNAME_READDIR,	{ .vop_readdir = devnet_readdir },
	VOPNAME_LOOKUP,		{ .vop_lookup = devnet_lookup },
	VOPNAME_INACTIVE,	{ .vop_inactive = devnet_inactive },
	VOPNAME_CREATE,		{ .error = fs_nosys },
	VOPNAME_REMOVE,		{ .error = fs_nosys },
	VOPNAME_MKDIR,		{ .error = fs_nosys },
	VOPNAME_RMDIR,		{ .error = fs_nosys },
	VOPNAME_SYMLINK,	{ .error = fs_nosys },
	VOPNAME_SETSECATTR,	{ .error = fs_nosys },
	NULL,			NULL
};