/*
 * Copyright (c) 2017-2020, AT&T Intellectual Property. All rights reserved.
 * Copyright (c) 2015-2017 by Brocade Communications Systems, Inc.
 * All rights reserved.
 *
 * SPDX-License-Identifier: LGPL-2.1-only
 *
 * Controller vplane (dataplane) database and associated access API's.
 *
 * This module consists of two broad sets of function, those associated
 * with provisioning (configuration) of vplane instances
 * (vplane_cfg_xxx()) and those associated with the run-time update and
 * lookup of vplane instances.
 *
 * Provisioning (configuration) of vplanes is a multi-stage process.
 *
 * Firstly all existing (active) instances are marked as "delete
 * pending".
 *
 * Next each new vplane instance is created and populated with its
 * attributes as generated by the parser code. Each instance is
 * maintained in a simple linear list.
 *
 * Once the parsing process is complete the list is processed and each
 * configured instance is either merged with an existing active instance
 * (clearing the "delete pending" marker in the process) or a new active
 * instance is created.
 *
 * Finally any active instances marked as "delete pending" are
 * destroyed, i.e. remove and destroy any vplanes that are no longer
 * part of the configuration.
 *
 * Should the parsing process fail, the list of configured instances is
 * simply discarded.
 *
 * Once configured and following the exchange of the initial
 * "connect<->accept" message sequence, the vplane instance is marked as
 * connected.
 *
 * Access to active vplane instances is either via its configured UUID
 * or its session ID. The session ID is established as part of the
 * vplane connection process.
 *
 * All ZeroMQ ROUTER-DEALER messages contain an outer envelope that
 * contains a 32-bit flow identifier. It is this ID, in string format,
 * that is used as the vplane session ID. Thus once connected, all
 * messages from a vplane can use the session ID to obtain the
 * appropriate vplane context.
 *
 */

#include <stdio.h>
#include <string.h>
#include <syslog.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <linux/if.h>
#include <assert.h>
#include <czmq.h>
#include <json.h>

#include "controller.h"
#include "vplane.h"

#define DEFAULT_UUID "00000000-0000-0000-0000-000000000000"
#define FOR_ALL_VPLANES_BYUUID(vplaneuuid, vp)  \
	for (vp = zhash_first(vplaneuuid);	\
	     vp != NULL;			\
	     vp = zhash_next(vplaneuuid))

typedef enum {
	VP_ATTR_MIN = 0,
	VP_ATTR_UUID = VP_ATTR_MIN,
	VP_ATTR_SESSIONID,
	VP_ATTR_CONTROL,
	VP_ATTR_MAX
} vp_attr_t;

typedef struct vplaneif_ {
	uint32_t ifindex;
	uint32_t operstate;
	int fd;
	void *cookie;
	bool delpend;
	char ifname[IFNAMSIZ];
} vplaneif_t;

typedef struct vplane_ {
	int id;
	uint32_t timeout;
	bool local;
	bool connected;
	bool delpend;
	bool update_needed;
	struct ip_addr ctladdr;
	zframe_t *envelope;
	int64_t clocktick;
	const char *discon_reason;
	int connects;
	char *attr[VP_ATTR_MAX];
	vplaneif_t *interfaces[MAX_PORTS];
} vplane_t;

/*
 * Hash table of provisioned vplanes, keyed using the UUID
 */
static zhash_t *vplaneuuid;

/*
 * Hash table of connected vplanes, keyed using the session ID.
 */
static zhash_t *vplanesession;

/*
 * List of vplanes that are being configured
 */
static zlist_t *vplanecfg;

/*
 * List of vplanes that have expired and are marked for delete. Used
 * during configuration cleanup following an update.
 */
static zlist_t *vplanedelpend;

/*
 * Cached copy of the last configured vplane
 */
static vplane_t *vplanecfg_lastfound;

static inline void attr_free(vplane_t *vp, vp_attr_t attr)
{
	free(vp->attr[attr]);
	vp->attr[attr] = NULL;
}

/* Do we have any remote vplanes configured? */
bool
vplane_remote(void)
{
	vplane_t *vp;

	FOR_ALL_VPLANES_BYUUID(vplaneuuid, vp) {
		if (!vplane_is_local(vp))
			return true;
	}

	return false;
}

