summaryrefslogtreecommitdiff
path: root/daemon/snmp-engine.c
diff options
context:
space:
mode:
Diffstat (limited to 'daemon/snmp-engine.c')
-rw-r--r--daemon/snmp-engine.c636
1 files changed, 636 insertions, 0 deletions
diff --git a/daemon/snmp-engine.c b/daemon/snmp-engine.c
new file mode 100644
index 0000000..e1bff30
--- /dev/null
+++ b/daemon/snmp-engine.c
@@ -0,0 +1,636 @@
+/*
+ * Copyright (c) 2005, Nate Nielsen
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above
+ * copyright notice, this list of conditions and the
+ * following disclaimer.
+ * * Redistributions in binary form must reproduce the
+ * above copyright notice, this list of conditions and
+ * the following disclaimer in the documentation and/or
+ * other materials provided with the distribution.
+ * * The names of contributors to this software may not be
+ * used to endorse or promote products derived from this
+ * software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
+ * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
+ * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
+ * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
+ * DAMAGE.
+ *
+ *
+ * CONTRIBUTORS
+ * Nate Nielsen <nielsen@memberwebs.com>
+ *
+ */
+
+#include "usuals.h"
+#include <errno.h>
+#include <syslog.h>
+
+#include <bsnmp/asn1.h>
+#include <bsnmp/snmp.h>
+
+#include "stringx.h"
+#include "rrdbotd.h"
+#include "server-mainloop.h"
+
+/* The socket to use */
+int snmp_socket = -1;
+
+/* The last request: start at a strange number */
+uint32_t snmp_request = 0x0A0A0A0A;
+
+/* Since we only deal with one packet at a time, global buffer */
+unsigned char snmp_buffer[0x1000];
+
+/* -----------------------------------------------------------------------------
+ * REQUESTS
+ */
+
+/* rb_request waaaaayyyyy too big */
+typedef struct _rb_request
+{
+ /* The SNMP request identifier */
+ uint32_t id;
+
+ mstime next_retry; /* Time of the next retry */
+ mstime interval; /* How long between retries */
+ mstime timeout; /* When this request times out */
+ uint sent; /* How many times we've sent */
+
+ /* The poller and host associated with this request */
+ rb_poller* poll;
+ const rb_host* host;
+
+ /* The actual request data */
+ struct snmp_pdu pdu;
+}
+rb_request;
+
+/* a scrolling window on a loop */
+static rb_request* requests = NULL;
+static int reqhigh = -1;
+static int reqlow = -1;
+static uint nrequests = 0;
+
+static rb_request*
+new_req()
+{
+ rb_request* req = NULL;
+ uint num;
+ int i, first, overlap = 0;
+
+ if(nrequests)
+ {
+ /* We allocate in a loop starting after the last allocation. */
+ for(i = (reqhigh + 1) % nrequests; i != reqhigh;
+ i = (i + 1) % nrequests)
+ {
+ /*
+ * We can overlap past reqlow, but in that case no
+ * updating reqhigh. This can happen after reallocating.
+ */
+ if(i == reqlow)
+ overlap = 1;
+
+ if(requests[i].id == 0)
+ {
+ req = &(requests[i]);
+
+ if(!overlap)
+ reqhigh = i;
+ break;
+ }
+ }
+ }
+
+ if(!req)
+ {
+ /*
+ * A note about the scrolling window and extending allocations...
+ * The only reason this works is because whenever we reallocate
+ * reqhigh and reqlow are the same.
+ */
+ ASSERT(reqlow == reqhigh);
+
+ /* Reallocate the request block */
+ /* TODO: Once we use less memory this can be higher */
+ num = nrequests ? nrequests * 2 : 32;
+ requests = (rb_request*)realloc(requests, sizeof(rb_request) * num);
+ if(!requests)
+ {
+ /* Note we leave old requests allocated */
+ errno = ENOMEM;
+ return NULL;
+ }
+
+ /* Clear out the new ones */
+ memset(requests + nrequests, 0, sizeof(rb_request) * (num - nrequests));
+
+ /* We return the next one */
+ req = requests + nrequests;
+
+ nrequests = num;
+
+ if(reqhigh == -1)
+ reqhigh = 0;
+ if(reqlow == -1)
+ reqlow = nrequests - 1;
+ }
+
+ /* A incrementing counter for each request */
+ req->id = snmp_request++;
+ return req;
+}
+
+static rb_request*
+find_req(uint32_t id)
+{
+ int i, first;
+
+ if(!nrequests)
+ return NULL;
+
+ /*
+ * Search backwards from the in the scrolling window. This gives
+ * us as high performance for the high performing pollers and
+ * less performance for the low ones.
+ */
+ for(i = reqhigh, first = 1; first || i != reqlow;
+ i = (i ? i : nrequests) - 1)
+ {
+ if(id == requests[i].id)
+ return &(requests[i]);
+ first = 0;
+ }
+
+ return NULL;
+}
+
+static void
+free_req(rb_request* req)
+{
+ int i;
+
+ memset(req, 0, sizeof(*req));
+
+ /* Update the bottom of the scrolling loop */
+ for(i = reqlow; i != reqhigh; i = (i + 1) % nrequests)
+ {
+ /* If used then done */
+ if(requests[i].id)
+ break;
+
+ /* reqlow is not inclusive */
+ reqlow = i;
+ }
+}
+
+/* -----------------------------------------------------------------------------
+ * PACKET HANDLING
+ */
+
+static void
+finish_poll(rb_poller* poll, mstime when)
+{
+#ifdef _DEBUG
+ {
+ rb_item* it;
+ for(it = poll->items; it; it = it->next)
+ ASSERT(!it->req);
+ }
+#endif
+
+ /* Update the book-keeping */
+ poll->last_polled = when;
+
+ rb_messagex(LOG_DEBUG, "collected poll values. sending them to rrd");
+
+ /* And send off our collection of values */
+ rb_rrd_update(poll);
+}
+
+static void
+send_req(rb_request* req, mstime when)
+{
+ struct asn_buf b;
+ ssize_t ret;
+
+ b.asn_ptr = snmp_buffer;
+ b.asn_len = sizeof(snmp_buffer);
+
+ if(snmp_pdu_encode(&(req->pdu), &b))
+ rb_message(LOG_CRIT, "couldn't encode snmp buffer");
+ else
+ {
+ ret = sendto(snmp_socket, snmp_buffer, b.asn_ptr - snmp_buffer, 0,
+ &SANY_ADDR(req->host->address), SANY_LEN(req->host->address));
+ if(ret == -1)
+ rb_message(LOG_ERR, "couldn't send snmp packet to: %s", req->host->name);
+ else
+ rb_messagex(LOG_DEBUG, "sent request '%d' to: %s", req->id, req->host->name);
+ }
+
+ /* And update our bookkeeping */
+ req->sent++;
+ if(req->sent <= g_state.retries)
+ req->next_retry = when + req->interval;
+}
+
+static void
+timeout_req(rb_request* req, mstime when)
+{
+ rb_poller* poll = req->poll;
+ int incomplete = 0;
+ rb_item* it;
+
+ ASSERT(poll);
+
+ /*
+ * Marks of this requests items as unknown. Request is
+ * over, free. See if poller is done
+ */
+
+ for(it = poll->items; it; it = it->next)
+ {
+ if(it->req == req)
+ {
+ rb_messagex(LOG_DEBUG, "value for field '%s' timed out", it->rrdfield);
+
+ it->value = RB_UNKNOWN;
+ it->req = NULL;
+ }
+
+ else if(it->req)
+ incomplete = 1;
+ }
+
+ free_req(req);
+
+ if(!incomplete)
+ finish_poll(poll, when);
+}
+
+static void
+check_req(rb_request* req, mstime when)
+{
+ ASSERT(req->id);
+
+ /* See if it's timed out */
+ if(when >= req->timeout)
+ timeout_req(req, when);
+
+ if(!req->next_retry)
+ return;
+
+ /* Resend if necessary */
+ if(when >= req->next_retry)
+ send_req(req, when);
+}
+
+static void
+respond_req(rb_request* req, struct snmp_pdu* pdu, mstime when)
+{
+ struct snmp_value* value;
+ rb_poller* poll = req->poll;
+ rb_item* item;
+ int incomplete = 0;
+ int i;
+
+ ASSERT(req->id == pdu->request_id);
+
+ for(i = 0; i < pdu->nbindings; i++)
+ {
+ value = &(pdu->bindings[i]);
+
+ for(item = poll->items; item; item = item->next)
+ {
+ if(asn_compare_oid(&(value->var), &(item->snmpfield.var)) == 0)
+ {
+ const char *msg = NULL;
+ switch(value->syntax)
+ {
+ case SNMP_SYNTAX_NULL:
+ item->value = RB_UNKNOWN;
+ break;
+ case SNMP_SYNTAX_INTEGER:
+ item->value = value->v.integer;
+ break;
+ case SNMP_SYNTAX_COUNTER:
+ case SNMP_SYNTAX_GAUGE:
+ case SNMP_SYNTAX_TIMETICKS:
+ item->value = value->v.uint32;
+ break;
+ case SNMP_SYNTAX_COUNTER64:
+ item->value = value->v.counter64;
+ break;
+ case SNMP_SYNTAX_OCTETSTRING:
+ case SNMP_SYNTAX_OID:
+ case SNMP_SYNTAX_IPADDRESS:
+ msg = "snmp server returned non numeric value for field: %s";
+ break;
+ case SNMP_SYNTAX_NOSUCHOBJECT:
+ case SNMP_SYNTAX_NOSUCHINSTANCE:
+ case SNMP_SYNTAX_ENDOFMIBVIEW:
+ msg = "value not available on snmp server for field: %s";
+ break;
+ default:
+ msg = "snmp server returned invalid or unsupported value for field: %s";
+ break;
+ }
+
+ if(msg)
+ rb_messagex(LOG_WARNING, msg, item->rrdfield);
+ else
+ rb_messagex(LOG_DEBUG, "got value for field '%s': %0.2f",
+ item->rrdfield, item->value);
+
+ /* Mark this value as done */
+ item->req = NULL;
+ break;
+ }
+ }
+ }
+
+ /* We're done with this request */
+ free_req(req);
+
+ /* Now see if the entire request is done */
+ for(item = poll->items; item; item = item->next)
+ {
+ if(item->req)
+ return;
+ }
+
+
+ /* And if so then hand off */
+ finish_poll(poll, when);
+}
+
+static int
+poller_timer(mstime when, void* arg)
+{
+ rb_poller* poll = (rb_poller*)arg;
+ const rb_host* last_host = NULL;
+ rb_request* req = NULL;
+ rb_item* it;
+
+ /*
+ * If the previous poll has not completed, then we count it
+ * as a timeout.
+ */
+ for(it = poll->items; it; it = it->next)
+ {
+ if(it->req)
+ {
+ ASSERT(it->req->poll == poll);
+ timeout_req(it->req, when);
+ }
+
+ /* timeout_req above should have cleared this */
+ ASSERT(!it->req);
+ }
+
+
+ for(it = poll->items; it; it = it->next)
+ {
+ /*
+ * We assume that the polled items are sorted by host. Done
+ * in config.c. This allows us to fire off the least amount
+ * of requests. Generate new requests when:
+ *
+ * - first or new host
+ * - too many items in the same request
+ */
+ if(!req || it->host != last_host ||
+ req->pdu.nbindings >= SNMP_MAX_BINDINGS)
+ {
+ /* Send off last request ... */
+ if(req)
+ send_req(req, when);
+
+ /* ... and make a new one */
+ req = new_req();
+ if(!req)
+ {
+ rb_message(LOG_CRIT, "couldn't allocate a new snmp request");
+ return 1;
+ }
+
+ req->poll = poll;
+ req->host = it->host;
+
+ rb_messagex(LOG_DEBUG, "preparing request '%d' for: %s@%s",
+ req->id, req->host->community, req->host->name);
+
+ /* Setup the packet */
+ strlcpy(req->pdu.community, req->host->community, sizeof(req->pdu.community));
+ req->pdu.request_id = req->id;
+ req->pdu.version = req->host->version;
+ req->pdu.type = SNMP_PDU_GET;
+ req->pdu.error_status = 0;
+ req->pdu.error_index = 0;
+ req->pdu.nbindings = 0;
+
+ /* Send interval is 200 ms when poll interval is below 2 seconds */
+ req->interval = (poll->interval <= 2000) ? 200L : 600L;
+
+ /* Timeout is for the last packet sent, not first */
+ req->timeout = when + (req->interval * ((mstime)g_state.retries)) + poll->timeout;
+ req->sent = 0;
+
+ last_host = it->host;
+ }
+
+ /* Add an item to this request */
+ req->pdu.bindings[req->pdu.nbindings].var = it->snmpfield.var;
+ req->pdu.bindings[req->pdu.nbindings].syntax = it->snmpfield.syntax;
+ req->pdu.nbindings++;
+
+ /* Mark item as active by this request */
+ it->req = req;
+ it->value = RB_UNKNOWN;
+ }
+
+ if(req)
+ send_req(req, when);
+
+ return 1;
+}
+
+
+static void
+receive_resp(int fd, int type, void* arg)
+{
+ char hostname[MAXPATHLEN];
+ struct sockaddr_any from;
+ struct snmp_pdu pdu;
+ struct asn_buf b;
+ rb_request* req;
+ int len, ret;
+ int32_t ip;
+
+ ASSERT(snmp_socket == fd);
+
+ /* Read in the packet */
+
+ SANY_LEN(from) = sizeof(from);
+ len = recvfrom(snmp_socket, snmp_buffer, sizeof(snmp_buffer), 0,
+ &SANY_ADDR(from), &SANY_LEN(from));
+ if(len < 0)
+ {
+ if(errno != EAGAIN && errno != EWOULDBLOCK)
+ rb_message(LOG_ERR, "error receiving snmp packet from network");
+ return;
+ }
+
+
+ if(sock_any_ntop(&from, hostname, MAXPATHLEN, 0) == -1)
+ strcpy(hostname, "[UNKNOWN]");
+
+ rb_messagex(LOG_DEBUG, "received packet from: %s", hostname);
+
+ /* Now parse the packet */
+
+ b.asn_ptr = snmp_buffer;
+ b.asn_len = len;
+
+ ret = snmp_pdu_decode(&b, &pdu, &ip);
+ if(ret != SNMP_CODE_OK)
+ {
+ rb_message(LOG_WARNING, "invalid snmp packet received from: %s", hostname);
+ return;
+ }
+
+ /* Lookup and check the request properly */
+
+ req = find_req(pdu.request_id);
+ if(!req)
+ return;
+
+ if(pdu.error_status != SNMP_ERR_NOERROR)
+ {
+ /* TODO: Textual errors */
+ rb_message(LOG_ERR, "snmp error '%d' from host: %s", pdu.error_status, hostname);
+ return;
+ }
+
+ if(pdu.version != req->pdu.version)
+ rb_message(LOG_WARNING, "wrong version snmp packet from: %s", hostname);
+
+ rb_messagex(LOG_DEBUG, "response to request '%d' from: %s", req->id, hostname);
+ respond_req(req, &pdu, server_get_time());
+}
+
+static int
+resend_timer(mstime when, void* arg)
+{
+ int i;
+
+ /* Search forwards through the scrolling window */
+ for(i = (reqlow + 1) % nrequests; i <= reqhigh;
+ i = (i + 1) % nrequests)
+ {
+ if(requests[i].id)
+ check_req(&(requests[i]), when);
+ }
+
+ return 1;
+}
+
+static int
+prep_timer(mstime when, void* arg)
+{
+ /*
+ * We don't prepare all timers at exactly the same time
+ * but we sort of randomly start the various timers. We're
+ * going to be hitting these over and over again, so there's
+ * lots of benefits to spreading them out randomly over a
+ * few seconds.
+ */
+
+ rb_poller* poll;
+ int next;
+
+ /* All done? */
+ if(!arg)
+ return 0;
+
+ poll = (rb_poller*)arg;
+ if(server_timer(poll->interval, poller_timer, poll) == -1)
+ rb_message(LOG_CRIT, "couldn't setup poller timer");
+
+ /* Setup the next poller anywhere between 0 and 750 ms */
+ next = rand() % 750;
+ server_oneshot(next, prep_timer, poll->next);
+ return 0;
+}
+
+
+void
+rb_snmp_engine_init()
+{
+ struct sockaddr_in addr;
+ rb_request* req;
+
+ ASSERT(snmp_socket == -1);
+ snmp_socket = socket(PF_INET, SOCK_DGRAM, 0);
+ if(snmp_socket < 0)
+ err(1, "couldn't open snmp socket");
+
+ /* Get a random IPv4 UDP socket for client use */
+ memset(&addr, 0, sizeof(addr));
+ addr.sin_family = AF_INET;
+
+ if(bind(snmp_socket, (struct sockaddr*)&addr, sizeof(addr)) < 0)
+ err(1, "couldn't listen on port");
+
+ if (server_watch(snmp_socket, SERVER_READ, receive_resp, NULL) == -1)
+ err(1, "couldn't listen on socket");
+
+ /* Allocate some requests to make sure we have memory */
+ req = new_req();
+ if(!req)
+ err(1, "out of memory");
+ free_req(req);
+
+ /* Start the preparation timers for setting up randomly */
+ if(server_oneshot(100, prep_timer, g_state.polls) == -1)
+ err(1, "couldn't setup timer");
+
+ /* We fire off the resend timer every 1/5 second */
+ if(server_timer(200, resend_timer, NULL) == -1)
+ err(1, "couldn't setup timer");
+}
+
+void
+rb_snmp_engine_uninit()
+{
+ if(snmp_socket != -1)
+ {
+ server_unwatch(snmp_socket);
+ close(snmp_socket);
+ snmp_socket = -1;
+ }
+
+ if(requests)
+ {
+ free(requests);
+ nrequests = 0;
+ reqhigh = 0;
+ reqlow = 0;
+ }
+}