/*
** SellaNMS
** $Id: snmp.cxx,v 1.13 2005/08/22 02:25:42 sella Exp $
** Copyright (c) 2001-2005 Digital Genesis Software, LLC. All Rights Reserved.
** Released under the GPL Version 2 License.
** http://www.digitalgenesis.com
*/

#ifdef HAVE_CONFIG_H
	#include "config.h"
#endif

#include "snmp.h"
#include "debug.h"
#include "defaults.h"
#include "ip_util.h"
#include "snmp_util.h"
#include <math.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <values.h>
#include <unistd.h>
#include <unistd.h>
#include <pthread.h>
#include <linux/if.h>
#include <sys/timeb.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <sys/poll.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include "snmp_util.h"

#define BUF_SIZE 512

SNMP::SNMP() {
	this->name.append(" snmp");

	has_scheduler = false;
	has_execute = true;

	pthread_mutex_init(&(this->bulk_mutex), NULL);
	pthread_mutex_init(&(this->element_mutex), NULL);
}

SNMP::~SNMP() {
	pthread_mutex_destroy(&this->bulk_mutex);
	pthread_mutex_destroy(&this->element_mutex);
}

RESULT SNMP::readConfig() {
	RESULT result = SUCCESS;
	string str;
	vector<string> v;
	vector<string>::iterator v_it;
	ConfigNode node;
	struct in_addr ip;

	unsigned long tmp = 0;

	/* How often to start a polling inteval. */
	config->assignValue("interval", interval, MODULE_MONITOR_SNMP_INTERVAL_DEFAULT);

	/* Number of entries to return per bulkget request. */
	config->assignValue("bulkget-repetitions", repetitions, MODULE_MONITOR_SNMP_REPETITIONS_DEFAULT);
	if (this->repetitions < 1 || this->repetitions > 64000) {
		this->repetitions = MODULE_MONITOR_SNMP_REPETITIONS_DEFAULT;
	}

	/* Number of elements to be polled at one time. */
	config->assignValue("max-elemenets", maxElements, MODULE_MONITOR_SNMP_MAXELEMENETS_DEFAULT);
	if (this->maxElements < 1 || this->maxElements > 64000) {
		this->maxElements = MODULE_MONITOR_SNMP_MAXELEMENETS_DEFAULT;
	}

#if 0
	/* Number of packets to send to each address per interval. */
	config->assignValue("transmit", transmit, MODULE_MONITOR_SNMP_TRANSMIT_DEFAULT);

	/* Number of policy queue entries allowed to be processed at a time. (impacts memory usage) */
	config->assignValue("policy-queue-limit-max", policyQueueLimitMax, MODULE_MONITOR_SNMP_POLICYQUEUELIMITMAX_DEFAULT);

	/* The message placed in the payload of the SNMP packet. Up to 36 characters. */
	config->assignValue("payload", payloadMessage, MODULE_MONITOR_SNMP_PAYLOADMESSAGE_DEFAULT);
#endif

	if (config->assignValue("source", str)) {
		inet_pton(AF_INET, str.c_str(), &(this->source));
	} else if (config->assignValue("interface", str)) {
		source_ip(str.c_str(), &ip);
		this->source = ip.s_addr;
	} else {
		this->source = htonl(INADDR_ANY);
	}

#if 0
	/* Timemout value for an SNMP packet before it is considered lost. */
	config->assignValue("max-rtt", tmp, MODULE_MONITOR_SNMP_MAXRTT_DEFAULT);
	this->maxRTT.tv_sec = tmp / 1000;
	this->maxRTT.tv_usec = (tmp % 1000) * 1000;

	/* This option should be removed. A policy should handle this. */
	this->ignoreEdge = config->isNode("ignore-edge");

	if (config->assignValue("tos", tmp)) {
		this->tosDefault = tmp;
	} else if (config->assignValue("dscp", tmp)) {
		this->tosDefault = tmp << 2;
	} else if (config->assignValue("precedence", tmp)) {
		this->tosDefault = tmp << 5;
	} else {
		this->tosDefault = MODULE_MONITOR_SNMP_TOS_DEFAULT;
	}

	if (config->assignValue("ttl", ttl, MODULE_MONITOR_SNMP_TTL_DEFAULT)) {
		if (ttl > MAXTTL) {
			ttl = MAXTTL;
		}
	}
#endif

	/* Maxiumum allowed depth of calc_queue and policy_queue. */
	if (config->assignValue("queue-depth", queueDepth, MODULE_MONITOR_SNMP_QUEUEDEPTH_DEFAULT)) {
		if (queueDepth > 10000) {
			queueDepth = MODULE_MONITOR_SNMP_QUEUEDEPTH_DEFAULT;
		}
	}

#if 0
	/* Interval (sec) that calc_thread runs calculations on collected data and passes results to policy_thread. */
	if (config->assignValue("calc-interval", calcInterval, MODULE_MONITOR_SNMP_CALCINTERVAL_DEFAULT)) {
		if (calcInterval > 3600) {
			calcInterval = MODULE_MONITOR_SNMP_CALCINTERVAL_DEFAULT;
		}
	}

	/* Number of secs to auto adjust calcInterval by, when system is getting behind, or catches up. */
	if (config->assignValue("calc-interval-adjustment", calcIntervalAdj, MODULE_MONITOR_SNMP_CALCINTERVALADJ_DEFAULT)) {
		if (calcIntervalAdj > calcInterval) {
			calcIntervalAdj = MODULE_MONITOR_SNMP_CALCINTERVALADJ_DEFAULT;
		}
	}
#endif

	/* Setup RRDTool library. */
   if (config->assignNode("rrdtool", node) && !node.empty()) {
		rrdtool.setConfig(node);
	} else if (config->assignNode("system global rrdtool", node) && !node.empty()) {
		rrdtool.setConfig(node);
	} else {
#ifdef HAVE_LIBRRD
		LMSG("WARNING: No 'rrdtool' section found within the configuration.");
#endif
	}

	/* Add and compile input policy. */
	if (config->assignValues("policy input", v)) {
		/* This is actually just added to the begining of the output policies. */
		if (inputPolicy.addPolicies(config, v) == FAILURE) {
			LMSG("NOTICE: Failed to add/compile input policies.");

			return FAILURE;
		}
	}

	/* Add and compile output policy. */
	if (config->assignValues("policy output", v)) {
		if (outputPolicy.addPolicies(config, v) == FAILURE) {
			LMSG("NOTICE: Failed to add/compile output policies.");

			return FAILURE;
		}
	}

	/* Verify we have required policies. */
	if (outputPolicy.empty()) {
		LMSG("NOTICE: An output policy is required for this module.");

		return FAILURE;
	}

	/* Configure Storage */
	storage.setConfig(*config);

	return result;
}