bool
vplane_is_local(const vplane_t *vp)
{
	return (vp != NULL) && vp->local;
}

bool
vplane_is_connected(const vplane_t *vp)
{
	return (vp != NULL) && !vp->delpend && vp->connected;
}

bool
vplane_is_update_needed(const vplane_t *vp)
{
	return vplane_is_connected(vp) && vp->update_needed;
}

zframe_t *vplane_get_envelope(const vplane_t *vp)
{
	return vp ? vp->envelope : NULL;
}

/*
 * For a given vplane instance return its identifier.
 */
int
vplane_get_id(const vplane_t *vp)
{
	if (vp != NULL)
		return vp->id;

	return -1;
}

/*
 * For a given vplane instance return its UUID string.
 */
const char *
vplane_get_uuid(const vplane_t *vp)
{
	if (vp != NULL)
		return vp->attr[VP_ATTR_UUID];

	return NULL;
}

/*
 * For a given vplane instance return its current 0MQ control URL,
 * e.g. "tcp://10.0.0.101:5907"
 */
const char *
vplane_get_control(const vplane_t *vp)
{
	if (vp != NULL)
		return vp->attr[VP_ATTR_CONTROL];

	return NULL;
}

/*
 * Update this vplane instance with its 0MQ URL for use with the REQ/REP
 * socket, i.e. used to retrieve operational data from the vplane.
 */
int
vplane_set_control(vplane_t *vp, const char *url)
{
	int rc = -1;

	if (vp == NULL)
		errno = EINVAL;
	else {
		attr_free(vp, VP_ATTR_CONTROL);
		vp->attr[VP_ATTR_CONTROL] = strdup(url);
		if (vp->attr[VP_ATTR_CONTROL] != NULL)
			rc = 0;
	}

	return rc;
}

/*
 * Attempt to locate a vplane instance using the 0MQ ROUTER-DEALER
 * session ID (message envelope)
 */
vplane_t *
vplane_findbysession(zframe_t *envelope)
{
	vplane_t *vp;

	vp = NULL;
	if (envelope != NULL) {
		char *sessionid;

		sessionid = zframe_strhex(envelope);
		if (sessionid != NULL) {
			vp = zhash_lookup(vplanesession, sessionid);
			free(sessionid);
		}
	}

	return vp;
}

/*
 * Attempt to locate a vplane instances using a UUID
 */
vplane_t *
vplane_findbyuuid(const char *uuid)
{
	if (uuid != NULL)
		return zhash_lookup(vplaneuuid, uuid);

	return NULL;
}

const vplane_t *
vplane_findbyid(int id)
{
	vplane_t *vp;

	FOR_ALL_VPLANES_BYUUID(vplaneuuid, vp) {
		if (vp->id == id)
			return vp;
	}
	return NULL;
}

/* Walk on all vplanes */
void vplane_walk_all(vplane_iter_func_t func, void *arg)
{
	vplane_t *vp;

	FOR_ALL_VPLANES_BYUUID(vplaneuuid, vp) {
		(func)(vp, arg);
	}
}

static vplaneif_t *
vplane_iface_get(const vplane_t *vp, uint32_t ifn, bool checkexists,
		 const char *who)
{
	vplaneif_t *vpif;

	if ((vp == NULL) ||
	    (ifn >= MAX_PORTS)) {
		err("vplane: invalid or missing arguments to %s (%u)",
		    who, ifn);
		return NULL;
	}

	vpif = vp->interfaces[ifn];
	if ((vpif == NULL) && checkexists)
		err("vplane(%d.%u): cannot find interface (%s)", vp->id,
		    ifn, who);

	return vpif;
}

const char *
vplane_iface_get_ifname(const vplane_t *vp, uint32_t ifn)
{
	const vplaneif_t *vpif;

	vpif = vplane_iface_get(vp, ifn, true, "iface_get_ifname");
	if (vpif != NULL)
		return vpif->ifname;

	return NULL;
}

uint32_t
vplane_iface_get_ifindex(const vplane_t *vp, uint32_t ifn)
{
	const vplaneif_t *vpif;

	vpif = vplane_iface_get(vp, ifn, true, "iface_get_ifindex");
	if (vpif != NULL)
		return vpif->ifindex;

	return 0;
}

