/*\
 *	DISTRIBUTION: HNMS v2.0
 *	FILE: hnmsd/snmp.c
 *
 *	SNMP for HNMS I/O module.
 *
 *	Jude George
 *	NAS Facility, NASA Ames Research Center
 *
 *	Copyright (c) 1994 Jude George
 *
 *	This program is free software; you can redistribute it and/or modify
 *	it under the terms of the GNU General Public License as published by
 *	the Free Software Foundation; either version 1, 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 General Public License for more details.
 *
 *	You should have received a copy of the GNU General Public License
 *	along with this program; if not, write to the Free Software
 *	Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
\*/

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/ioctl.h>
#include <sys/time.h>

#include "stdhnms.h"
#include "../gen/SNMP-types.h"

#define	SNMP_PORT			161
#define	SNMP_BUFSIZ			4500

#define SNMP_PDU_GET			type_SNMP_PDUs_get__request
#define SNMP_PDU_GETNEXT		type_SNMP_PDUs_get__next__request
#define SNMP_PDU_RESPONSE		type_SNMP_PDUs_get__response
#define SNMP_PDU_SET			type_SNMP_PDUs_set__request
#define SNMP_PDU_TRAP			type_SNMP_PDUs_trap

/*\
 *  We're dividing up SNMP's request-id field into:
 *
 *	31					0
 *	OPERATION	QUERY_ID	  HNMS_ID
 *	(8 bits)	(8 bits)	(16 bits)
 *
 *  This means we can define 255 types of operations, have 255
 *  different query (GET, GETNEXT, or WALK) operations going on
 *  at once, and have an upper limit of 65535 HNMS objects.
 *  Non-query operations (DISCOVER, STATUS, SUBSCRIPTION, and SET)
 *  do not use the query-id subfield.  Certain DISCOVER operations
 *  do not use the hnms-id subfield.
\*/
#define SNMP_REQID_OP_BITS		0xFF000000
#define SNMP_REQID_QUERY_ID_BITS	0x00FF0000
#define SNMP_REQID_HNMS_ID_BITS		0x0000FFFF
#define SNMP_OP_DISCOVER		0x01000000
#define SNMP_OP_SUBSCRIPTION		0x02000000
#define SNMP_OP_SET			0x03000000
#define SNMP_OP_GET			0x04000000
#define SNMP_OP_GETNEXT			0x05000000
#define SNMP_OP_WALK			0x06000000
#define SNMP_REQID_OP(x)		(x & SNMP_REQID_OP_BITS)
#define SNMP_REQID_QUERY_ID(x)		((x & SNMP_REQID_QUERY_ID_BITS) >> 16)
#define SNMP_REQID_HNMS_ID(x)		(x & SNMP_REQID_HNMS_ID_BITS)

typedef struct type_SNMP_Message	*SMessage;
typedef struct type_SNMP_VarBindList	*SVarBindList;
typedef struct type_SNMP_VarBind	*SVarBind;

static int			snmp_sock;
static struct sockaddr_in	snmp_local_addr, snmp_remote_addr;
static char			snmp_buf[SNMP_BUFSIZ];
static PE			nullpe;

struct query {
    int			in_use;
    int			hnms_id;
    int			module_id;
    VarBindList		query;
    VarBindList		vbl;
};

static struct query	queries[255];

static char		*snmp_msg_str[] = {"", "GetRequest",
					       "GetNextRequest",
					       "GetResponse",
					       "SetRequest", "Trap"};
static char		*snmp_err_str[] = {"noError", "tooBig",
					       "noSuchName", "badValue",
					       "readOnly", "genErr"};
static char		*snmp_op_str[] = {"", "Discover OP",
					      "Subscription OP", "Set OP",
					      "Get OP", "Getnext OP",
					      "Walk OP"};
static char		*snmp_trap_str[] = {"coldStart", "warmStart",
						"linkDown", "linkUp",
						"authenticationFailure",
						"egpNeighborLoss",
						"enterpriseSpecific"};
/*\
 *  Init SNMP.
\*/
int SNMP_init()
{
    int				parm, len;

    snmp_sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (snmp_sock <= 0)
	return -1;

    /*
     * Set the socket for non-blocking I/O.
     */
    parm = 1;
    if (ioctl(snmp_sock, FIONBIO, &parm) < 0) {
	close(snmp_sock);
	return -2;
    }

    /*
     * Enable re-use of local addresses.  Don't know if this is really
     * necessary.
     */
    parm = 1;
    if (setsockopt(snmp_sock, SOL_SOCKET, SO_REUSEADDR, &parm, sizeof(int))
	< 0) {
	close(snmp_sock);
	return -3;
    }

    /*
     * Set address family, local address, local port.
     */
    bzero((char *) &snmp_local_addr, sizeof(struct sockaddr_in));
    snmp_local_addr.sin_family = AF_INET;
    snmp_local_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    snmp_local_addr.sin_port = 0;

    /*
     * Bind the socket.  bind() will choose a local port for us, since
     * we do not need to use the SNMP well-known port.
     */
    if (bind(snmp_sock, &snmp_local_addr, sizeof(struct sockaddr_in)) < 0) {
	close(snmp_sock);
	return -5;
    }

    len = sizeof(struct sockaddr_in);
    getsockname(snmp_sock, &snmp_local_addr, &len);
    HNMS_debug(DEBUG_SNMP, "SNMP listening on port %d\n",
	       snmp_local_addr.sin_port);

    /*
     * Prepare for all the sendto()'s that are to come.
     */
    snmp_remote_addr.sin_family = AF_INET;

    /*
     * Create a NULL PE that I will use fairly often.
     */
    nullpe = pe_create_null();

    return 0;
}