RESULT SNMP::printConfig() {
	in_addr s;
	s.s_addr = this->source;

	char inet_buffer[INET_ADDRSTRLEN];

	LMSG("interval: %lu (sec)", this->interval);
	LMSG("source: %s", inet_ntop(AF_INET, &s, inet_buffer, INET_ADDRSTRLEN));
	LMSG("bulkget-repetitions: %lu", this->repetitions);
#if 0
	LMSG("max-rtt: %lu (msec)", tvtousec(this->maxRTT)/1000);
	LMSG("transmit: %lu", this->transmit);
	LMSG("payload: %s", this->payloadMessage.c_str());
	LMSG("tos: %lu (0x%02x)", this->tosDefault, this->tosDefault);
	LMSG("dscp: %lu (0x%02x)", this->tosDefault >> 2, this->tosDefault);
	LMSG("precedence: %lu (0x%02x)", this->tosDefault >> 5, this->tosDefault);
	LMSG("ttl: %lu (hops)", this->ttl);
	LMSG("calc-interval: %lu (sec)", this->calcInterval);
	LMSG("calc-interval-adjustment: %lu (sec)", this->calcIntervalAdj);
	LMSG("queue-depth: %lu", this->queueDepth);
	LMSG("policy-queue-limit-max: %lu", this->policyQueueLimitMax);
#endif

	return Monitor::printConfig();
}