int
vplane_iface_get_fd(const vplane_t *vp, uint32_t ifn)
{
	const vplaneif_t *vpif;

	vpif = vplane_iface_get(vp, ifn, false, "iface_get_fd");
	if (vpif != NULL)
		return vpif->fd;

	return -1;
}

int
vplane_iface_set_fd(const vplane_t *vp, uint32_t ifn, int fd)
{
	vplaneif_t *vpif;

	vpif = vplane_iface_get(vp, ifn, true, "iface_set_fd");
	if (vpif != NULL) {
		vpif->fd = fd;
		return 0;
	}

	return -1;
}

int
vplane_iface_set_state(const vplane_t *vp, uint32_t ifn, uint32_t operstate)
{
	vplaneif_t *vpif;

	vpif = vplane_iface_get(vp, ifn, true, "iface_set_state");
	if (vpif != NULL) {
		if (operstate != vpif->operstate) {
			info("vplane(%d.%u) %s state change %u->%u",
			     vp->id, ifn, vpif->ifname,
			     vpif->operstate, operstate);
			vpif->operstate = operstate;
		}
		return 0;
	}

	return -1;
}

/*
 * Delete an interface associated with the given vplane
 */
void
vplane_iface_del(vplane_t *vp, uint32_t ifn)
{
	vplaneif_t *vpif;

	vpif = vplane_iface_get(vp, ifn, false, "iface_del");
	if (vpif != NULL) {
		info("vplane(%d.%u): delete interface %s (%u)", vp->id,
		     ifn, vpif->ifname, vpif->ifindex);
		vpif->operstate = IF_OPER_UNKNOWN;
		free(vpif);
		vp->interfaces[ifn] = NULL;
	}
}

/*
 * Add a new interface to the given vplane.
 */
int
vplane_iface_add(vplane_t *vp, uint32_t ifn, uint32_t ifindex,
		 const char *ifname)
{
	vplaneif_t *vpif;
	const char *action;

	if ((vp == NULL) ||
	    (ifname == NULL) ||
	    (ifn >= MAX_PORTS)) {
		err("vplane: invalid arguments to iface_add (%u, %u)",
		    ifn, ifindex);
		return -1;
	}

	vpif = vp->interfaces[ifn];
	if (vpif == NULL) {
		vpif = malloc(sizeof(*vpif));
		if (vpif == NULL) {
			err("vplane(%d.%u): cannot add interface %s (%u)",
			    vp->id, ifn, ifname, ifindex);
			return -1;
		}
		action = "add";
		vpif->fd = -1;
	} else {
		if ((vpif->ifindex == ifindex) &&
		   (streq(vpif->ifname, ifname)))
			action = "re-add";
		else {
			info("vplane(%d.%u): update interface %s/%s (%u/%u)",
			     vp->id, ifn, vpif->ifname, ifname, vpif->ifindex,
			     ifindex);
			action = NULL;
		}
	}

	vpif->ifindex = ifindex;
	vpif->operstate = IF_OPER_UNKNOWN;
	vpif->delpend = false;
	int sz = sizeof(vpif->ifname);

	strncpy(vpif->ifname, ifname, sz-1);
	vpif->ifname[sz-1] = '\0';
	vp->interfaces[ifn] = vpif;
	if (action != NULL)
		info("vplane(%d.%u): %s interface %s (%u)", vp->id, ifn,
		     action, vpif->ifname, vpif->ifindex);
	return 0;
}

void *vplane_iface_get_cookie(const vplane_t *vp, uint32_t ifn)
{
	const vplaneif_t *vpif;

	vpif = vplane_iface_get(vp, ifn, true, "iface_get_cookie");
	if (vpif != NULL)
		return vpif->cookie;

	return NULL;
}

int vplane_iface_set_cookie(const vplane_t *vp, uint32_t ifn, void *cookie)
{
	vplaneif_t *vpif;

	vpif = vplane_iface_get(vp, ifn, true, "iface_set_cookie");
	if (vpif != NULL) {
		vpif->cookie = cookie;
		return 0;
	}

	return -1;
}

bool vplane_iface_get_delpend(const vplane_t *vp, uint32_t ifn)
{
	vplaneif_t *vpif;

	vpif = vplane_iface_get(vp, ifn, true, "iface_get_delpend");
	if (vpif != NULL)
		return vpif->delpend;

	return false;
}

