/*
 * 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 2008 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <assert.h>
#include <sys/dld.h>
#include <libdladm_impl.h>
#include <libdllink.h>
#include <libdlvlan.h>

/*
 * VLAN Administration Library.
 *
 * This library is used by administration tools such as dladm(1M) to
 * configure VLANs.
 */

/*
 * Returns the current attributes of the specified VLAN.
 */
static dladm_status_t
i_dladm_vlan_info_active(datalink_id_t vlanid, dladm_vlan_attr_t *dvap)
{
	int			fd;
	dld_ioc_vlan_attr_t	div;
	dladm_status_t		status = DLADM_STATUS_OK;

	if ((fd = open(DLD_CONTROL_DEV, O_RDWR)) < 0)
		return (dladm_errno2status(errno));

	div.div_vlanid = vlanid;

	if (ioctl(fd, DLDIOC_VLAN_ATTR, &div) < 0)
		status = dladm_errno2status(errno);

	dvap->dv_vid = div.div_vid;
	dvap->dv_linkid = div.div_linkid;
	dvap->dv_force = div.div_force;
	dvap->dv_implicit = div.div_implicit;
done:
	(void) close(fd);
	return (status);
}

/*
 * Returns the persistent attributes of the specified VLAN.
 */
static dladm_status_t
i_dladm_vlan_info_persist(datalink_id_t vlanid, dladm_vlan_attr_t *dvap)
{
	dladm_conf_t	conf = DLADM_INVALID_CONF;
	dladm_status_t	status;
	uint64_t	u64;

	if ((status = dladm_read_conf(vlanid, &conf)) != DLADM_STATUS_OK)
		return (status);

	status = dladm_get_conf_field(conf, FLINKOVER, &u64, sizeof (u64));
	if (status != DLADM_STATUS_OK)
		goto done;
	dvap->dv_linkid = (datalink_id_t)u64;

	status = dladm_get_conf_field(conf, FFORCE, &dvap->dv_force,
	    sizeof (boolean_t));
	if (status != DLADM_STATUS_OK)
		goto done;

	dvap->dv_implicit = B_FALSE;

	status = dladm_get_conf_field(conf, FVLANID, &u64, sizeof (u64));
	if (status != DLADM_STATUS_OK)
		goto done;
	dvap->dv_vid = (uint16_t)u64;

done:
	dladm_destroy_conf(conf);
	return (status);
}

dladm_status_t
dladm_vlan_info(datalink_id_t vlanid, dladm_vlan_attr_t *dvap, uint32_t flags)
{
	assert(flags == DLADM_OPT_ACTIVE || flags == DLADM_OPT_PERSIST);
	if (flags == DLADM_OPT_ACTIVE)
		return (i_dladm_vlan_info_active(vlanid, dvap));
	else
		return (i_dladm_vlan_info_persist(vlanid, dvap));
}

static dladm_status_t
dladm_persist_vlan_conf(const char *vlan, datalink_id_t vlanid,
    boolean_t force, datalink_id_t linkid, uint16_t vid)
{
	dladm_conf_t	conf = DLADM_INVALID_CONF;
	dladm_status_t	status;
	uint64_t	u64;

	if ((status = dladm_create_conf(vlan, vlanid, DATALINK_CLASS_VLAN,
	    DL_ETHER, &conf)) != DLADM_STATUS_OK) {
		return (status);
	}

	u64 = linkid;
	status = dladm_set_conf_field(conf, FLINKOVER, DLADM_TYPE_UINT64, &u64);
	if (status != DLADM_STATUS_OK)
		goto done;

	status = dladm_set_conf_field(conf, FFORCE, DLADM_TYPE_BOOLEAN, &force);
	if (status != DLADM_STATUS_OK)
		goto done;

	u64 = vid;
	status = dladm_set_conf_field(conf, FVLANID, DLADM_TYPE_UINT64, &u64);
	if (status != DLADM_STATUS_OK)
		goto done;

	status = dladm_write_conf(conf);

done:
	dladm_destroy_conf(conf);
	return (status);
}

/*
 * Create a VLAN on given link.
 */
dladm_status_t
dladm_vlan_create(const char *vlan, datalink_id_t linkid, uint16_t vid,
    uint32_t flags)
{
	dld_ioc_create_vlan_t	dic;
	int			fd;
	datalink_id_t		vlanid = DATALINK_INVALID_LINKID;
	uint_t			media;
	datalink_class_t	class;
	dladm_status_t		status;

	if (vid < 1 || vid > 4094)
		return (DLADM_STATUS_VIDINVAL);

	if ((fd = open(DLD_CONTROL_DEV, O_RDWR)) < 0)
		return (dladm_errno2status(errno));

	status = dladm_datalink_id2info(linkid, NULL, &class, &media, NULL, 0);
	if (status != DLADM_STATUS_OK || media != DL_ETHER ||
	    class == DATALINK_CLASS_VLAN) {
		return (DLADM_STATUS_BADARG);
	}

	status = dladm_create_datalink_id(vlan, DATALINK_CLASS_VLAN, DL_ETHER,
	    flags & (DLADM_OPT_ACTIVE | DLADM_OPT_PERSIST), &vlanid);
	if (status != DLADM_STATUS_OK)
		goto fail;

	if (flags & DLADM_OPT_PERSIST) {
		status = dladm_persist_vlan_conf(vlan, vlanid,
		    (flags & DLADM_OPT_FORCE) != 0, linkid, vid);
		if (status != DLADM_STATUS_OK)
			goto fail;
	}

	if (flags & DLADM_OPT_ACTIVE) {
		dic.dic_vlanid = vlanid;
		dic.dic_linkid = linkid;
		dic.dic_vid = vid;
		dic.dic_force = (flags & DLADM_OPT_FORCE) != 0;

		if (ioctl(fd, DLDIOC_CREATE_VLAN, &dic) < 0) {
			status = dladm_errno2status(errno);
			if (flags & DLADM_OPT_PERSIST)
				(void) dladm_remove_conf(vlanid);
			goto fail;
		}
	}

	(void) close(fd);
	return (DLADM_STATUS_OK);

fail:
	if (vlanid != DATALINK_INVALID_LINKID) {
		(void) dladm_destroy_datalink_id(vlanid,
		    flags & (DLADM_OPT_ACTIVE | DLADM_OPT_PERSIST));
	}
	(void) close(fd);
	return (status);
}