RESULT SNMP::execute() {
	RESULT result = SUCCESS;
	RESULT t_result = SUCCESS;

	pthread_t thread_id = 0;
	this->module_id = (u_int16_t) pthread_self();

	bool running = true;

	slist<pthread_t> t_list;
	slist<pthread_t>::iterator it;

	if (trace.thread) {
		LMSG("SNMP::execute() starting");
	}

	/* Create the calc thread */
	if ((result = createThread(thread_id, (void* (*)(void*))SNMP::policy_thread, this)) == SUCCESS) {
		t_list.push_front(thread_id);
	}

	/* Create the storage thread */
	if (result == SUCCESS) {
		if ((result = createThread(thread_id, (void* (*)(void*))SNMP::calc_thread, this)) == SUCCESS) {
			t_list.push_front(thread_id);
		}
	}

	/* Create the transmit thread */
	if (result == SUCCESS) {
		if ((result = createThread(thread_id, (void* (*)(void*))SNMP::transmit_thread, this)) == SUCCESS) {
			t_list.push_front(thread_id);
		}
	}

	// Waits for first thread in list (last inserted). When that thread completes,
	// kills the next thread and waits for it to complete, until all complete.
	for (it = t_list.begin(); it != t_list.end();) {
		/* Wait for thread to complete. */
		if (this->getThreadResult(*it, t_result) != SUCCESS || t_result != SUCCESS) {
			result = FAILURE;
		}
		/* Kill the next thread. */
		if (++it != t_list.end()) {
			this->killThread(*it);
		}
	}

	if (trace.thread) {
		LMSG("SNMP::execute() exiting");
	}

	return result;
}