int vplane_iface_set_delpend(const vplane_t *vp, uint32_t ifn, bool delpend)
{
	vplaneif_t *vpif;

	vpif = vplane_iface_get(vp, ifn, true, "iface_set_delpend");
	if (vpif != NULL) {
		vpif->delpend = delpend;
		dbg("vplane(%d.%u) %s delete pending %d",
		    vp->id, ifn, vpif->ifname, delpend);
		return 0;
	}

	return -1;
}

void vplane_iface_iterate(const vplane_t *vp, vplane_iface_iter_func_t func,
			  void *arg)
{
	uint32_t iface;

	for (iface = 0; iface < MAX_PORTS; iface++) {
		vplaneif_t *vpif;

		vpif = vp->interfaces[iface];
		if (vpif != NULL)
			(func)(vp, iface, arg);
	}
}

static void
vplane_interfacechange(const vplane_t *vp, bool up)
{
	int iface;

	for (iface = 0; iface < MAX_PORTS; iface++) {
		vplaneif_t *vpif;
		uint32_t operstate;

		vpif = vp->interfaces[iface];
		if (vpif == NULL)
			continue;

		operstate = up ? IF_OPER_UP : IF_OPER_DORMANT;
		if (port_state_change(vp, iface, vpif->ifindex, operstate) == 0)
			if (operstate != vpif->operstate) {
				info("vplane(%d.%u) %s state change %u->%u",
				     vp->id, iface, vpif->ifname,
				     vpif->operstate, operstate);
				vpif->operstate = operstate;
			}
	}
}

static int
vplane_do_connect(vplane_t *vp, zframe_t *envelope, const char *how)
{
	char *sessionid;
	int rc;

	if (envelope != NULL)
		sessionid = zframe_strhex(envelope);
	else {
		sessionid = NULL;
		errno = EINVAL;
	}
	if (sessionid == NULL) {
		err("vplane(%d): cannot get session ID from envelope: %s",
		    vp->id, strerror(errno));
		return -1;
	}

	if (vp->connected) {
		info("vplane(%d): re-connecting (%s)", vp->id,
		     vp->attr[VP_ATTR_SESSIONID]);
		assert(vp == zhash_lookup(vplanesession,
					  vp->attr[VP_ATTR_SESSIONID]));
		zhash_delete(vplanesession, vp->attr[VP_ATTR_SESSIONID]);
		attr_free(vp, VP_ATTR_SESSIONID);
		if (vp->envelope) {
			zframe_destroy(&vp->envelope);
			vp->envelope = NULL;
		}
		vp->connected = false;
		vp->discon_reason = "re-connect";
		vplane_interfacechange(vp, false);
	}

	rc = zhash_insert(vplanesession, sessionid, vp);
	if (rc < 0)
		err("vplane(%d): session table insert failed: %s",
		    vp->id, strerror(errno));
	else {
		vp->clocktick = zclock_mono() + vp->timeout;
		vp->connected = true;
		vp->connects++;
		vp->discon_reason = NULL;
		if (envelope)
			vp->envelope = zframe_dup(envelope);
		vp->attr[VP_ATTR_SESSIONID] = sessionid;
		info("vplane(%d): connected (%s, %s)", vp->id, sessionid, how);
		/* Notify topology manager */
		vplane_interfacechange(vp, true);
		sessionid = NULL;
	}

	free(sessionid);
	return rc;
}

/*
 * A legacy vplane instance has connected to the controller. Using its
 * IPv4 address as a key, record the session (flow) ID.
 */
int
vplane_local_connect(vplane_t *vp, zframe_t *envelope)
{
	if (vp != NULL) {
		char addr[INET6_ADDRSTRLEN];

		return vplane_do_connect(vp, envelope,
					 inet_ntop(vp->ctladdr.af,
						   &vp->ctladdr.ip,
						   addr,
						   sizeof(addr)));
	}

	errno = EINVAL;
	return -1;
}

/*
 * A vplane instance has connected to the controller, record the
 * session (flow) ID and bring up any already defined interfaces.
 */
int
vplane_connect(vplane_t *vp, zframe_t *envelope)
{
	if (vp != NULL)
		return vplane_do_connect(vp, envelope, vp->attr[VP_ATTR_UUID]);

	errno = EINVAL;
	return -1;
}