/*\
 *  Create an SNMP SMessage of the requested type, with the given
 *  community, request-id, error-status, and error-index.  Trap messages
 *  are not created by the I/O module.  The version field is automatically
 *  filled in for all messages.
 *  The request-id is uaually the HNMS ID of the processor being polled,
 *  but can take special values when the SNMP poll is being used for
 *  discovery rather than regular data collection.
 *
 *  Return the SMessage if successful, NULL otherwise.
\*/
static SMessage SNMP_Message_create(type, community, request_id, error_status,
			     error_index)
    const int		type;
    const char		*community;
    const int		request_id;
    const int		error_status;
    const int		error_index;
{
    SMessage		smsg;

    /*
     * Create the message header.
     */
    smsg = HNMS_alloc(1, sizeof *smsg);
    smsg->version = int_SNMP_version_version__1;
    smsg->community = str2qb(community, strlen(community), 0);

    /*
     * Create the message data section.
     */
    smsg->data = HNMS_alloc(1, sizeof *smsg->data);
    smsg->data->offset = type;
    switch (type) {
	/*
	 * All three PDU types which we use are identical at this
	 * level.  Use get-request as a template.
	 */
    case SNMP_PDU_GET:
    case SNMP_PDU_GETNEXT:
    case SNMP_PDU_SET:
	smsg->data->un.get__request =
	    HNMS_alloc(1, sizeof *smsg->data->un.get__request);
	break;

    default:;
	break;
    }

    /*
     * Fill in the PDU information.
     */
    smsg->data->un.get__request->request__id = request_id;
    smsg->data->un.get__request->error__status = error_status;
    smsg->data->un.get__request->error__index = error_index;

    return smsg;
}

/*\
 *  Set the variable-bindings area of the SNMP message with the
 *  given HNMP VarBindList.  The HNMP VarBindList is converted
 *  to SNMP VarBindList format.  Don't use more variables than can
 *  fit into one SNMP packet.  Return 0, or -1 if the operation
 *  could not be completed.
\*/
static int SNMP_Message_set_vbl(smsg, vbl)
    SMessage		smsg;
    const VarBindList	vbl;
{
    VarBindList		v;
    SVarBindList	svbl, sv, prev;
    SVarBind		svb;

    svbl = 0;
    prev = 0;
    for (v = vbl; v; v = v->next) {
	sv = HNMS_alloc(1, sizeof *sv);
	svb = HNMS_alloc(1, sizeof *svb);
	sv->VarBind = svb;
	sv->VarBind->name = oid_cpy(v->VarBind->name);
	if (v->VarBind->value)
	    sv->VarBind->value = pe_cpy(v->VarBind->value);
	else
	    sv->VarBind->value = pe_create_null();
	sv->next = NULL;
	if (prev)
	    prev->next = sv;
	else
	    svbl = sv;
	prev = sv;
    }
    if (!svbl)
	return -1;

    smsg->data->un.get__request->variable__bindings = svbl;
    return 0;
}

/*\
 *  Get the variable-bindings from the SNMP message as an HNMP
 *  VarBindList.  The SNMP VarBindList is converted to HNMP
 *  VarBindList format.  Space is created for the HNMP VarBindList.
 *  The timestamp of each variable is set to the current time.
 *  Return the VarBindList, or NULL if the operation could not be
 *  completed.
\*/
static VarBindList SNMP_Message_get_vbl(smsg)
    const SMessage	smsg;
{
    VarBindList		vbl, v, prev;
    VarBind		vb;
    SVarBindList	sv;
    unsigned int	current_time;

    current_time = get_int_time();
    vbl = 0;
    prev = 0;
    if (smsg->data->offset == SNMP_PDU_TRAP)
	sv = smsg->data->un.trap->variable__bindings;
    else
	sv = smsg->data->un.get__response->variable__bindings;
    for (; sv; sv = sv->next) {
	v = HNMS_alloc(1, sizeof *v);
	vb = HNMS_alloc(1, sizeof *vb);
	vb->name = oid_cpy(sv->VarBind->name);
	vb->value = pe_cpy(sv->VarBind->value);
	vb->timestamp = HNMS_alloc(1, sizeof *vb->timestamp);
	/*
	 * For each variable, the timestamp is set to the current time.
	 */
	vb->timestamp->parm = current_time;
	v->VarBind = vb;
	v->next = NULL;
	if (prev)
	    prev->next = v;
	else
	    vbl = v;
	prev = v;
    }
    if (!vbl)
	return NULL;
    
    return vbl;
}

/*\
 *  Encode and send an SNMP SMessage to the given address/port.
 *  This function free()'s the message.
 *  Return 0, or -1 if the operation could not be completed.
 \*/
static int SNMP_Message_send(smsg, ip_address, udp_port)
    SMessage		smsg;
    const unsigned int	ip_address, udp_port;
{
    PS			ps1;
    PE			*pe1;
    int			len, rval;
    
    /*
     * Encode the message structs into a presentation element and
     * get its encoded length; malloc a character buffer of that size.
     */
    rval = encode_SNMP_Message(&pe1, 1, 0, NULLVP, smsg);
    len = ps_get_abs(pe1);
    if (len > SNMP_BUFSIZ)
	HNMS_debug(DEBUG_SNMP, "snmp creating a message that's too long");
    /*
     * Get a presentation stream and associate the character buffer
     * with the stream.  Serialize the presentation element into the
     * buffer.  We can then do away with the presentation element
     * and the presentation stream.
     */
    if ((ps1 = ps_alloc(str_open)) == NULLPS) {
	pe_free(ps1);
	free_SNMP_Message(smsg);
	HNMS_debug(DEBUG_SNMP, "snmp error making presentation stream");
	hnms_errno = HNMS_err_presentation_stream;
	return -1;
    }
    bzero(snmp_buf, SNMP_BUFSIZ);
    if (str_setup(ps1, snmp_buf, HNMP_BUFSIZ, 1) == NOTOK) {
	pe_free(pe1);
	ps_free(ps1);
	free_SNMP_Message(smsg);
	HNMS_debug(DEBUG_SNMP, "snmp error making presentation stream");
	hnms_errno = HNMS_err_presentation_stream;
	return -1;
    }
    rval = pe2ps(ps1, pe1);
    pe_free(pe1);
    ps_free(ps1);
    
    /*
     * Send the message.
     */
    snmp_remote_addr.sin_addr.s_addr = htonl(ip_address);
    snmp_remote_addr.sin_port = htons(udp_port);
    if (sendto(snmp_sock, snmp_buf, len, 0, &snmp_remote_addr,
	       sizeof(struct sockaddr_in)) < 0) {
	hnms_errno = HNMS_err_sendto;
	HNMS_debug(DEBUG_SNMP, "SNMP_Message_send", len);
    }
    free_SNMP_Message(smsg);
    return 0;
}