RESULT SNMP::transmit_thread(void* args) {
	RESULT result = SUCCESS;	
	STATUS status;

	bool running = true;
	struct snmp_pdu *req;
	struct timeval tv;
	int fds = 0, block = 0, active_elements;
	fd_set fdset;
	time_t last = 0;
	snmp_data_t snmp_data;

	pthread_t thread_id = pthread_self();

	SNMP *instance = (SNMP*) args;
	instance->setThreadStatus(thread_id, THREAD_RUNNING);

	if (instance->trace.thread) {
		LMSG("SNMP::transmit_thread() starting main loop");
	}

	while (running && !instance->interrupt) {
		instance->getThreadStatus(thread_id, status);
		if (status == THREAD_KILLED) {
			running = false;
			continue;
		}

		active_elements = 0;

		/* Walk the list for hosts that need to be started. */
		if (last < time(NULL)) {
			last = time(NULL);
			LMSG("Running though lists..");

			for (tx_slist_t::iterator it = instance->txList.begin(); it != instance->txList.end(); ++it) {
				LMSG("Looking at %s", it->element.c_str());

				if (it->session != NULL) { /* Is this element currently active? */
					active_elements++;
					LMSG("COUNTED AS ACTIVE (%lu)", active_elements);

					if (it->oid_done == true) { /* If current variable is complete, move to next. */
						LMSG("OID DONE. Increment to next oid");
						if (++it->oids_it == it->oids.end()) { /* Move to next and check for end. */
							LMSG("NO MORE OIDS! Closs this element off");

							//FIXME. I have a string, Bill expects a unsigned!
							//snmp_data.element = tx_element->element;
							snmp_data.stamp = 0;
							snmp_data.type = 0;
							snmp_data.variable.erase();
							snmp_data.value.erase();

							/* Send a zero'd message into the queue. */
							pthread_mutex_lock(&(instance->bulk_mutex));
							instance->bulk_queue.push(snmp_data);
							pthread_mutex_unlock(&(instance->bulk_mutex));

							/* No more oids. Reset oids and close the session. */
							it->oids_it = it->oids.begin();
							snmp_close(it->session);
							it->session = NULL;
							continue;
						}
					} else {
						LMSG("OID NOT DONE. Skip to next element");
						continue;
					}
				} else if (active_elements < instance->maxElements && it->start + instance->interval < time(NULL)) { /* Time to start this element? */
					active_elements++;
					LMSG("START UP ELEMENT!!!");
					if (instance->trace.debug) {
						LMSG("starting up elemenet %s", it->element.c_str());
					}
					/* Reset this elements fields for startup. */
					it->start = time(NULL);

					/* Create a session for this element. */
					if ((it->session = snmp_async_session(it->element, it->community, (void *)receive_callback, &(*it))) == NULL) {
						LMSG("Error: Failed to create snmp session for %s", it->element.c_str());

						/* Disable element */
						it->start = -1;

						continue;
					}
				} else {
					continue;
				}

				LMSG("PARSING %s", it->oids_it->c_str());

				/* Parse the current variable */
				it->rootlen = MAX_OID_LEN;
				if (convTxtToOid(it->root, it->rootlen, *(it->oids_it)) == false) {
					LMSG("Error: Failed to parse oid: %s", it->oids_it->c_str());

					/* Disable element */
					snmp_close(it->session);
					it->session = NULL;
					it->start = -1;

					continue;
				}

				/* Generate PDU. */
				req = snmp_pdu_create(SNMP_MSG_GETBULK);	/* Send the first GETBULK. */
				req->non_repeaters = 0;
				req->max_repetitions = instance->repetitions;

				snmp_add_null_var(req, it->root, it->rootlen);

				if (!snmp_send(it->session, req)) {
					LMSG("Failed to send!!");
				}

				LMSG("SENT!!!");
			}
		}

		LMSG("GOING INTO SELECT!!");

		/* Wait for a responce. */
		tv.tv_sec = 5;
		tv.tv_usec = 0;
		FD_ZERO(&fdset);
		LMSG("1: %lu", block);
		snmp_select_info(&fds, &fdset, &tv, &block);
		block = 0;
		LMSG("2: %lu, sec: %lu, usec: %lu", block, tv.tv_sec, tv.tv_usec);
		if ((fds = select(fds, &fdset, NULL, NULL, block ? NULL : &tv)) < 0) {
			LMSG("Error: SNMP::transmit_thread() select failed.");

			running = false;
			result = FAILURE;
			break;
		}

		if (fds) {
			LMSG("CALLING READ!");
			snmp_read(&fdset);
		} else {
			LMSG("CALLING TIMEOUT");
			snmp_timeout();
		}
	}

	instance->setThreadStatus(thread_id, THREAD_COMPLETED);

	THREAD_RETURN(result);
}