static void
vplane_disconnect(vplane_t *vp, const char *why)
{
	zhash_delete(vplanesession, vp->attr[VP_ATTR_SESSIONID]);
	assert(NULL == zhash_lookup(vplanesession,
				    vp->attr[VP_ATTR_SESSIONID]));
	attr_free(vp, VP_ATTR_SESSIONID);
	vp->connected = false;
	vp->discon_reason = why;
	vplane_interfacechange(vp, false);
	info("vplane(%d) disconnected (%s)", vp->id, why);
}

static void
vplane_init(vplane_t *vp)
{
	memset(vp, '\0', sizeof(*vp));
	vp->id = -1;
}

/*
 * Delete a vplane descriptor. This is used directly by the following
 * code and used by the ZeroMQ hash library to destroy an instance when
 * the UUID hash table is deleted.
 */
static void
vplane_delete(vplane_t *vp, bool logging)
{
	int iface;
	vp_attr_t attr;

	if (vp == NULL)
		return;

	if (!vp->delpend)
		die("vplane(%d): unexpected delete", vp->id);

	if (vp->connected)
		die("vplane(%d): connected delete", vp->id);

	if (logging && (vp->attr[VP_ATTR_UUID] != NULL))
		info("vplane(%d): delete %s", vp->id, vp->attr[VP_ATTR_UUID]);

	for (iface = 0; iface < MAX_PORTS; iface++)
		if (vp->interfaces[iface] != NULL) {
			free(vp->interfaces[iface]);
			vp->interfaces[iface] = NULL;
		}

	if (vp->envelope)
		zframe_destroy(&vp->envelope);

	assert(vp->attr[VP_ATTR_SESSIONID] == NULL);
	for (attr = VP_ATTR_MIN; attr < VP_ATTR_MAX; attr++)
		free(vp->attr[attr]);

	vplane_init(vp);
	free(vp);
}

/*
 * Delete wrapper invoked as a callback by ZeroMQ.
 */
static void
vplane_zeromq_delete(void *arg)
{
	vplane_delete(arg, true);
}

/*
 * Add a vplane instance to the UUID hash table, i.e. the vplane is now
 * ready for use.
 */
static void
vplane_insert(zhash_t *hash, vplane_t *vp)
{
	vplane_t *tmp;

	if ((vp == NULL) ||
	    (vp->attr[VP_ATTR_UUID] == NULL))
		die("vplane_insert()");

	if (zhash_insert(hash, vp->attr[VP_ATTR_UUID], vp) < 0)
		die("vplane(%d): zhash_insert() failed: %s", vp->id,
		    strerror(errno));

	vp->delpend = false;
	vp->update_needed = true;
	tmp = zhash_freefn(hash, vp->attr[VP_ATTR_UUID],
			   vplane_zeromq_delete);
	assert(tmp == vp);
	info("vplane(%d): inserted %s", vp->id, vp->attr[VP_ATTR_UUID]);
}

/*
 * Merge new configuration information with an existing (active) vplane
 * instance.
 */
static void
vplane_merge(vplane_t *curr, vplane_t *new)
{
	vp_attr_t attr;

	curr->delpend = false;
	if (new->timeout != curr->timeout) {
		curr->timeout = new->timeout;
		curr->update_needed = true;
	}
	new->timeout = -1;

	curr->ctladdr = new->ctladdr;
	curr->id = new->id;

	for (attr = VP_ATTR_MIN; attr < VP_ATTR_MAX; attr++) {
		if (new->attr[attr] == NULL)
			continue;

		if (streq(new->attr[attr], curr->attr[attr]))
			continue;

		free(curr->attr[attr]);
		curr->attr[attr] = new->attr[attr];
		new->attr[attr] = NULL;
		curr->update_needed = true;
	}

	info("vplane(%d): merged", curr->id);
}