/*\
 *  Send an SNMP PDU to an IP address.  Specify the type of PDU,
 *  SNMP community name, variable list (in HNMP variable format),
 *  and request-id.
 \*/
static void SNMP_send(ip_address, community, vbl, type, reqid)
    const unsigned int	ip_address;
    const char		*community;
    const VarBindList	vbl;
    const int		type;
    const unsigned int	reqid;
{
    SMessage		smsg;

    HNMS_debug(DEBUG_SNMP, "SNMP: %s to %x, %s for object %d\n",
	       snmp_msg_str[type], ip_address,
	       snmp_op_str[reqid >> 24],
	       SNMP_REQID_HNMS_ID(reqid));
    
    smsg = SNMP_Message_create(type, community, reqid,
			       int_SNMP_error__status_noError, 0);
    SNMP_Message_set_vbl(smsg, vbl);
    SNMP_Message_send(smsg, ip_address, SNMP_PORT);
}

/*\
 *  Allocate a query id.  Each query operation (GET, GETNEXT, WALK)
 *  currently in progress has its own query id.  The IO module can
 *  handle up to 255 queries at a time (255 dictated by the 8 bits
 *  we give to the query-id subfield we make in SNMP's request-id field.
 *  Return the query id, or -1 if one couldn't be allocated.
\*/
int SNMP_allocate_query_id()
{
    static int		initialized = 0;
    int			query_id;
    
    if (!initialized) {
	initialized = 1;
	bzero(queries, 255 * sizeof(struct query));
    }

    for (query_id = 0; query_id < 256; query_id++)
	if (!queries[query_id].in_use) {
	    queries[query_id].in_use = 1;
	    return query_id;
	}

    return -1;
}

/*\
 *  Initiate a SET operation.  If this is not an Agent object, look
 *  for its parent Agent.  Do not perform SNMP SETs on HNMS variables.
\*/
void SNMP_set(id, vbl)
    const int		id;
    const VarBindList	vbl;
{
    int			class;
    int			agent_id;
    VarBindList		v, vbl_out;
    unsigned int	ip_address;
    static char		community[STRBUFLEN];
    
    OBJ_get(id, oid_hnmsObjClass, &class, 0, 0, 0, 0);
    if (class == OBJ_agent)
	agent_id = id;
    else
	agent_id = OBJ_get_agent_id(id);
    if (!agent_id)
	return;
    
    bzero(community, STRBUFLEN);
    if (!OBJ_get(agent_id, oid_hnmsAgentIpaddr, &ip_address, 0, 0, 0, 0) ||
	!OBJ_get(agent_id, oid_hnmsAgentCommunity, community, 0, 0, 0, 0))
	return;

    vbl_out = NULL;
    for (v = vbl; v; v = v->next)
	if (!oid_is_hnms(v->VarBind->name))
	    VarBindList_set(&vbl_out, v->VarBind->name,
			    nullpe, 0, 0, 0, 0, 0);
    if (!vbl_out)
	return;
	
    SNMP_send(ip_address, community, vbl_out, SNMP_PDU_SET, SNMP_OP_SET);
    VarBindList_free(vbl_out);
}

/*\
 *  Initiate a GET operation.
\*/
void SNMP_get(hnms_id, vbl, module_id)
    const int		hnms_id;
    const VarBindList	vbl;
    const int		module_id;
{
    int			class;
    int			agent_id;
    int			query_id;
    unsigned int	ip_address;
    static char		community[STRBUFLEN];

    OBJ_get(hnms_id, oid_hnmsObjClass, &class, 0, 0, 0, 0);
    if (class == OBJ_agent)
	agent_id = hnms_id;
    else
	agent_id = OBJ_get_agent_id(hnms_id);
    if (!agent_id)
	return;

    bzero(community, STRBUFLEN);
    if (!OBJ_get(agent_id, oid_hnmsAgentIpaddr, &ip_address, 0, 0, 0, 0) ||
	!OBJ_get(agent_id, oid_hnmsAgentCommunity, community, 0, 0, 0, 0))
	return;

    query_id = SNMP_allocate_query_id();
    if (query_id == -1)
	return;
    queries[query_id].hnms_id = hnms_id;
    queries[query_id].module_id = module_id;
    queries[query_id].query = VarBindList_copylist(vbl);
    queries[query_id].vbl = NULL;

    bzero(community, STRBUFLEN);
    if (OBJ_get(agent_id, oid_hnmsAgentIpaddr, &ip_address, 0, 0, 0, 0) &&
	OBJ_get(agent_id, oid_hnmsAgentCommunity, community, 0, 0, 0, 0)) {
	SNMP_send(ip_address, community, vbl, SNMP_PDU_GET,
		  SNMP_OP_GET | (query_id << 16) | hnms_id);
    }
}