int SNMP::receive_callback(int operation, struct snmp_session *sp, int reqid, struct snmp_pdu *pdu, void *magic) {
	tx_element_t* tx_element = (tx_element_t*) magic;
	struct snmp_pdu *req;
	struct variable_list *vp;
	oid root[MAX_OID_LEN];
	size_t rootlen;

	snmp_data_t snmp_data;

	switch (operation) {
		case NETSNMP_CALLBACK_OP_RECEIVED_MESSAGE:
			for (vp = pdu->variables; vp; vp = vp->next_variable) {
				if (vp->name_length < tx_element->rootlen || memcmp(tx_element->root, vp->name, tx_element->rootlen * sizeof(oid))) {
					tx_element->oid_done = true;
					break;
				}

				//FIXME. I have a string, Bill expects a unsigned!
				//snmp_data.element = tx_element->element;
				snmp_data.stamp = time(NULL);
				snmp_data.type = vp->type;
				convOidToString(snmp_data.variable, vp->name, vp->name_length);
				convOidValueToString(snmp_data.value, vp);

//				LMSG("VAR: %s -> %s", snmp_data.variable.c_str(), snmp_data.value.c_str());

				pthread_mutex_lock(&(tx_element->instance->bulk_mutex));
				tx_element->instance->bulk_queue.push(snmp_data);
				pthread_mutex_unlock(&(tx_element->instance->bulk_mutex));

				if (tx_element->instance->trace.internal) {
					print_variable(vp->name, vp->name_length, vp);
				}

				if (vp->type == SNMP_ENDOFMIBVIEW || vp->type == SNMP_NOSUCHOBJECT || vp->type == SNMP_NOSUCHINSTANCE) {
					break;
				}

				if (vp->next_variable == NULL) { /* Request the next block? */
					LMSG("REQUESTING NEXT BLOCK");
					memmove(root, vp->name, vp->name_length * sizeof(oid));
					rootlen = vp->name_length;

					req = snmp_pdu_create(SNMP_MSG_GETBULK);  /* Send the first GETBULK. */
					req->non_repeaters = 0;
					req->max_repetitions = tx_element->instance->repetitions;

					snmp_add_null_var(req, root, rootlen);

					if (!snmp_send(tx_element->session, req)) {
						LMSG("Failed to send!!");
					}
				}
			}

			break;

		case NETSNMP_CALLBACK_OP_TIMED_OUT:
			LMSG("Timeout");
			break;

		default:
			LMSG("Default");
	}

	/* Free pdu */
	if (pdu) {
//		snmp_free_pdu(pdu);
	}

	return 1; /* Successful async call. */
}

/* PLACE HOLDER. */