static vplane_t *
vplane_cfg_findbyid(int id, const char *who)
{
	vplane_t *vp;

	if (vplanecfg != NULL) {
		/*
		 * Do we have a cached copy of the last found vplane
		 * (the configuration attributes for a particular vplane
		 * appear one after another)?
		 */
		if ((vplanecfg_lastfound != NULL) &&
		    (vplanecfg_lastfound->id == id))
			return vplanecfg_lastfound;

		vplanecfg_lastfound = NULL;
		for (vp = zlist_first(vplanecfg);
		     vp != NULL;
		     vp = zlist_next(vplanecfg))
			if (vp->id == id) {
				vplanecfg_lastfound = vp;
				return vp;
			}
	}

	if (who != NULL)
		err("vplane(%d): not found (%s)", id, who);

	return NULL;
}

static int
vplane_cfg_set_string(vplane_t *vp, vp_attr_t attr, const char *value)
{
	if ((vp->attr[attr] != NULL) &&
	    (streq(vp->attr[attr], value)))
		return 0;

	free(vp->attr[attr]);
	vp->attr[attr] = strdup(value);
	if (vp->attr[attr] == NULL)
		return -1;

	return 0;
}

static int
vplane_cfg_set_ip_control_address(vplane_t *vp, const char *addr)
{
	if (1 == inet_pton(AF_INET, addr, &vp->ctladdr.ip)) {
		vp->ctladdr.af = AF_INET;
	} else if (1 == inet_pton(AF_INET6, addr, &vp->ctladdr.ip)) {
		vp->ctladdr.af = AF_INET6;
	} else {
		err("vplane(%d): invalid IP address (%s)", vp->id, addr);
		return -1;
	}
	return 0;
}

static int
vplane_cfg_set_timeout(vplane_t *vp, const char *seconds)
{
	uint32_t to;

	parse_atoi(&to, seconds);
	vp->timeout = to * 1000;
	return 0;
}

static int
vplane_cfg_set_uuid(vplane_t *vp, const char *uuid)
{
	return vplane_cfg_set_string(vp, VP_ATTR_UUID, uuid);
}

static bool
vplane_cfg_valid(vplane_t *vp)
{
	/*
	 * Use the IP address of vplaned & vplane to distinguish
	 * between a local and remote vplane.
	 *
	 * On VR there is one instance of vplaned and only one vplane. They
	 * are co-located, hence vplane is local.
	 *
	 * If this vplane is local, generate a fake UUID - it
	 * keeps the remainder of the code very much simpler.
	 */
	if (!addr_cmp(&vp->ctladdr, parser_local_addr())) {
		vp->local = true;
		vplane_cfg_set_uuid(vp, DEFAULT_UUID);
	}

	if (vp->attr[VP_ATTR_UUID] == NULL) {
		err("vplane(%d): missing UUID - ignored", vp->id);
		return false;
	}

	/*
	 * Need for compatibility with the "current" dataplane (messages
	 * are keyed using the IPv4 address). Once the
	 * vplane<->controller handshake has been implemented this can
	 * be deleted.
	 */
	if (vp->ctladdr.af == 0) {
		info("vplane(%d): missing control address - ignored", vp->id);
		return false;
	}

	if (vp->timeout == 0)
		vp->timeout = parser_controller_timeout();

	return true;
}

static vplane_t *
vplane_cfg_add(int id)
{
	vplane_t *vp;

	if (vplanecfg == NULL) {
		err("vplane(%d): cfg_add() not in configuration mode", id);
		return NULL;
	}

	vp = malloc(sizeof(*vp));
	if (vp != NULL) {
		vplane_init(vp);
		vp->id = id;
		if (zlist_append(vplanecfg, vp) < 0) {
			free(vp);
			vp = NULL;
		}
	}

	if (vp == NULL)
		err("vplane(%d): cfg_add() failed: %s", id,
		    strerror(errno));
	else {
		vp->delpend = true;
		dbg("vplane(%d): added to configuration", id);
	}

	return vp;
}

typedef int (vplane_cfg_set)(vplane_t *, const char *);

typedef struct vplane_cfg_attr_ {
	const char *name;
	vplane_cfg_set *set;
} vplane_cfg_attr_t;

/*
 * List of known/interesting attributes
 */
static vplane_cfg_attr_t vplane_attr[] = {
	{.name = "timeout", .set = vplane_cfg_set_timeout},
	{.name = "uuid", .set = vplane_cfg_set_uuid},
	{.name = "ip", .set = vplane_cfg_set_ip_control_address},
	{.name = NULL, .set = NULL}
};

/*
 * Process a vplane ("Fabric") configuration attribute. This is invoked
 * by the controller.conf parser for each vplane-related item in the
 * file.
 */