/*\
 *  Initiate a GETNEXT operation.
\*/
void SNMP_getnext(hnms_id, vbl, module_id)
    const int		hnms_id;
    const VarBindList	vbl;
    const int		module_id;
{
    int			class;
    int			agent_id;
    int			query_id;
    unsigned int	ip_address;
    static char		community[STRBUFLEN];

    OBJ_get(hnms_id, oid_hnmsObjClass, &class, 0, 0, 0, 0);
    if (class == OBJ_agent)
	agent_id = hnms_id;
    else
	agent_id = OBJ_get_agent_id(hnms_id);
    if (!agent_id)
	return;
    
    bzero(community, STRBUFLEN);
    if (!OBJ_get(agent_id, oid_hnmsAgentIpaddr, &ip_address, 0, 0, 0, 0) ||
	!OBJ_get(agent_id, oid_hnmsAgentCommunity, community, 0, 0, 0, 0))
	return;

    query_id = SNMP_allocate_query_id();
    if (query_id == -1)
	return;
    queries[query_id].hnms_id = hnms_id;
    queries[query_id].module_id = module_id;
    queries[query_id].query = VarBindList_copylist(vbl);
    queries[query_id].vbl = NULL;

    bzero(community, STRBUFLEN);
    if (OBJ_get(agent_id, oid_hnmsAgentIpaddr, &ip_address, 0, 0, 0, 0) &&
	OBJ_get(agent_id, oid_hnmsAgentCommunity, community, 0, 0, 0, 0)) {
	SNMP_send(ip_address, community, vbl, SNMP_PDU_GETNEXT,
		  SNMP_OP_GETNEXT | (query_id << 16) | hnms_id);
    }
}

/*\
 *  Initiate a WALK operation.  We walk the tree starting from the
 *  first variable in the vbl.  Ignore any other variables -- we
 *  don't want to shoot ourselves by walking multiple vars in one query.
\*/
void SNMP_walk(hnms_id, vbl, module_id)
    const int		hnms_id;
    const VarBindList	vbl;
    const int		module_id;
{
    int			class;
    int			agent_id;
    int			query_id;
    VarBindList		vbl2;
    unsigned int	ip_address;
    static char		community[STRBUFLEN];

    OBJ_get(hnms_id, oid_hnmsObjClass, &class, 0, 0, 0, 0);
    if (class == OBJ_agent)
	agent_id = hnms_id;
    else
	agent_id = OBJ_get_agent_id(hnms_id);
    if (!agent_id)
	return;

    bzero(community, STRBUFLEN);
    if (!OBJ_get(agent_id, oid_hnmsAgentIpaddr, &ip_address, 0, 0, 0, 0) ||
	!OBJ_get(agent_id, oid_hnmsAgentCommunity, community, 0, 0, 0, 0))
	return;

    query_id = SNMP_allocate_query_id();
    if (query_id == -1)
	return;

    vbl2 = NULL;
    VarBindList_set(&vbl2, vbl->VarBind->name, nullpe, 0, 0, 0, 0);
    queries[query_id].hnms_id = hnms_id;
    queries[query_id].module_id = module_id;
    queries[query_id].query = VarBindList_copylist(vbl);
    queries[query_id].vbl = NULL;

    bzero(community, STRBUFLEN);
    if (OBJ_get(agent_id, oid_hnmsAgentIpaddr, &ip_address, 0, 0, 0, 0) &&
	OBJ_get(agent_id, oid_hnmsAgentCommunity, community, 0, 0, 0, 0)) {
	SNMP_send(ip_address, community, vbl2, SNMP_PDU_GETNEXT,
		  SNMP_OP_WALK | (query_id << 16) | hnms_id);
    }
    VarBindList_free(vbl2);
}

/*\
 *  Initiate a DISCOVER operation.  Given an Agent, try to discover
 *  the following objects:
 *  a Processor, a Site, an Administrator, Ipaddrs, and Interfaces.
\*/
static void SNMP_discover_from_agent(id)
    const int		id;
{
    int			class;
    unsigned int	dbi;
    unsigned int	time, last_time;
    unsigned int	ip_address;
    char		community[STRBUFLEN];
    VarBindList		vbl;
    
    OBJ_get(id, oid_hnmsObjClass, &class, 0, 0, 0, 0, 0);
    if (class != OBJ_agent)
	return;
    dbi = PARAM_get_int(oid_hnmsModuleIoDiscoverInterval);
    last_time = 0;
    OBJ_get(id, oid_hnmsAgentLastDiscoverTime, &last_time, 0, 0, 0, 0, 0);
    time = get_int_time();
    if (time - last_time < dbi)
	return;
    OBJ_set(id, oid_hnmsAgentLastDiscoverTime,
	    &time, 0, MIB_timeticks, 0, 0, &time);
    bzero(community, STRBUFLEN);
    /*
     * We send a GetRequest with the variables needed to make a Site,
     * Processor, and administrator.  We send a GetNextRequest with
     * the variables needed to make an Ipaddr and Interface.  The
     * sysContact in the latter comes back as a sysName.
     */
    if (OBJ_get(id, oid_hnmsAgentIpaddr, &ip_address, 0, 0, 0, 0, 0) &&
	OBJ_get(id, oid_hnmsAgentCommunity, community, 0, 0, 0, 0, 0)) {
	vbl = NULL;
	VarBindList_set(&vbl, oid_sysLocation, nullpe, 0, 0, 0, 0, 0);
	VarBindList_set(&vbl, oid_sysName, nullpe, 0, 0, 0, 0, 0);
	VarBindList_set(&vbl, oid_sysContact, nullpe, 0, 0, 0, 0, 0);
	SNMP_send(ip_address, community, vbl, SNMP_PDU_GET,
		  SNMP_OP_DISCOVER | id);
	VarBindList_free(vbl);
	vbl = NULL;
	VarBindList_set(&vbl, oid_sysContact, nullpe, 0, 0, 0, 0, 0);
	VarBindList_set(&vbl, oid_ifIndex, nullpe, 0, 0, 0, 0, 0);
	VarBindList_set(&vbl, oid_ipAdEntAddr, nullpe, 0, 0, 0, 0, 0);
	VarBindList_set(&vbl, oid_ipAdEntNetMask, nullpe, 0, 0, 0, 0, 0);
	VarBindList_set(&vbl, oid_ipAdEntIfIndex, nullpe, 0, 0, 0, 0, 0);
	SNMP_send(ip_address, community, vbl, SNMP_PDU_GETNEXT,
		  SNMP_OP_DISCOVER | id);
	VarBindList_free(vbl);
    }
}