RESULT SNMP::calc_thread(void* args) {
	RESULT result = SUCCESS, calc_result;	
	STATUS status;

	snmp_data_t					snmp_data;

	oid_data_t					oid_data;
	element_data_t				element_data;

	element_map_t				elements;
	element_map_t				compiled_elements;

	element_map_t::iterator	current_element;
	element_map_t::iterator	compiled_element;

	oid_map_t::iterator		current_oid;

	pthread_t thread_id = pthread_self();

	SNMP *instance = (SNMP*) args;
	instance->setThreadStatus(thread_id, THREAD_RUNNING);

	bool 						running = true;
	bool						update = true;
	char						buffer[1024];

	unsigned long			ul_x = 0;
	unsigned long			ul_y = 0;
	unsigned long long	ull_x = 0;
	unsigned long long	ull_y = 0;

	if (instance->trace.thread) {
		LMSG("SNMP::calc_thread() starting main loop");
	}

	/* Drop root privileges */
	instance->dropPriv();

	while (running && !instance->interrupt) {
		/* Drain the variable lizzard. */
		pthread_mutex_lock(&(instance->bulk_mutex));
		while (!instance->bulk_queue.empty()) {
			snmp_data = instance->bulk_queue.front();

			if (snmp_data.stamp != 0) {
				update = true;

				oid_data.type = snmp_data.type;
				oid_data.stamp = snmp_data.stamp;

				current_element = elements.find(snmp_data.element);
				if (current_element == elements.end()) {
					/* This is the first variable response for this element. */
					element_data.element = snmp_data.element;
					element_data.variables.clear();

					elements[snmp_data.element] = element_data;
					current_element = elements.find(element_data.element);
				}
				switch (oid_data.type) {
					case ASN_GAUGE:
					case ASN_COUNTER:
					case ASN_TIMETICKS:
						current_oid = current_element->second.variables.find(snmp_data.variable);
						if (current_oid != current_element->second.variables.end()) {
							ul_x = strtol(snmp_data.value.c_str(), NULL, 10);
							ul_y = strtol(current_oid->second.value.c_str(), NULL, 10);

							if (ul_x > ul_y) {
								snprintf(buffer, 1024, "%d", ul_x - ul_y);
							} else {
								snprintf(buffer, 1024, "%d", (MAXLONG - ul_y) + ul_x);
							}
							oid_data.value = buffer;
				
							current_element->second.variables[snmp_data.variable] = oid_data;
						} else {
							update = false;
						}
						break;

					case ASN_COUNTER64:
						current_oid = current_element->second.variables.find(snmp_data.variable);
						if (current_oid != current_element->second.variables.end()) {
							ull_x = strtoll(snmp_data.value.c_str(), NULL, 10);
							ull_y = strtoll(current_oid->second.value.c_str(), NULL, 10);

							if (ull_x > ull_y) {
								snprintf(buffer, 1024, "%d", ull_x - ull_y);
							} else {
								snprintf(buffer, 1024, "%d", (MAXLONG - ull_y) + ull_x);
							}
							oid_data.value = buffer;
				
							current_element->second.variables[snmp_data.variable] = oid_data;
						} else {
							update = false;
						}
						break;

					default:
						oid_data.value = snmp_data.value;
						break;

				}
				if (update) {
					compiled_element = compiled_elements.find(snmp_data.element);
					if (compiled_element == compiled_elements.end()) {
						element_data.element = snmp_data.element;
						element_data.variables.clear();

						compiled_elements[snmp_data.element] = element_data;
						compiled_element = compiled_elements.find(element_data.element);
					}
					compiled_element->second.variables[snmp_data.variable] = oid_data;
				}
				oid_data.value = snmp_data.value;
				current_element->second.variables[snmp_data.variable] = oid_data;
			} else {
				/* This element has completed it's walk. */
				compiled_element = compiled_elements.find(snmp_data.element);

				if (compiled_element != compiled_elements.end()) {
					pthread_mutex_lock(&(instance->element_mutex));

					instance->element_queue.push(compiled_element->second);
					compiled_elements.erase(snmp_data.element);

					pthread_mutex_unlock(&(instance->element_mutex));
				}
			}
			instance->bulk_queue.pop();
		}
		pthread_mutex_unlock(&(instance->bulk_mutex));

		instance->getThreadStatus(thread_id, status);
		if (status == THREAD_KILLED) {
			running = false;
		} else {
			usleep(USLEEP_LONGLOOP_TIME);
		}
	}

	if (instance->trace.thread) {
		LMSG("SNMP::calc_thread() ending main loop");
	}

	instance->setThreadStatus(thread_id, THREAD_COMPLETED);

	THREAD_RETURN(result);
}

RESULT SNMP::policy_thread(void* args) {
	RESULT result = SUCCESS;
	ACTION action;
	STATUS status;

	pthread_t thread_id = pthread_self();
	char buf[BUF_SIZE];

	bool running = true;

	SNMP *instance = (SNMP*) args;
	instance->dropPriv();
	instance->setThreadStatus(thread_id, THREAD_RUNNING);

	if (instance->trace.thread) {
		LMSG("SNMP::policy_thread() starting main loop");
	}

	instance->dropPriv();

	// Until we are asked to stop, process policy data.
	while (running && !instance->interrupt) {
		/* Drain the element lizzard. */
		pthread_mutex_lock(&(instance->element_mutex));
		if (!instance->element_queue.empty()) {
			instance->element_queue.pop();		
		}	
		pthread_mutex_unlock(&(instance->element_mutex));

		instance->getThreadStatus(thread_id, status);
		if (status == THREAD_KILLED) {
			running = false;
		} else {
			usleep(USLEEP_LONGLOOP_TIME);
		}
	}

	if (instance->trace.thread) {
		LMSG("SNMP::policy_thread() ending main loop");
	}

	instance->setThreadStatus(thread_id, THREAD_COMPLETED);

	THREAD_RETURN(result);
}

/*
** Local Variables:
** c-basic-offset: 3
** tab-width: 3
** End:
** vim600: fdm=marker
** vim: noet ts=3 sw=3
*/