parse_result_t
vplane_cfg_set_attribute(int id, const char *name, const char *value)
{
	vplane_cfg_attr_t *attr;

	for (attr = &vplane_attr[0]; attr->name != NULL; attr++) {
		/*
		 * Are we interested in this attribute?
		 */
		if (streq(name, attr->name)) {
			vplane_t *vp;

			/*
			 * Do we have a vplane instance? If not create
			 * one.
			 */
			vp = vplane_cfg_findbyid(id, NULL);
			if (vp == NULL) {
				vp = vplane_cfg_add(id);
				if (vp == NULL)
					return PARSE_ERR;
			}

			/*
			 * Invoke the handler to process the attribute
			 */
			if ((attr->set)(vp, value) < 0)
				return PARSE_ERR;

			return PARSE_OK;
		}
	}

	return PARSE_IGNORED;
}

/*
 * Create JSON object describing an interface
 */
static json_object *
vplane_if_to_json(vplaneif_t *vpif)
{
	json_object *state;
	json_object *jobj = json_object_new_object();

	json_object_object_add(jobj, "index",
			       json_object_new_int(vpif->ifindex));

	switch (vpif->operstate) {
	case IF_OPER_UP:
		state = json_object_new_string("up");
		break;
	case IF_OPER_DORMANT:
		state = json_object_new_string("dormant");
		break;
	default:
		state = json_object_new_string("unknown");
		break;
	}
	json_object_object_add(jobj, "state", state);

	json_object_object_add(jobj, "name",
			       json_object_new_string(vpif->ifname));

	return jobj;
}

/*
 * Create JSON object describing a vplane
 */
static json_object *
vplane_to_json(vplane_t *vp)
{
	char addr[INET6_ADDRSTRLEN];
	int iface;
	json_object *jobj = json_object_new_object();

	json_object_object_add(jobj, "id", json_object_new_int(vp->id));

	json_object_object_add(jobj, "timeout",
			       json_object_new_int(vp->timeout));

	json_object_object_add(jobj, "local",
			       json_object_new_boolean(vp->local));

	json_object_object_add(jobj, "connected",
			       json_object_new_boolean(vp->connected));

	json_object_object_add(jobj, "delpend",
			       json_object_new_boolean(vp->delpend));

	if (vplane_is_local(vp)) {
		json_object_object_add(jobj, "ctladdr",
				json_object_new_string(inet_ntop(vp->ctladdr.af,
								&vp->ctladdr.ip,
								addr,
								sizeof(addr))));
	} else {
		json_object_object_add(jobj, "ctladdr",
				       json_object_new_string("-"));
	}

	if (vp->clocktick)
		json_object_object_add(jobj, "clocktick",
				 json_object_new_int64(zclock_mono() +
						       vp->timeout -
						       vp->clocktick));

	if (vp->discon_reason != NULL) {
		json_object_object_add(jobj, "discon_reason",
				json_object_new_string(vp->discon_reason));
	}

	json_object_object_add(jobj, "connects",
			       json_object_new_int(vp->connects));

	if (vp->attr[VP_ATTR_UUID] != NULL) {
		json_object_object_add(jobj, "uuid",
			json_object_new_string(vp->attr[VP_ATTR_UUID]));
	}

	if (vp->attr[VP_ATTR_SESSIONID] != NULL) {
		json_object_object_add(jobj, "sessionid",
			json_object_new_string(vp->attr[VP_ATTR_SESSIONID]));
	}

	if (vp->attr[VP_ATTR_CONTROL] != NULL) {
		json_object_object_add(jobj, "control",
			json_object_new_string(vp->attr[VP_ATTR_CONTROL]));
	}

	json_object *jarray = json_object_new_array();

	for (iface = 0; iface < MAX_PORTS; iface++) {
		vplaneif_t *vpif;

		vpif = vp->interfaces[iface];
		if (vpif == NULL)
			continue;

		json_object_array_add(jarray, vplane_if_to_json(vpif));

	}
	json_object_object_add(jobj, "interfaces", jarray);

	return jobj;
}

/*
 * Create JSON string describing the vplane database
 */