/*\
 *  Initiate a SUBSCRIPTION operation.  Get subscription vars for an
 *  object, if it is time to do so.
 \*/
static void SNMP_poll_subscribed_vars(hnms_id)
    const int		hnms_id;
{
    int			agent_id;
    unsigned int	ip_address;
    char		community[STRBUFLEN];
    VarBindList		v,vbl, vbl2;
    int			c;
    unsigned int	time;
    
    /*
     * Get the current polling address and SNMP community string for
     * this object's agent.
     */
    agent_id = OBJ_get_agent_id(hnms_id);
    if (!agent_id)
	return;
    ip_address = 0;
    bzero(community, STRBUFLEN);
    OBJ_get(agent_id, oid_hnmsAgentIpaddr, &ip_address, 0, 0, 0, 0);
    OBJ_get(agent_id, oid_hnmsAgentCommunity, community, 0, 0, 0, 0);
    if (ip_address == 0 || strlen(community) == 0)
	return;
    
    time = get_int_time();
    
    /*
     * Put no more than 4 vars in each packet.  SNMP agents sometimes
     * barf on more than 4.
     */
    vbl = PEER_get_sub_out(hnms_id);
    
    vbl2 = 0;
    for (v = vbl, c = 1; v; v = v->next, c++) {
	if ((time - (unsigned int)v->VarBind->timestamp->parm) >=
	    (unsigned int)v->VarBind->interval) {
	    v->VarBind->timestamp->parm = time;
	    VarBindList_set(&vbl2, v->VarBind->name, nullpe, 0, 0, 0, 0);
	}
	if (c > 4) {
	    c = 1;
	    SNMP_send(ip_address, community, vbl2, SNMP_PDU_GET,
		      SNMP_OP_SUBSCRIPTION | hnms_id);
	    VarBindList_free(vbl2);
	    vbl2 = 0;
	}
    }
    if (vbl2) {
	SNMP_send(ip_address, community, vbl2, SNMP_PDU_GET,
		  SNMP_OP_SUBSCRIPTION | hnms_id);
	VarBindList_free(vbl2);
    }
}