/*
 * Delete a given VLAN.
 */
dladm_status_t
dladm_vlan_delete(datalink_id_t vlanid, uint32_t flags)
{
	dld_ioc_delete_vlan_t	did;
	int			fd;
	datalink_class_t	class;
	dladm_status_t		status = DLADM_STATUS_OK;

	if ((dladm_datalink_id2info(vlanid, NULL, &class, NULL, NULL, 0) !=
	    DLADM_STATUS_OK) || (class != DATALINK_CLASS_VLAN)) {
		return (DLADM_STATUS_BADARG);
	}

	if (flags & DLADM_OPT_ACTIVE) {
		if ((fd = open(DLD_CONTROL_DEV, O_RDWR)) < 0)
			return (dladm_errno2status(errno));

		did.did_linkid = vlanid;
		if ((ioctl(fd, DLDIOC_DELETE_VLAN, &did) < 0) &&
		    ((errno != ENOENT) || !(flags & DLADM_OPT_PERSIST))) {
			(void) close(fd);
			return (dladm_errno2status(errno));
		}
		(void) close(fd);

		/*
		 * Delete active linkprop before this active link is deleted.
		 */
		(void) dladm_set_linkprop(vlanid, NULL, NULL, 0,
		    DLADM_OPT_ACTIVE);
	}

	(void) dladm_destroy_datalink_id(vlanid,
	    flags & (DLADM_OPT_ACTIVE | DLADM_OPT_PERSIST));

	if (flags & DLADM_OPT_PERSIST)
		(void) dladm_remove_conf(vlanid);

	return (status);
}

/*
 * Callback used by dladm_vlan_up()
 */
static int
i_dladm_vlan_up(datalink_id_t vlanid, void *arg)
{
	dladm_vlan_attr_t	dva;
	dld_ioc_create_vlan_t	dic;
	dladm_status_t		*statusp = arg;
	uint32_t		flags;
	int			fd;
	dladm_status_t		status;

	status = dladm_vlan_info(vlanid, &dva, DLADM_OPT_PERSIST);
	if (status != DLADM_STATUS_OK)
		goto done;

	/*
	 * Validate (and delete) the link associated with this VLAN, see if
	 * the specific hardware has been removed during system shutdown.
	 */
	if ((status = dladm_datalink_id2info(dva.dv_linkid, &flags, NULL,
	    NULL, NULL, 0)) != DLADM_STATUS_OK) {
		goto done;
	}

	if (!(flags & DLADM_OPT_ACTIVE)) {
		status = DLADM_STATUS_BADARG;
		goto done;
	}

	dic.dic_linkid = dva.dv_linkid;
	dic.dic_force = dva.dv_force;
	dic.dic_vid = dva.dv_vid;

	if ((fd = open(DLD_CONTROL_DEV, O_RDWR)) < 0) {
		status = dladm_errno2status(errno);
		goto done;
	}

	dic.dic_vlanid = vlanid;
	if (ioctl(fd, DLDIOC_CREATE_VLAN, &dic) < 0) {
		status = dladm_errno2status(errno);
		goto done;
	}

	if ((status = dladm_up_datalink_id(vlanid)) != DLADM_STATUS_OK) {
		dld_ioc_delete_vlan_t did;

		did.did_linkid = vlanid;
		(void) ioctl(fd, DLDIOC_DELETE_VLAN, &did);
	} else {
		/*
		 * Reset the active linkprop of this specific link.
		 */
		(void) dladm_init_linkprop(vlanid, B_FALSE);
	}

	(void) close(fd);
done:
	*statusp = status;
	return (DLADM_WALK_CONTINUE);
}

/*
 * Bring up one VLAN, or all persistent VLANs.  In the latter case, the
 * walk may terminate early if bringup of a VLAN fails.
 */
dladm_status_t
dladm_vlan_up(datalink_id_t linkid)
{
	dladm_status_t	status;

	if (linkid == DATALINK_ALL_LINKID) {
		(void) dladm_walk_datalink_id(i_dladm_vlan_up, &status,
		    DATALINK_CLASS_VLAN, DATALINK_ANY_MEDIATYPE,
		    DLADM_OPT_PERSIST);
		return (DLADM_STATUS_OK);
	} else {
		(void) i_dladm_vlan_up(linkid, &status);
		return (status);
	}
}