int vplane_get_json_config(const char *topic __unused, zmsg_t *msg __unused,
			   char **json)
{
	vplane_t *vp;

	if (vplaneuuid == NULL)
		return -1;

	json_object *jobj = json_object_new_object();
	json_object *jarray = json_object_new_array();

	FOR_ALL_VPLANES_BYUUID(vplaneuuid, vp) {
		json_object_array_add(jarray, vplane_to_json(vp));
	}

	json_object_object_add(jobj, "dataplanes", jarray);

	*json = strdup(json_object_to_json_string(jobj));

	/* Free JSON objects */
	json_object_put(jobj);

	return 0;
}

/*
 * Configuration parser failed for some reason, discard any instances
 */
void
vplane_cfg_failed(void)
{
	vplane_t *vp;

	err("vplane: configuration failed");
	while ((vp = zlist_pop(vplanecfg)) != NULL)
		vplane_delete(vp, false);

	zlist_destroy(&vplanecfg);
	vplanecfg_lastfound = NULL;
}

/*
 * Configuration parse process has finished, add or merge any generated
 * vplane instances.
 */
void
vplane_cfg_end(void)
{
	vplane_t *new;

	dbg("vplane: configuration update sequence beginning");
	while ((new = zlist_pop(vplanecfg)) != NULL) {
		if (vplane_cfg_valid(new)) {
			vplane_t *curr;

			curr = zhash_lookup(vplaneuuid,
					    new->attr[VP_ATTR_UUID]);
			if (curr != NULL)
				vplane_merge(curr, new);
			else {
				vplane_insert(vplaneuuid, new);
				new = NULL;
			}
		}

		vplane_delete(new, false);
	}

	zlist_destroy(&vplanecfg);
	dbg("vplane: configuration update sequence finished");
}

/*
 * The configuration parse process is about to begin, tag all known
 * vplane instances as "delete pending". Should the instance be no
 * longer defined by the configuration, it will get removed once its
 * keepalive timer expires (see below).
 */
void
vplane_cfg_begin(void)
{
	vplane_t *vp;

	dbg("vplane: configuration beginning");
	vplanecfg = zlist_new();
	vplanecfg_lastfound = NULL;
	FOR_ALL_VPLANES_BYUUID(vplaneuuid, vp)
		vp->delpend = true;
}

void
vplane_keepalive(vplane_t *vp, const char *request, uint32_t ifn)
{
	if ((vp != NULL) && vplane_is_connected(vp)) {
		vp->clocktick = zclock_mono() + vp->timeout;
		if (ifn >= MAX_PORTS)
			dbg("vplane(%d) keepalive '%s'", vp->id, request);
		else
			dbg("vplane(%d.%u) keepalive '%s' (%s)", vp->id, ifn,
			    request, vplane_iface_get_ifname(vp, ifn));
	}
}

void
vplane_tick(void)
{
	vplane_t *vp;

	assert(zlist_size(vplanedelpend) == 0);
	FOR_ALL_VPLANES_BYUUID(vplaneuuid, vp) {
		if (!vp->connected) {
			if (vp->delpend)
				zlist_append(vplanedelpend, vp);
		} else if (zclock_mono() > vp->clocktick) {
			vplane_disconnect(vp, "timeout");
			if (vp->delpend)
				zlist_append(vplanedelpend, vp);
		}
	}

	while ((vp = zlist_pop(vplanedelpend)) != NULL) {
		zhash_delete(vplaneuuid, vp->attr[VP_ATTR_UUID]);
	}

}

void
vplane_test_set_timeout(int id, int ms)
{
	vplane_t *vp;

	vp = vplane_cfg_findbyid(id, __func__);
	if (vp != NULL)
		vp->timeout = ms;
}

void
vplane_setup(void)
{
	vplaneuuid = zhash_new();
	vplanesession = zhash_new();
	vplanedelpend = zlist_new();
}

void
vplane_disconnect_all(void)
{
	vplane_t *vp;

	FOR_ALL_VPLANES_BYUUID(vplaneuuid, vp) {
		vp->delpend = true;
		if (vp->connected)
			vplane_disconnect(vp, "forced");
	}
}

void
vplane_teardown(void)
{
	assert(zhash_size(vplanesession) == 0);
	zhash_destroy(&vplaneuuid);
	zhash_destroy(&vplanesession);
	zlist_destroy(&vplanedelpend);
}