/*\
 *  Process the reply to a DISCOVER operation.  Possibly create
 *  a Processor, Site, Administrator, Ipaddr, or Interface, or any/all
 *  of the above.  Build in the hnmsObjPhysParent relations.
 *
 *  If I received an entry from an ipAddrTable or IfTable,
 *  send out a discover poll for the next entry in that table.
\*/
static void SNMP_process_discover(vbl, agent_ipaddr, agent_community, agent_id)
    const VarBindList	vbl;
    const unsigned int	agent_ipaddr;
    const char		*agent_community;
    const int		agent_id;
{
    int			rval;
    int			id;
    char		*cp;
    PE			agent_pe;
    unsigned int	time;
    static char		sysname[STRBUFLEN];
    static char		syslocation[STRBUFLEN];
    static char		syscontact[STRBUFLEN];
    static char		latitude[STRBUFLEN];
    static char		longitude[STRBUFLEN];
    static char		label[STRBUFLEN];
    int			len;
    unsigned int	if_index;
    unsigned int	parent_proc;
    static char		ip_addr_str[STRBUFLEN];
    unsigned int	ip_address, mask;
    static char		buf[STRBUFLEN];
    PE			pe1, pe2, pe3;
    OID			noid1, noid2, noid3;
    int			int_tmp;
    VarBindList		vbl_tmp, vbl_out;
    
    time = get_int_time();
    agent_pe = OBJ_get(agent_id, oid_hnmsObjUName, 0, 0, 0, 0, 0);
    if (!agent_pe)
	return;
    
    bzero(syslocation, STRBUFLEN);
    bzero(sysname, STRBUFLEN);
    bzero(syscontact, STRBUFLEN);
    VarBindList_get(vbl, oid_sysLocation, syslocation, 0, 0, 0, 0);
    VarBindList_get(vbl, oid_sysName, sysname, 0, 0, 0, 0);
    VarBindList_get(vbl, oid_sysContact, syscontact, 0, 0, 0, 0);
    
    /*
     * So we don't have to go hunting for physParents, every DISCOVER
     * operation has sysName in it.  This saves a lot of CPU time
     * and takes up only a few more bytes per operation.
     */
    if (sysname[0] == 0)
	return;
    
    /*
     * Prepare to ask for additional vars in another DISCOVER, initiated
     * by this response.
     */
    vbl_out = NULL;
    
    /*
     * Create a Site.
     * Set it's physParent to be the Internet.
     */
    if (syslocation[0] != 0) {
	rval = OBJ_add(0, OBJ_site, syslocation);
	id = ABS(rval);
	if (id) {
	    bzero(longitude, STRBUFLEN);
	    bzero(latitude, STRBUFLEN);
	    rval = sscanf(syslocation, "%[^-]%s %s", buf, longitude, latitude);
	    if (rval == 0)
		rval = sscanf(syslocation, "%s %s", longitude, latitude);
	    if ((atoi(longitude) != 0) && (atoi(latitude) != 0)) {
		sprintf(buf, "%s %s", longitude, latitude);
		OBJ_set(id, oid_hnmsSiteLocation, buf,
			strlen(buf), MIB_octetstring, 0, &time);
	    }
	    cp = OBJ_make_uname(OBJ_internet, "0.0.0.0");
	    OBJ_set(id, oid_hnmsObjPhysParent, cp, strlen(cp),
		    MIB_octetstring, 0, &time);
	    OBJ_set(id, oid_hnmsObjAgent, agent_pe, 0, 0, 0, &time);
	}
    }
    
    /*
     * Create a Processor.
     * Set it's physParent to be the above Site.
     */
    if (sysname[0] != 0) {
	rval = OBJ_add(0, OBJ_processor, sysname);
	id = ABS(rval);
	len = strlen(sysname);
	OBJ_set(id, oid_sysName, sysname, len, MIB_octetstring, 0, &time);
	bzero(label, STRBUFLEN);
	strcpy(label, sysname);
	for (cp = label; cp < label + len; cp++)
	    if (*cp == '.')
		*cp = 0;
	OBJ_set(id, oid_hnmsObjLabel,
		label, strlen(label), MIB_octetstring, 0, &time);
	cp = OBJ_make_uname(OBJ_site, syslocation);
	if (cp)
	    OBJ_set(id, oid_hnmsObjPhysParent,
		    cp, strlen(cp), MIB_octetstring, 0, &time);
	OBJ_set(id, oid_hnmsObjAgent, agent_pe, 0, 0, 0, &time);
    }
    
    /*
     * Create an Administrator.
     * Set it's physParent to be the above Site.
     */
    if (syscontact[0] != 0) {
	rval = OBJ_add(0, OBJ_administrator, syscontact);
	id = ABS(rval);
	cp = OBJ_make_uname(OBJ_site, syslocation);
	if (cp)
	    OBJ_set(id, oid_hnmsObjPhysParent,
		    cp, strlen(cp), MIB_octetstring, 0, &time);
	OBJ_set(id, oid_hnmsObjAgent, agent_pe, 0, 0, 0, &time);
    }
    
    /*
     * Create an Interface, and ask for the next one in the table.
     * We use the sysName of the interface's host in conjunction with
     * it's ifIndex to make it's unique name.  In a perfect world,
     * we would just use ifPhysAddr and be done with it, since MAC-layer
     * addresses are supposed to be unique.
     * Set it's physParent to be the above Processor.
     */
    if_index = 0;
    noid1 = NULL;
    pe1 = VarBindList_getnext(vbl, oid_ifIndex, &noid1, 0, 0, 0, 0, 0);
    if (pe1 && (oid_cmp_n(oid_ifIndex, noid1, 10) == 0))
	if_index = prim2num(pe1);
    if (if_index != 0) {
	sprintf(buf, "%s_%d", sysname, if_index);
	rval = OBJ_add(0, OBJ_interface, buf);
	id = ABS(rval);
	OBJ_set(id, oid_hnmsIfProcIfIndex,
		&if_index, 0, MIB_integer, 0, &time);
	cp = OBJ_make_uname(OBJ_processor, sysname);
	if (cp)
	    OBJ_set(id, oid_hnmsObjPhysParent,
		    cp, strlen(cp), MIB_octetstring, 0, &time);
	OBJ_set(id, oid_hnmsObjAgent, agent_pe, 0, 0, 0, &time);
	OBJ_set(id, oid_hnmsObjAgent, agent_pe, 0, 0, 0, &time);
    }
    if (noid1 && if_index)
	VarBindList_set(&vbl_out, noid1, nullpe, 0, 0, 0, 0);

    /*
     * Create an Ipaddr, and ask for the next one in the table.
     * We use both the IP address and the mask to make it's unique name.
     * We tack on the ifIndex associated with the IP address because
     * it's useful for doing actual network management.  Ignore loopback
     * addresses.
     * Set it's physParent to be the above Processor.
     */
    ip_address = 0;
    mask = 0;
    if_index = 0;
    noid1 = noid2 = noid3 = NULL;
    pe1 = VarBindList_getnext(vbl, oid_ipAdEntAddr, &noid1, 0, 0, 0, 0, 0);
    pe2 = VarBindList_getnext(vbl, oid_ipAdEntNetMask, &noid2, 0, 0, 0, 0, 0);
    pe3 = VarBindList_getnext(vbl, oid_ipAdEntIfIndex, &noid3, 0, 0, 0, 0, 0);
    if (pe1 && pe2 && pe3 &&
	(oid_cmp_n(oid_ipAdEntAddr, noid1, 10) == 0) &&
	(oid_cmp_n(oid_ipAdEntNetMask, noid2, 10) == 0) &&
	(oid_cmp_n(oid_ipAdEntIfIndex, noid3, 10) == 0)) {
	    ip_address = ipaddr_pe2int(pe1);
	    mask = ipaddr_pe2int(pe2);
	    if_index = prim2num(pe3);
	}
    if (ip_address != 0 && ip_address != 0x7f000001 && mask != 0) {
	strcpy(ip_addr_str, ipaddr_int2str(ip_address));
	sprintf(buf, "%s_%s", ip_addr_str, ipaddr_int2str(mask));
	rval = OBJ_add(0, OBJ_ipaddr, buf);
	id = ABS(rval);
	OBJ_set(id, oid_hnmsObjIpaddr, &ip_address, 0, MIB_ipaddr, 0, &time);
	OBJ_set(id, oid_hnmsObjMask, &mask, 0, MIB_ipaddr, 0, &time);
	OBJ_set(id, oid_hnmsIpaddrIfIndex,
		&if_index, 0, MIB_integer, 0, &time);
	cp = OBJ_make_uname(OBJ_processor, sysname);
	if (cp)
	    OBJ_set(id, oid_hnmsObjPhysParent,
		    cp, strlen(cp), MIB_octetstring, 0, &time);
	OBJ_set(id, oid_hnmsObjAgent, agent_pe, 0, 0, 0, &time);
    }
    if (noid1 && ip_address && noid2 && mask) {
	VarBindList_set(&vbl_out, oid_sysContact, nullpe, 0, 0, 0, 0);
	VarBindList_set(&vbl_out, noid1, nullpe, 0, 0, 0, 0);
	VarBindList_set(&vbl_out, noid2, nullpe, 0, 0, 0, 0);
    }
    if (noid3 && if_index) {
	VarBindList_set(&vbl_out, oid_sysContact, nullpe, 0, 0, 0, 0);
	VarBindList_set(&vbl_out, noid3, nullpe, 0, 0, 0, 0);
    }

    /*
     * Now send out the request for the next set of variables.
     */
    if (vbl_out) {
	SNMP_send(agent_ipaddr, agent_community, vbl_out, SNMP_PDU_GETNEXT,
		  SNMP_OP_DISCOVER | agent_id);
	VarBindList_free(vbl_out);
    }
}

/*\
 *  Process the reply to a GET operation.
\*/
static void SNMP_process_get(vbl, hnms_id, query_id)
    const VarBindList		vbl;
    const int			hnms_id;
    const int			query_id;
{
    if (queries[query_id].in_use == 0 ||
	queries[query_id].hnms_id != hnms_id)
	return;

    PEER_send_data(hnms_id, vbl, queries[query_id].module_id);
    VarBindList_free(queries[query_id].query);
    bzero(&(queries[query_id]), sizeof(struct query));
}

/*\
 *  Process the reply to a GETNEXT operation.
\*/
static void SNMP_process_getnext(vbl, hnms_id, query_id)
    const VarBindList		vbl;
    const int			hnms_id;
    const int			query_id;
{
    if (queries[query_id].in_use == 0 ||
	queries[query_id].hnms_id != hnms_id)
	return;

    PEER_send_data(hnms_id, vbl, queries[query_id].module_id);
    VarBindList_free(queries[query_id].query);
    bzero(&(queries[query_id]), sizeof(struct query));
}

/*\
 *  Process the reply to a WALK operation.  If the walk continues,
 *  send out a GetNext for the next entry in the walk.
\*/
static void SNMP_process_walk(vbl, ip_address, community, hnms_id, query_id)
    const VarBindList		vbl;
    unsigned int		ip_address;
    char			*community;
    const int			hnms_id;
    const int			query_id;
{
    if (queries[query_id].in_use == 0 ||
	queries[query_id].hnms_id != hnms_id)
	return;

    if (!oid_cmp_n(vbl->VarBind->name, queries[query_id].query->VarBind->name,
		   queries[query_id].query->VarBind->name->oid_nelem)) {
	VarBindList_set(&(queries[query_id].vbl), vbl->VarBind->name,
			vbl->VarBind->value, 0, 0, 0,
			&(vbl->VarBind->timestamp->parm));
    	SNMP_send(ip_address, community, vbl, SNMP_PDU_GETNEXT,
		  SNMP_OP_WALK | (query_id << 16) | hnms_id);
    }
    else {
	PEER_send_data(hnms_id, queries[query_id].vbl,
		       queries[query_id].module_id);
	VarBindList_free(queries[query_id].query);
	VarBindList_free(queries[query_id].vbl);
	bzero(&(queries[query_id]), sizeof(struct query));
    }
}

/*\
 *  Process the reply to a SUBSCRIPTION operation.
\*/
static void SNMP_process_subscription(vbl, hnms_id)
    const VarBindList		vbl;
    const int			hnms_id;
{
    OBJ_set_list(hnms_id, vbl);
}

/*\
 *  Process a TRAP.
\*/
static void SNMP_process_trap(oid, ip_address, community, gen, spec, ts, vbl)
    OID				oid;
    unsigned int		ip_address;
    const char			community;
    int				gen;
    int				spec;
    unsigned int		ts;
    const VarBindList		vbl;
{
    char			*cp;
    int				hnms_id;
    int				n;
    VarBindList			v;
    unsigned int		current_time;
    static char			buf[STRBUFLEN];

    cp = ipaddr_int2str(ip_address);
    hnms_id = OBJ_find(OBJ_make_uname(OBJ_agent, cp));
    if (!hnms_id)
	return;

    bzero(buf, STRBUFLEN);
    cp = MIB_oid2ode(oid, NULL);
    current_time = get_int_time();
    for (v = vbl, n = 0; v; v = v->next, n++);
    sprintf(buf, "%s TRAP spec=%d oid=%s [%d variables]",
	    gen <= 6 ? snmp_trap_str[gen] : "malformed", cp ? cp : "", n);
    OBJ_set(hnms_id, oid_hnmsObjLastEvent,
	    buf, strlen(buf), MIB_displaystring, 0, &current_time);
}

/*\
 *  Process an SNMP message that I've received.
\*/
static void SNMP_Message_process(ip_address, udp_port, smsg)
    const unsigned int		ip_address, udp_port;
    const SMessage		smsg;
{
    unsigned int		reqid;
    unsigned int		hnms_id;
    int				query_id;
    VarBindList			vbl;
    char			*community;
    int				community_len;

    switch (smsg->data->offset) {
    case SNMP_PDU_RESPONSE:
	reqid = smsg->data->un.get__response->request__id;
	hnms_id = SNMP_REQID_HNMS_ID(reqid);
	query_id = SNMP_REQID_QUERY_ID(reqid);
	if (smsg->data->un.get__request->error__status !=
	    int_SNMP_error__status_noError) {
	    HNMS_debug(DEBUG_SNMP, "SNMP: error (%s); reqid is %x\n",
		       snmp_err_str[smsg->data->un.get__request->
				    error__status], reqid);
	    return;
	}
	/*
	 * Convert the SNMP VarBindList to HNMP VarBindList format.
	 * The timestamp-in field will be set to the current time.
	 */
	vbl = SNMP_Message_get_vbl(smsg);
	if (!vbl)
	    return;
	/*
	 * Use the flag bits in the request-id field (which I put there
	 * when sending out the original requests) to determine what type
	 * of request this packet is a response to.
	 */
	community = qb2str(smsg->community, &community_len);
	if (!community) {
	    VarBindList_free(vbl);
	    return;
	}
	switch (SNMP_REQID_OP(reqid)) {
	case SNMP_OP_DISCOVER:
	    HNMS_debug(DEBUG_SNMP,
		       "SNMP: DISCOVER response from Agent id %d\n", hnms_id);
	    SNMP_process_discover(vbl, ip_address, community, hnms_id);
	    break;
	    
	case SNMP_OP_GET:
	    HNMS_debug(DEBUG_SNMP,
		       "SNMP: GET response from Agent id %d\n", hnms_id);
	    SNMP_process_get(vbl, hnms_id, query_id);
	    break;
	    
	case SNMP_OP_GETNEXT:
	    HNMS_debug(DEBUG_SNMP,
		       "SNMP: GETNEXT response from Agent id %d\n", hnms_id);
	    SNMP_process_getnext(vbl, hnms_id, query_id);
	    break;
	    
	case SNMP_OP_WALK:
	    HNMS_debug(DEBUG_SNMP,
		       "SNMP: WALK response from Agent id %d\n", hnms_id);
	    SNMP_process_walk(vbl, ip_address, community, hnms_id, query_id);
	    break;
	    
	case SNMP_OP_SUBSCRIPTION:
	    HNMS_debug(DEBUG_SNMP,
		       "SNMP: SUBSCRIPTION response from Agent id %d\n",
		       hnms_id);
	    SNMP_process_subscription(vbl, hnms_id);
	    break;
	    
	default:;
	}
	VarBindList_free(vbl);
	free(community);
	break;

    case SNMP_PDU_TRAP:
	HNMS_debug(DEBUG_SNMP, "SNMP: TRAP from ip address %x\n", ip_address);
	vbl = SNMP_Message_get_vbl(smsg);
	SNMP_process_trap(smsg->data->un.trap->enterprise,
			  ip_address,
			  community,
			  smsg->data->un.trap->generic__trap,
			  smsg->data->un.trap->specific__trap,
			  smsg->data->un.trap->time__stamp->parm,
			  vbl);
	free(community);
	VarBindList_free(vbl);
	break;

    default:;
    }
}

/*\
 *  Decode an incoming SNMP PDU.  The message may be:
 *  
 *	A response to one of my DISCOVER operations.
 *	A response to one of my SUBSCRIPTION operations.
 *	A response to one of my QUERY (GET, GETNEXT, or WALK) operations.
 *	An unsolicited trap.
\*/
static void SNMP_recv()
{
    unsigned int	ip_address, udp_port;
    int			remote_len;
    PS			ps1;
    PE			pe1;
    SMessage		smsg;
    int			rval;

    remote_len = sizeof(struct sockaddr_in);

    /*
     * Retrieve the message into a buffer.
     */
    bzero(snmp_buf, SNMP_BUFSIZ);
    if ((rval = recvfrom(snmp_sock, snmp_buf, SNMP_BUFSIZ, 0,
			 &snmp_remote_addr, &remote_len)) < 0)
	HNMS_debug(DEBUG_SNMP, "SNMP RECVFROM");

    ip_address = ntohl(snmp_remote_addr.sin_addr.s_addr);
    udp_port = ntohs(snmp_remote_addr.sin_port);

    /*
     * Get a presentation stream, and associate the SNMP buffer with
     * the presentation stream.
     */
    if ((ps1 = ps_alloc(str_open)) == NULLPS) {
	hnms_errno = HNMS_err_presentation_stream;
	HNMS_debug(DEBUG_SNMP, "SNMP_recv");
	return;
    }
    if (str_setup(ps1, snmp_buf, SNMP_BUFSIZ, 1) == NOTOK) {
	hnms_errno = HNMS_err_presentation_stream;
	HNMS_debug(DEBUG_SNMP, "SNMP_recv");
	ps_free(ps1);
	return;
    }

    /*
     * Decode the presentation element into an SNMP SMessage structure
     * and process it.
     */
    pe1 = ps2pe(ps1);
    if (!pe1) {
	ps_free(ps1);
	return;
    }
    rval = decode_SNMP_Message(pe1, 1, NULLCP, NULLVP, &smsg);
    ps_free(ps1);
    if (rval == NOTOK) {
	hnms_errno = HNMS_err_presentation_stream;
	HNMS_debug(DEBUG_SNMP, "SNMP_recv");
	return;
    }
    pe_free(pe1);
    SNMP_Message_process(ip_address, udp_port, smsg);
    free_SNMP_Message(smsg);
}

/*\
 *  Check if I have SNMP packets waiting at the snmp socket.  Return a
 *  positive integer if I do, 0 otherwise.
\*/
static int SNMP_ready()
{
    struct timeval	select_time;
    fd_set		snmp_read_ready;
    int			ready_descs;

    select_time.tv_sec = 0;
    select_time.tv_usec = 0;
    FD_ZERO(&snmp_read_ready);
    FD_SET(snmp_sock, &snmp_read_ready);

    if ((ready_descs = select(getdtablesize(), &snmp_read_ready, 0,
			      0, &select_time)) < 0) {
	HNMS_debug(DEBUG_SNMP,
		   "The SNMP listening port has already been claimed.");
	return 0;
    }

    return ready_descs;
}

/*\
 *  Listen for incoming SNMP packets.
\*/
int SNMP_listen()
{
    while(SNMP_ready())
	SNMP_recv();
    return 0;
}

/*\
 *  Carry out DISCOVER operations.
\*/
int SNMP_discovery_poll()
{
    OBJ_iterate(SNMP_discover_from_agent, NULL);
}

/*\
 *  Carry out SUBSCRIPTION operations.
\*/
int SNMP_sub_poll()
{
    OBJ_iterate(SNMP_poll_subscribed_vars, NULL);
}
